Updated posts design
This commit is contained in:
parent
18c793d8c6
commit
4ad2c214f0
|
@ -7,7 +7,6 @@
|
|||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
B90DEB1F2C822C2700D06121 /* 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 */
|
||||
|
||||
/* 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>"; };
|
||||
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>"; };
|
||||
|
@ -587,7 +585,6 @@
|
|||
children = (
|
||||
B9C20D592B923D53004DC9B3 /* Bubble.entitlements */,
|
||||
B9FB94A02B2EF23100D81C07 /* Info.plist */,
|
||||
B9029FC12B81259400AA9B68 /* Secret.plist */,
|
||||
B9FB945A2B2DEECE00D81C07 /* BubbleApp.swift */,
|
||||
B9EBE8572B474FD600FB594D /* AppDelegate.swift */,
|
||||
B9FB946E2B2DF3BB00D81C07 /* Components */,
|
||||
|
@ -873,7 +870,6 @@
|
|||
B9DC69302B79378400E625B9 /* BubblePlus.storekit in Resources */,
|
||||
B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */,
|
||||
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */,
|
||||
B9029FC22B81259400AA9B68 /* Secret.plist in Resources */,
|
||||
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */,
|
||||
B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */,
|
||||
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */,
|
||||
|
|
|
@ -7,7 +7,9 @@ import RevenueCat
|
|||
@main
|
||||
struct BubbleApp: App {
|
||||
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 DEBUG
|
||||
|
@ -15,8 +17,6 @@ struct BubbleApp: App {
|
|||
#endif
|
||||
Purchases.configure(withAPIKey: apiKey, appUserID: deviceId)
|
||||
}
|
||||
|
||||
BubbleShortcuts.updateAppShortcutParameters() //might not work?
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
|
|
@ -23,20 +23,14 @@ struct CompactPostView: View {
|
|||
@State private var quoteStatus: Status? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 4.0) {
|
||||
notices
|
||||
|
||||
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)
|
||||
.containerShape(Rectangle())
|
||||
.padding(.vertical, 6.0)
|
||||
.background(postBackground())
|
||||
.contextMenu {
|
||||
PostMenu(status: status)
|
||||
|
@ -300,7 +294,13 @@ struct CompactPostView: View {
|
|||
)
|
||||
.opacity(0.2)
|
||||
} 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 {
|
||||
ScrollView(.vertical) {
|
||||
ScrollViewReader { proxy in
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 1.5) {
|
||||
if statuses.isEmpty {
|
||||
statusPost(detailedStatus)
|
||||
CompactPostView(status: detailedStatus)
|
||||
} else {
|
||||
ForEach(statuses) { status in
|
||||
let isLast: Bool = status.id == statuses.last?.id ?? ""
|
||||
|
||||
if status.id == detailedStatus.id {
|
||||
statusPost(detailedStatus)
|
||||
.padding(.horizontal, 15)
|
||||
.padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical)
|
||||
CompactPostView(status: detailedStatus)
|
||||
.onAppear {
|
||||
proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
CompactPostView(status: status)
|
||||
repPost(status)
|
||||
}
|
||||
|
||||
if !isLast {
|
||||
Divider()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,82 +60,18 @@ struct PostDetailsView: View {
|
|||
.safeAreaPadding()
|
||||
.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 {
|
||||
PostMenu(status: status)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundStyle(Color.white.opacity(0.3))
|
||||
.font(.body)
|
||||
.contentShape(Rectangle())
|
||||
.padding(7.5)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
@ViewBuilder
|
||||
func repPost(_ status: Status) -> some View {
|
||||
HStack(alignment: .center, spacing: 8.0) {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.4))
|
||||
.frame(maxWidth: 2.0, maxHeight: .infinity, alignment: .leading)
|
||||
|
||||
CompactPostView(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func fetchStatusDetail() async {
|
||||
guard let client = accountManager.getClient() else { return }
|
||||
do {
|
||||
|
@ -160,74 +101,7 @@ struct PostDetailsView: View {
|
|||
let status: Status
|
||||
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? {
|
||||
let content = detailedStatus.content
|
||||
if let client = accountManager.getClient() {
|
||||
|
|
|
@ -4,33 +4,33 @@ import SwiftUI
|
|||
|
||||
struct PostsView: View {
|
||||
@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 timelines: [TimelineFilter] = [.home, .trending, .local, .federated]
|
||||
|
||||
|
||||
@State private var loadingStatuses: Bool = false
|
||||
@State private var statuses: [Status]?
|
||||
@State private var lastSeen: Int?
|
||||
|
||||
|
||||
@State var filter: TimelineFilter
|
||||
@State var showHero: Bool = false
|
||||
@State var timelineModel: FetchTimeline // home timeline by default
|
||||
|
||||
|
||||
init(timelineModel: FetchTimeline, filter: TimelineFilter, showHero: Bool = false) {
|
||||
self.timelineModel = timelineModel
|
||||
self.filter = filter
|
||||
self.timelineModel.setTimelineFilter(filter)
|
||||
self.showHero = showHero
|
||||
}
|
||||
|
||||
|
||||
init(filter: TimelineFilter, showHero: Bool = false) {
|
||||
self.timelineModel = .init(client: AccountManager.shared.forceClient())
|
||||
self.filter = filter
|
||||
self.timelineModel.setTimelineFilter(filter)
|
||||
self.showHero = showHero
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if statuses != nil {
|
||||
|
@ -49,54 +49,12 @@ struct PostsView: View {
|
|||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if showPicker {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
picker
|
||||
}
|
||||
|
||||
posts
|
||||
}
|
||||
.refreshable {
|
||||
if let client = accountManager.getClient() {
|
||||
|
@ -118,46 +76,10 @@ struct PostsView: View {
|
|||
// .padding(.top)
|
||||
.background(Color.appBackground)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
emptyView
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
timelineModel.setTimelineFilter(filter)
|
||||
if let client = accountManager.getClient() {
|
||||
Task {
|
||||
statuses = await timelineModel.fetch(client: client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
loadingView
|
||||
}
|
||||
}
|
||||
.navigationTitle(filter.localizedTitle())
|
||||
|
@ -165,7 +87,112 @@ struct PostsView: View {
|
|||
.toolbarBackground(Color.appBackground, 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 {
|
||||
guard let client = accountManager.getClient() else { return }
|
||||
statuses = nil
|
||||
|
|
|
@ -37,113 +37,133 @@ struct TimelineView: View {
|
|||
NavigationStack(path: $navigator.path) {
|
||||
if statuses != nil {
|
||||
if !statuses!.isEmpty {
|
||||
ScrollView(showsIndicators: false) {
|
||||
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)
|
||||
statusesView
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
emptyView
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
loadingView
|
||||
}
|
||||
}
|
||||
.environmentObject(navigator)
|
||||
.background(Color.appBackground)
|
||||
.toolbarBackground(Color.appBackground, 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 {
|
||||
VStack {
|
||||
if showHero {
|
||||
|
|
|
@ -262,11 +262,6 @@ struct ProfileView: View {
|
|||
.padding(.horizontal)
|
||||
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: .infinity, height: 1)
|
||||
.padding(.bottom, 3)
|
||||
|
||||
statusesList
|
||||
}
|
||||
}
|
||||
|
@ -307,15 +302,21 @@ struct ProfileView: View {
|
|||
}
|
||||
|
||||
var statusesList: some View {
|
||||
LazyVStack {
|
||||
LazyVStack(spacing: 0) {
|
||||
if loadingStatuses == false {
|
||||
if !(statusesPinned?.isEmpty ?? true) {
|
||||
ForEach(statusesPinned!, id: \.id) { status in
|
||||
Divider()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
CompactPostView(status: status, pinned: true)
|
||||
}
|
||||
}
|
||||
if !(statuses?.isEmpty ?? true) {
|
||||
ForEach(statuses!, id: \.id) { status in
|
||||
Divider()
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
CompactPostView(status: status)
|
||||
.onDisappear() {
|
||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||
|
|
Loading…
Reference in New Issue