Updated posts design

This commit is contained in:
lumaa-dev 2024-11-02 18:15:10 +01:00
parent 18c793d8c6
commit 4ad2c214f0
7 changed files with 276 additions and 358 deletions

View File

@ -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 */,

View File

@ -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 {

View File

@ -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)
} }
} }
} }

View File

@ -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() {

View File

@ -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

View File

@ -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 {

View File

@ -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 })