Updated posts design
This commit is contained in:
parent
18c793d8c6
commit
4ad2c214f0
|
@ -7,7 +7,6 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
B9029FC22B81259400AA9B68 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = B9029FC12B81259400AA9B68 /* Secret.plist */; };
|
|
||||||
B9029FC42B8125CE00AA9B68 /* HuggingFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */; };
|
B9029FC42B8125CE00AA9B68 /* HuggingFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */; };
|
||||||
B90DEB1F2C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; };
|
B90DEB1F2C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; };
|
||||||
B90DEB202C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; };
|
B90DEB202C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; };
|
||||||
|
@ -272,7 +271,6 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
B9029FC12B81259400AA9B68 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secret.plist; sourceTree = "<group>"; };
|
|
||||||
B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFace.swift; sourceTree = "<group>"; };
|
B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFace.swift; sourceTree = "<group>"; };
|
||||||
B90DEB1E2C822C2700D06121 /* StatusDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDraft.swift; sourceTree = "<group>"; };
|
B90DEB1E2C822C2700D06121 /* StatusDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDraft.swift; sourceTree = "<group>"; };
|
||||||
B90DEB212C822ED400D06121 /* PostDraftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDraftView.swift; sourceTree = "<group>"; };
|
B90DEB212C822ED400D06121 /* PostDraftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDraftView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -587,7 +585,6 @@
|
||||||
children = (
|
children = (
|
||||||
B9C20D592B923D53004DC9B3 /* Bubble.entitlements */,
|
B9C20D592B923D53004DC9B3 /* Bubble.entitlements */,
|
||||||
B9FB94A02B2EF23100D81C07 /* Info.plist */,
|
B9FB94A02B2EF23100D81C07 /* Info.plist */,
|
||||||
B9029FC12B81259400AA9B68 /* Secret.plist */,
|
|
||||||
B9FB945A2B2DEECE00D81C07 /* BubbleApp.swift */,
|
B9FB945A2B2DEECE00D81C07 /* BubbleApp.swift */,
|
||||||
B9EBE8572B474FD600FB594D /* AppDelegate.swift */,
|
B9EBE8572B474FD600FB594D /* AppDelegate.swift */,
|
||||||
B9FB946E2B2DF3BB00D81C07 /* Components */,
|
B9FB946E2B2DF3BB00D81C07 /* Components */,
|
||||||
|
@ -873,7 +870,6 @@
|
||||||
B9DC69302B79378400E625B9 /* BubblePlus.storekit in Resources */,
|
B9DC69302B79378400E625B9 /* BubblePlus.storekit in Resources */,
|
||||||
B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */,
|
B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */,
|
||||||
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */,
|
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */,
|
||||||
B9029FC22B81259400AA9B68 /* Secret.plist in Resources */,
|
|
||||||
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */,
|
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */,
|
||||||
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */,
|
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */,
|
||||||
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */,
|
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */,
|
||||||
|
|
|
@ -7,7 +7,9 @@ import RevenueCat
|
||||||
@main
|
@main
|
||||||
struct BubbleApp: App {
|
struct BubbleApp: App {
|
||||||
init() {
|
init() {
|
||||||
guard let plist = AppDelegate.readSecret() else { fatalError("Missing Secret.plist file") }
|
BubbleShortcuts.updateAppShortcutParameters() //might not work?
|
||||||
|
|
||||||
|
guard let plist = AppDelegate.readSecret() else { print("Missing Secret.plist file"); return }
|
||||||
|
|
||||||
if let apiKey = plist["RevenueCat_public"], let deviceId = UIDevice.current.identifierForVendor?.uuidString {
|
if let apiKey = plist["RevenueCat_public"], let deviceId = UIDevice.current.identifierForVendor?.uuidString {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
@ -15,8 +17,6 @@ struct BubbleApp: App {
|
||||||
#endif
|
#endif
|
||||||
Purchases.configure(withAPIKey: apiKey, appUserID: deviceId)
|
Purchases.configure(withAPIKey: apiKey, appUserID: deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
BubbleShortcuts.updateAppShortcutParameters() //might not work?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|
|
@ -23,20 +23,14 @@ struct CompactPostView: View {
|
||||||
@State private var quoteStatus: Status? = nil
|
@State private var quoteStatus: Status? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
notices
|
notices
|
||||||
|
|
||||||
statusPost(status.reblogAsAsStatus ?? status)
|
statusPost(status.reblogAsAsStatus ?? status)
|
||||||
|
|
||||||
if !quoted && !imaging {
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color.gray.opacity(0.2))
|
|
||||||
.frame(width: .infinity, height: 1)
|
|
||||||
.padding(.bottom, 3)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.withCovers(sheetDestination: $navigator.presentedCover)
|
.withCovers(sheetDestination: $navigator.presentedCover)
|
||||||
.containerShape(Rectangle())
|
.containerShape(Rectangle())
|
||||||
|
.padding(.vertical, 6.0)
|
||||||
.background(postBackground())
|
.background(postBackground())
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
PostMenu(status: status)
|
PostMenu(status: status)
|
||||||
|
@ -300,7 +294,13 @@ struct CompactPostView: View {
|
||||||
)
|
)
|
||||||
.opacity(0.2)
|
.opacity(0.2)
|
||||||
} else {
|
} else {
|
||||||
Color.appBackground
|
// Color.appBackground
|
||||||
|
LinearGradient(
|
||||||
|
stops: [.init(color: Color.subClub, location: 0.0), .init(color: Color.appBackground, location: 0.2)],
|
||||||
|
startPoint: .topTrailing,
|
||||||
|
endPoint: .bottomLeading
|
||||||
|
)
|
||||||
|
.opacity(0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,20 +27,25 @@ struct PostDetailsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 1.5) {
|
||||||
if statuses.isEmpty {
|
if statuses.isEmpty {
|
||||||
statusPost(detailedStatus)
|
CompactPostView(status: detailedStatus)
|
||||||
} else {
|
} else {
|
||||||
ForEach(statuses) { status in
|
ForEach(statuses) { status in
|
||||||
|
let isLast: Bool = status.id == statuses.last?.id ?? ""
|
||||||
|
|
||||||
if status.id == detailedStatus.id {
|
if status.id == detailedStatus.id {
|
||||||
statusPost(detailedStatus)
|
CompactPostView(status: detailedStatus)
|
||||||
.padding(.horizontal, 15)
|
|
||||||
.padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical)
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom)
|
proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CompactPostView(status: status)
|
repPost(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLast {
|
||||||
|
Divider()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,82 +60,18 @@ struct PostDetailsView: View {
|
||||||
.safeAreaPadding()
|
.safeAreaPadding()
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func statusPost(_ status: Status) -> some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
HStack {
|
|
||||||
profilePicture
|
|
||||||
.onTapGesture {
|
|
||||||
navigator.navigate(to: .account(acc: status.account))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(status.account.username)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.bold()
|
|
||||||
.onTapGesture {
|
|
||||||
navigator.navigate(to: .account(acc: status.account))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Menu {
|
@ViewBuilder
|
||||||
PostMenu(status: status)
|
func repPost(_ status: Status) -> some View {
|
||||||
} label: {
|
HStack(alignment: .center, spacing: 8.0) {
|
||||||
Image(systemName: "ellipsis")
|
Rectangle()
|
||||||
.foregroundStyle(Color.white.opacity(0.3))
|
.fill(Color.gray.opacity(0.4))
|
||||||
.font(.body)
|
.frame(maxWidth: 2.0, maxHeight: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
|
||||||
.padding(7.5)
|
CompactPostView(status: status)
|
||||||
}
|
|
||||||
.padding([.trailing, .top])
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
// MARK: Status main content
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
if !status.content.asRawText.isEmpty {
|
|
||||||
TextEmoji(status.content, emojis: status.emojis, language: status.language)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.frame(width: 300, alignment: .topLeading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.font(.callout)
|
|
||||||
.id("\(detailedStatus.id)@\(detailedStatus.account.id)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.poll != nil {
|
|
||||||
PostPoll(poll: status.poll!)
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.card != nil && status.mediaAttachments.isEmpty {
|
|
||||||
PostCardView(card: status.card!)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !status.mediaAttachments.isEmpty {
|
|
||||||
ForEach(status.mediaAttachments) { attachment in
|
|
||||||
PostAttachment(attachment: attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasQuote {
|
|
||||||
if quoteStatus != nil {
|
|
||||||
QuotePostView(status: quoteStatus!)
|
|
||||||
} else {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(.circular)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: Action buttons
|
|
||||||
PostInteractor(status: status, isLiked: $isLiked, isReposted: $isReposted, isBookmarked: $isBookmarked)
|
|
||||||
|
|
||||||
// MARK: Status stats
|
|
||||||
stats.padding(.top, 5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchStatusDetail() async {
|
private func fetchStatusDetail() async {
|
||||||
guard let client = accountManager.getClient() else { return }
|
guard let client = accountManager.getClient() else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -160,74 +101,7 @@ struct PostDetailsView: View {
|
||||||
let status: Status
|
let status: Status
|
||||||
let context: StatusContext
|
let context: StatusContext
|
||||||
}
|
}
|
||||||
|
|
||||||
var profilePicture: some View {
|
|
||||||
if detailedStatus.reblog != nil {
|
|
||||||
OnlineImage(url: detailedStatus.reblog!.account.avatar, size: 50, useNuke: true)
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.clipShape(.circle)
|
|
||||||
} else {
|
|
||||||
OnlineImage(url: detailedStatus.account.avatar, size: 50, useNuke: true)
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.clipShape(.circle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats: some View {
|
|
||||||
//MARK: I acknowledge the existance of a count bug here
|
|
||||||
if detailedStatus.reblog == nil {
|
|
||||||
HStack {
|
|
||||||
if detailedStatus.repliesCount > 0 {
|
|
||||||
Text("status.replies-\(detailedStatus.repliesCount)")
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if detailedStatus.repliesCount > 0 && (detailedStatus.favouritesCount > 0 || isLiked) {
|
|
||||||
Text(verbatim: "•")
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if detailedStatus.favouritesCount > 0 || isLiked {
|
|
||||||
let likeCount: Int = detailedStatus.favouritesCount - (initialLike ? 1 : 0)
|
|
||||||
let incrLike: Int = isLiked ? 1 : 0
|
|
||||||
Text("status.favourites-\(likeCount + incrLike)")
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
.contentTransition(.numericText(value: Double(likeCount + incrLike)))
|
|
||||||
.transaction { t in
|
|
||||||
t.animation = .default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
if detailedStatus.reblog!.repliesCount > 0 {
|
|
||||||
Text("status.replies-\(detailedStatus.reblog!.repliesCount)")
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if detailedStatus.reblog!.repliesCount > 0 && (detailedStatus.reblog!.favouritesCount > 0 || isLiked) {
|
|
||||||
Text(verbatim: "•")
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if detailedStatus.reblog!.favouritesCount > 0 || isLiked {
|
|
||||||
let likeCount: Int = detailedStatus.reblog!.favouritesCount - (initialLike ? 1 : 0)
|
|
||||||
let incrLike: Int = isLiked ? 1 : 0
|
|
||||||
Text("status.favourites-\(likeCount + incrLike)")
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
.contentTransition(.numericText(value: Double(likeCount + incrLike)))
|
|
||||||
.transaction { t in
|
|
||||||
t.animation = .default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func embededStatusURL() -> URL? {
|
private func embededStatusURL() -> URL? {
|
||||||
let content = detailedStatus.content
|
let content = detailedStatus.content
|
||||||
if let client = accountManager.getClient() {
|
if let client = accountManager.getClient() {
|
||||||
|
|
|
@ -4,33 +4,33 @@ import SwiftUI
|
||||||
|
|
||||||
struct PostsView: View {
|
struct PostsView: View {
|
||||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||||
// @EnvironmentObject private var navigator: Navigator
|
// @EnvironmentObject private var navigator: Navigator
|
||||||
|
|
||||||
@State private var showPicker: Bool = false
|
@State private var showPicker: Bool = false
|
||||||
@State private var timelines: [TimelineFilter] = [.home, .trending, .local, .federated]
|
@State private var timelines: [TimelineFilter] = [.home, .trending, .local, .federated]
|
||||||
|
|
||||||
@State private var loadingStatuses: Bool = false
|
@State private var loadingStatuses: Bool = false
|
||||||
@State private var statuses: [Status]?
|
@State private var statuses: [Status]?
|
||||||
@State private var lastSeen: Int?
|
@State private var lastSeen: Int?
|
||||||
|
|
||||||
@State var filter: TimelineFilter
|
@State var filter: TimelineFilter
|
||||||
@State var showHero: Bool = false
|
@State var showHero: Bool = false
|
||||||
@State var timelineModel: FetchTimeline // home timeline by default
|
@State var timelineModel: FetchTimeline // home timeline by default
|
||||||
|
|
||||||
init(timelineModel: FetchTimeline, filter: TimelineFilter, showHero: Bool = false) {
|
init(timelineModel: FetchTimeline, filter: TimelineFilter, showHero: Bool = false) {
|
||||||
self.timelineModel = timelineModel
|
self.timelineModel = timelineModel
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
self.timelineModel.setTimelineFilter(filter)
|
self.timelineModel.setTimelineFilter(filter)
|
||||||
self.showHero = showHero
|
self.showHero = showHero
|
||||||
}
|
}
|
||||||
|
|
||||||
init(filter: TimelineFilter, showHero: Bool = false) {
|
init(filter: TimelineFilter, showHero: Bool = false) {
|
||||||
self.timelineModel = .init(client: AccountManager.shared.forceClient())
|
self.timelineModel = .init(client: AccountManager.shared.forceClient())
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
self.timelineModel.setTimelineFilter(filter)
|
self.timelineModel.setTimelineFilter(filter)
|
||||||
self.showHero = showHero
|
self.showHero = showHero
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if statuses != nil {
|
if statuses != nil {
|
||||||
|
@ -49,54 +49,12 @@ struct PostsView: View {
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if showPicker {
|
if showPicker {
|
||||||
ViewThatFits {
|
picker
|
||||||
HStack {
|
|
||||||
ForEach(timelines, id: \.self) { t in
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
await reloadTimeline(t)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text(t.localizedTitle())
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.buttonStyle(LargeButton(filled: t == filter, height: 7.5))
|
|
||||||
.disabled(t == filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView(.horizontal) {
|
|
||||||
HStack {
|
|
||||||
ForEach(timelines, id: \.self) { t in
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
await reloadTimeline(t)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text(t.localizedTitle())
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.buttonStyle(LargeButton(filled: t == filter, height: 7.5))
|
|
||||||
.disabled(t == filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical)
|
|
||||||
.scrollIndicators(.hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(statuses!, id: \.id) { status in
|
|
||||||
LazyVStack(alignment: .leading, spacing: 2) {
|
|
||||||
CompactPostView(status: status)
|
|
||||||
.onDisappear {
|
|
||||||
guard statuses != nil else { return }
|
|
||||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
if let client = accountManager.getClient() {
|
if let client = accountManager.getClient() {
|
||||||
|
@ -118,46 +76,10 @@ struct PostsView: View {
|
||||||
// .padding(.top)
|
// .padding(.top)
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
emptyView
|
||||||
Color.appBackground
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
Image("HeroIcon")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 50)
|
|
||||||
.padding(.bottom)
|
|
||||||
|
|
||||||
ContentUnavailableView {
|
|
||||||
Text("timeline.empty")
|
|
||||||
.bold()
|
|
||||||
} description: {
|
|
||||||
Text("timeline.empty.description")
|
|
||||||
}
|
|
||||||
.scrollDisabled(true)
|
|
||||||
}
|
|
||||||
.scrollDisabled(true)
|
|
||||||
.background(Color.appBackground)
|
|
||||||
.frame(height: 200)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
loadingView
|
||||||
Color.appBackground
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.onAppear {
|
|
||||||
timelineModel.setTimelineFilter(filter)
|
|
||||||
if let client = accountManager.getClient() {
|
|
||||||
Task {
|
|
||||||
statuses = await timelineModel.fetch(client: client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(.circular)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(filter.localizedTitle())
|
.navigationTitle(filter.localizedTitle())
|
||||||
|
@ -165,7 +87,112 @@ struct PostsView: View {
|
||||||
.toolbarBackground(Color.appBackground, for: .navigationBar)
|
.toolbarBackground(Color.appBackground, for: .navigationBar)
|
||||||
.toolbarBackground(.visible, for: .navigationBar)
|
.toolbarBackground(.visible, for: .navigationBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var posts: some View {
|
||||||
|
ForEach(statuses!, id: \.id) { status in
|
||||||
|
let isLast: Bool = status.id == statuses!.last?.id ?? ""
|
||||||
|
|
||||||
|
LazyVStack(alignment: .leading, spacing: 0.0) {
|
||||||
|
CompactPostView(status: status)
|
||||||
|
.onDisappear {
|
||||||
|
guard statuses != nil else { return }
|
||||||
|
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLast {
|
||||||
|
Divider()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var picker: some View {
|
||||||
|
ViewThatFits {
|
||||||
|
HStack {
|
||||||
|
ForEach(timelines, id: \.self) { t in
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await reloadTimeline(t)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(t.localizedTitle())
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.buttonStyle(LargeButton(filled: t == filter, height: 7.5))
|
||||||
|
.disabled(t == filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
HStack {
|
||||||
|
ForEach(timelines, id: \.self) { t in
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await reloadTimeline(t)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(t.localizedTitle())
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.buttonStyle(LargeButton(filled: t == filter, height: 7.5))
|
||||||
|
.disabled(t == filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var emptyView: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.appBackground
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Image("HeroIcon")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 50)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
ContentUnavailableView {
|
||||||
|
Text("timeline.empty")
|
||||||
|
.bold()
|
||||||
|
} description: {
|
||||||
|
Text("timeline.empty.description")
|
||||||
|
}
|
||||||
|
.scrollDisabled(true)
|
||||||
|
}
|
||||||
|
.scrollDisabled(true)
|
||||||
|
.background(Color.appBackground)
|
||||||
|
.frame(height: 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var loadingView: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.appBackground
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onAppear {
|
||||||
|
timelineModel.setTimelineFilter(filter)
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
Task {
|
||||||
|
statuses = await timelineModel.fetch(client: client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func reloadTimeline(_ filter: TimelineFilter) async {
|
private func reloadTimeline(_ filter: TimelineFilter) async {
|
||||||
guard let client = accountManager.getClient() else { return }
|
guard let client = accountManager.getClient() else { return }
|
||||||
statuses = nil
|
statuses = nil
|
||||||
|
|
|
@ -37,113 +37,133 @@ struct TimelineView: View {
|
||||||
NavigationStack(path: $navigator.path) {
|
NavigationStack(path: $navigator.path) {
|
||||||
if statuses != nil {
|
if statuses != nil {
|
||||||
if !statuses!.isEmpty {
|
if !statuses!.isEmpty {
|
||||||
ScrollView(showsIndicators: false) {
|
statusesView
|
||||||
picker
|
|
||||||
|
|
||||||
ForEach(statuses!, id: \.id) { status in
|
|
||||||
LazyVStack(alignment: .leading, spacing: 2) {
|
|
||||||
CompactPostView(status: status)
|
|
||||||
.onDisappear {
|
|
||||||
guard statuses != nil else { return }
|
|
||||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.refreshable {
|
|
||||||
if let client = accountManager.getClient() {
|
|
||||||
statuses = nil
|
|
||||||
|
|
||||||
Task {
|
|
||||||
loadingStatuses = true
|
|
||||||
statuses = await timelineModel.fetch(client: client)
|
|
||||||
loadingStatuses = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: lastSeen ?? 0) { _, new in
|
|
||||||
guard !loadingStatuses else { return }
|
|
||||||
Task {
|
|
||||||
loadingStatuses = true
|
|
||||||
statuses = await timelineModel.addStatuses(lastStatusIndex: new)
|
|
||||||
loadingStatuses = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
|
||||||
ToolbarItem(placement: .primaryAction) {
|
|
||||||
Button {
|
|
||||||
statuses = nil
|
|
||||||
|
|
||||||
Task {
|
|
||||||
loadingStatuses = true
|
|
||||||
statuses = await self.timelineModel.toggleContentFilter(filter: wordsFilter)
|
|
||||||
loadingStatuses = false
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: self.timelineModel.filtering ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
|
|
||||||
.symbolEffect(.pulse.wholeSymbol, isActive: self.timelineModel.filtering)
|
|
||||||
}
|
|
||||||
.tint(Color(uiColor: UIColor.label))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
.background(Color.appBackground)
|
|
||||||
.withAppRouter(navigator)
|
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
emptyView
|
||||||
Color.appBackground
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
picker
|
|
||||||
|
|
||||||
ContentUnavailableView {
|
|
||||||
Text("timeline.empty")
|
|
||||||
.bold()
|
|
||||||
} description: {
|
|
||||||
Text("timeline.empty.description")
|
|
||||||
}
|
|
||||||
.scrollDisabled(true)
|
|
||||||
}
|
|
||||||
.scrollDisabled(true)
|
|
||||||
.background(Color.appBackground)
|
|
||||||
.frame(height: 200)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
loadingView
|
||||||
Color.appBackground
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.onAppear {
|
|
||||||
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
|
||||||
self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
if let client = accountManager.getClient() {
|
|
||||||
Task {
|
|
||||||
statuses = await timelineModel.fetch(client: client)
|
|
||||||
|
|
||||||
if UserDefaults.standard.bool(forKey: "autoOnFilter") {
|
|
||||||
statuses = await self.timelineModel.useContentFilter(wordsFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(.circular)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(navigator)
|
.environmentObject(navigator)
|
||||||
.background(Color.appBackground)
|
.background(Color.appBackground)
|
||||||
.toolbarBackground(Color.appBackground, for: .navigationBar)
|
.toolbarBackground(Color.appBackground, for: .navigationBar)
|
||||||
.toolbarBackground(.visible, for: .navigationBar)
|
.toolbarBackground(.visible, for: .navigationBar)
|
||||||
.safeAreaPadding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Views
|
||||||
|
private var statusesView: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
picker
|
||||||
|
|
||||||
|
ForEach(statuses!, id: \.id) { status in
|
||||||
|
let isLast: Bool = status.id == statuses!.last?.id ?? ""
|
||||||
|
|
||||||
|
LazyVStack(alignment: .leading, spacing: 0.0) {
|
||||||
|
CompactPostView(status: status)
|
||||||
|
.onDisappear {
|
||||||
|
guard statuses != nil else { return }
|
||||||
|
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLast {
|
||||||
|
Divider()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
statuses = nil
|
||||||
|
|
||||||
|
Task {
|
||||||
|
loadingStatuses = true
|
||||||
|
statuses = await timelineModel.fetch(client: client)
|
||||||
|
loadingStatuses = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: lastSeen ?? 0) { _, new in
|
||||||
|
guard !loadingStatuses else { return }
|
||||||
|
Task {
|
||||||
|
loadingStatuses = true
|
||||||
|
statuses = await timelineModel.addStatuses(lastStatusIndex: new)
|
||||||
|
loadingStatuses = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button {
|
||||||
|
statuses = nil
|
||||||
|
|
||||||
|
Task {
|
||||||
|
loadingStatuses = true
|
||||||
|
statuses = await self.timelineModel.toggleContentFilter(filter: wordsFilter)
|
||||||
|
loadingStatuses = false
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: self.timelineModel.filtering ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
|
||||||
|
.symbolEffect(.pulse.wholeSymbol, isActive: self.timelineModel.filtering)
|
||||||
|
}
|
||||||
|
.tint(Color(uiColor: UIColor.label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .padding(.top)
|
||||||
|
.background(Color.appBackground)
|
||||||
|
.withAppRouter(navigator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyView: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.appBackground
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
picker
|
||||||
|
|
||||||
|
ContentUnavailableView {
|
||||||
|
Text("timeline.empty")
|
||||||
|
.bold()
|
||||||
|
} description: {
|
||||||
|
Text("timeline.empty.description")
|
||||||
|
}
|
||||||
|
.scrollDisabled(true)
|
||||||
|
}
|
||||||
|
.scrollDisabled(true)
|
||||||
|
.background(Color.appBackground)
|
||||||
|
.frame(height: 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingView: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.appBackground
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onAppear {
|
||||||
|
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
||||||
|
self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
if let client = accountManager.getClient() {
|
||||||
|
Task {
|
||||||
|
statuses = await timelineModel.fetch(client: client)
|
||||||
|
|
||||||
|
if UserDefaults.standard.bool(forKey: "autoOnFilter") {
|
||||||
|
statuses = await self.timelineModel.useContentFilter(wordsFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - View Component
|
||||||
private var picker: some View {
|
private var picker: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if showHero {
|
if showHero {
|
||||||
|
|
|
@ -262,11 +262,6 @@ struct ProfileView: View {
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Rectangle()
|
|
||||||
.fill(Color.gray.opacity(0.2))
|
|
||||||
.frame(width: .infinity, height: 1)
|
|
||||||
.padding(.bottom, 3)
|
|
||||||
|
|
||||||
statusesList
|
statusesList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,15 +302,21 @@ struct ProfileView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusesList: some View {
|
var statusesList: some View {
|
||||||
LazyVStack {
|
LazyVStack(spacing: 0) {
|
||||||
if loadingStatuses == false {
|
if loadingStatuses == false {
|
||||||
if !(statusesPinned?.isEmpty ?? true) {
|
if !(statusesPinned?.isEmpty ?? true) {
|
||||||
ForEach(statusesPinned!, id: \.id) { status in
|
ForEach(statusesPinned!, id: \.id) { status in
|
||||||
|
Divider()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
CompactPostView(status: status, pinned: true)
|
CompactPostView(status: status, pinned: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !(statuses?.isEmpty ?? true) {
|
if !(statuses?.isEmpty ?? true) {
|
||||||
ForEach(statuses!, id: \.id) { status in
|
ForEach(statuses!, id: \.id) { status in
|
||||||
|
Divider()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
CompactPostView(status: status)
|
CompactPostView(status: status)
|
||||||
.onDisappear() {
|
.onDisappear() {
|
||||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||||
|
|
Loading…
Reference in New Issue