From 3e2342588f276599115ec510e241f6b2b13aca2f Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 5 Jan 2024 12:27:21 -0500 Subject: [PATCH 01/69] rebase conflict --- .../xcshareddata/swiftpm/Package.resolved | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index d9b5f5c17..000000000 --- a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,86 +0,0 @@ -{ - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", - "version" : "0.11.0" - } - }, - { - "identity" : "keychainaccess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state" : { - "branch" : "master", - "revision" : "ecb18d8ce4d88277cc4fb103973352d91e18c535" - } - }, - { - "identity" : "nuke", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke", - "state" : { - "revision" : "989586f86b683680f7bd5765d6a5683edbea0c1b", - "version" : "12.1.4" - } - }, - { - "identity" : "semaphore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/Semaphore", - "state" : { - "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", - "version" : "0.0.8" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", - "version" : "0.4.0" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", - "version" : "0.1.1" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", - "version" : "0.6.0" - } - }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", - "state" : { - "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", - "version" : "2.1.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", - "version" : "0.9.0" - } - } - ], - "version" : 2 -} From 235001b80c985336d2a1d9d2657559cad4c95f8a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 5 Jan 2024 12:24:57 -0500 Subject: [PATCH 02/69] commit before rebase --- Mlem.xcodeproj/project.pbxproj | 18 ++++++++++++++++++ .../Tracker Items/PostModel+TrackerItem.swift | 17 +++++++++++++++++ .../Trackers/Feeds/NEW PostTracker.swift | 10 ++++++++++ .../Trackers/Generics/ChildTracker.swift | 4 ++-- .../Models/Trackers/Generics/CoreTracker.swift | 2 +- .../Generics/README - Generic Trackers.md | 15 +++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 Mlem/Extensions/Tracker Items/PostModel+TrackerItem.swift create mode 100644 Mlem/Models/Trackers/Feeds/NEW PostTracker.swift create mode 100644 Mlem/Models/Trackers/Generics/README - Generic Trackers.md diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 8b2aaff2a..38b641f00 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -327,6 +327,8 @@ CD05E7792A4E381A0081D102 /* PostSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD05E7782A4E381A0081D102 /* PostSize.swift */; }; CD05E77F2A4F263B0081D102 /* Menu Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD05E77E2A4F263B0081D102 /* Menu Function.swift */; }; CD0BE42F2A65A73600314B24 /* Haptic Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0BE42E2A65A73600314B24 /* Haptic Manager.swift */; }; + CD12627A2B4759BC007549F9 /* NEW PostTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1262792B4759BC007549F9 /* NEW PostTracker.swift */; }; + CD12627D2B475E45007549F9 /* PostModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD12627C2B475E45007549F9 /* PostModel+TrackerItem.swift */; }; CD1446182A58FC3B00610EF1 /* InfoStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446172A58FC3B00610EF1 /* InfoStackView.swift */; }; CD14461B2A5A4B6D00610EF1 /* PostSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD14461A2A5A4B6D00610EF1 /* PostSettingsView.swift */; }; CD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446202A5B328E00610EF1 /* Privacy Policy.swift */; }; @@ -865,6 +867,9 @@ CD05E7782A4E381A0081D102 /* PostSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSize.swift; sourceTree = ""; }; CD05E77E2A4F263B0081D102 /* Menu Function.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Function.swift"; sourceTree = ""; }; CD0BE42E2A65A73600314B24 /* Haptic Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Haptic Manager.swift"; sourceTree = ""; }; + CD1262792B4759BC007549F9 /* NEW PostTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostTracker.swift"; sourceTree = ""; }; + CD12627B2B475A80007549F9 /* README - Generic Trackers.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "README - Generic Trackers.md"; sourceTree = ""; }; + CD12627C2B475E45007549F9 /* PostModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostModel+TrackerItem.swift"; sourceTree = ""; }; CD1446172A58FC3B00610EF1 /* InfoStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoStackView.swift; sourceTree = ""; }; CD14461A2A5A4B6D00610EF1 /* PostSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsView.swift; sourceTree = ""; }; CD1446202A5B328E00610EF1 /* Privacy Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Privacy Policy.swift"; sourceTree = ""; }; @@ -1485,6 +1490,7 @@ 50F830EC2A4C8F8D00D67099 /* Generics */ = { isa = PBXGroup; children = ( + CD12627B2B475A80007549F9 /* README - Generic Trackers.md */, CDB45C5B2AF1A1D800A1FF08 /* CoreTracker.swift */, CD4368AD2AE23ED400BD8BD1 /* StandardTracker.swift */, CD4368AF2AE23F1400BD8BD1 /* ChildTracker.swift */, @@ -1991,6 +1997,7 @@ isa = PBXGroup; children = ( 50F830EC2A4C8F8D00D67099 /* Generics */, + CD1262782B47597E007549F9 /* Feeds */, CDF8425F2A49EA2A00723DA0 /* Inbox */, 6386E02E2A03ED39006B3C1D /* Comment Tracker.swift */, 63344C4E2A07BD2A001BC616 /* Filters Tracker.swift */, @@ -2160,6 +2167,14 @@ path = APIClient; sourceTree = ""; }; + CD1262782B47597E007549F9 /* Feeds */ = { + isa = PBXGroup; + children = ( + CD1262792B4759BC007549F9 /* NEW PostTracker.swift */, + ); + path = Feeds; + sourceTree = ""; + }; CD14461F2A5B328600610EF1 /* Data */ = { isa = PBXGroup; children = ( @@ -2445,6 +2460,7 @@ CDB45C5F2AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift */, CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */, CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */, + CD12627C2B475E45007549F9 /* PostModel+TrackerItem.swift */, ); path = "Tracker Items"; sourceTree = ""; @@ -3058,6 +3074,7 @@ 507573942A5AD59E00AA7ABD /* EquatableError.swift in Sources */, CDE6A81A2A490B970062D161 /* Inbox ReplyBodyView.swift in Sources */, 50811B3C2A92059C006BA3F2 /* BlockCommunityResponse+Mock.swift in Sources */, + CD12627A2B4759BC007549F9 /* NEW PostTracker.swift in Sources */, CD391F9E2A539F1800E213B5 /* ReplyToMention.swift in Sources */, CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */, 500C168E2A66FAAB006F243B /* HapticManager+Dependency.swift in Sources */, @@ -3351,6 +3368,7 @@ 035EB0CA2A8687C200227859 /* JumpButtonView.swift in Sources */, 5016A2B12A67EB8600B257E8 /* UIViewController+TopMostViewController.swift in Sources */, 6372184C2A3A2AAD008C4816 /* APIPostView.swift in Sources */, + CD12627D2B475E45007549F9 /* PostModel+TrackerItem.swift in Sources */, CDB0117D2A6F703800D043EB /* CommentEditor.swift in Sources */, 0308E1162B0EA42B000CA955 /* APILocalUserView.swift in Sources */, 030E863F2AC6C5E9000283A6 /* PictrsImageModel.swift in Sources */, diff --git a/Mlem/Extensions/Tracker Items/PostModel+TrackerItem.swift b/Mlem/Extensions/Tracker Items/PostModel+TrackerItem.swift new file mode 100644 index 000000000..c0e60c70b --- /dev/null +++ b/Mlem/Extensions/Tracker Items/PostModel+TrackerItem.swift @@ -0,0 +1,17 @@ +// +// PostModel+TrackerItem.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-04. +// + +import Foundation + +extension PostModel: TrackerItem { + func sortVal(sortType: TrackerSortType) -> TrackerSortVal { + switch sortType { + case .published: + return .published(published) + } + } +} diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift new file mode 100644 index 000000000..098fee688 --- /dev/null +++ b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift @@ -0,0 +1,10 @@ +// +// NEW PostTracker.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-04. +// + +import Foundation + +class NewPostTracker: StandardTracker {} diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index 2c722a03b..72e5854da 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -19,8 +19,8 @@ class ChildTracker: StandardTracker< } /// Gets the next item in the feed stream and increments the cursor - /// **WARNING** this is NOT a thread-safe function! Only one thread at a time may call this function! /// - Returns: next item in the feed stream + /// - Warning: This is NOT a thread-safe function! Only one thread at a time may call this function! func consumeNextItem() -> ParentItem? { assert(cursor < items.count, "consumeNextItem called on a tracker without a next item (cursor: \(cursor), count: \(items.count))!") @@ -33,9 +33,9 @@ class ChildTracker: StandardTracker< } /// Gets the sort value of the next item in feed stream for a given sort type without affecting the cursor. The sort type must match the sort type of this tracker. - /// **WARNING** this is NOT a thread-safe function! Only one thread at a time may call this function! /// - Parameter sortType: type of sorting being performed /// - Returns: sorting value of the next tracker item corresponding to the given sort type + /// - Warning: This is NOT a thread-safe function! Only one thread at a time may call this function! func nextItemSortVal(sortType: TrackerSortType) async throws -> TrackerSortVal? { assert(sortType == self.sortType, "Conflicting types for sortType! This will lead to unexpected sorting behavior.") diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index f06f4b8c2..b6a0b6d52 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -7,7 +7,7 @@ import Foundation -/// Class providing common tracker functionality for BasicTracker and ParentTracker +/// Class providing common tracker functionality for StandardTracker and ParentTracker class CoreTracker: ObservableObject { @Published var items: [Item] = .init() @Published private(set) var loadingState: LoadingState = .idle diff --git a/Mlem/Models/Trackers/Generics/README - Generic Trackers.md b/Mlem/Models/Trackers/Generics/README - Generic Trackers.md new file mode 100644 index 000000000..b60e61ee8 --- /dev/null +++ b/Mlem/Models/Trackers/Generics/README - Generic Trackers.md @@ -0,0 +1,15 @@ +# Generic Trackers + +This group contains a set of generic classes intended to back feed views. This document is intended as a high-level overview of the design principles and a quickstart guide for using the trackers; for detailed information, refer to the inline documentation. + +## Tracker Operation + +The heart of a tracker is very simple: an array of items and a method for loading more. + +## Tracker Types + +There are three types of trackers: `StandardTracker`, `ChildTracker`, and `ParentTracker`. `CoreTracker` holds shared logic between these trackers, and should **not** be used! + +`StandardTracker` should be used for feeds with a single item type (e.g., the main posts feed). + +`ChildTracker` and `ParentTracker` should always be used in conjunction! They handle feeds with mixed item types (e.g., the inbox feed). `ChildTracker` is a modified version of `StandardTracker`, and can safely be used to drive its own feed in addition to the mixed feed (as is done in the inbox). `ParentTracker` offers a similar interface, but functions radically differently: it relies on its `ChildTracker`s to load items! From ee4f84c246a074a616205f106f803f8f241d416c Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 5 Jan 2024 18:11:38 -0500 Subject: [PATCH 03/69] updated StandardTracker to use new LoadAction enum to govern loading, setting up cursor support --- Mlem.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 86 +++++++++++ .../Trackers/Feeds/NEW PostTracker.swift | 23 ++- .../Trackers/Generics/ChildTracker.swift | 29 ++-- .../Trackers/Generics/CoreTracker.swift | 4 +- .../Trackers/Generics/ParentTracker.swift | 2 +- .../Trackers/Generics/StandardTracker.swift | 133 +++++++++++------- Mlem/Views/Tabs/Inbox/Inbox View.swift | 1 + 8 files changed, 211 insertions(+), 69 deletions(-) create mode 100644 Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 38b641f00..c86baabb8 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -2980,7 +2980,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint lint --strict\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint lint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..d9b5f5c17 --- /dev/null +++ b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,86 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", + "version" : "0.11.0" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "branch" : "master", + "revision" : "ecb18d8ce4d88277cc4fb103973352d91e18c535" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke", + "state" : { + "revision" : "989586f86b683680f7bd5765d6a5683edbea0c1b", + "version" : "12.1.4" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", + "version" : "0.1.1" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", + "state" : { + "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", + "version" : "2.1.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", + "version" : "0.9.0" + } + } + ], + "version" : 2 +} diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift index 098fee688..da4780d06 100644 --- a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift @@ -5,6 +5,27 @@ // Created by Eric Andrews on 2024-01-04. // +import Dependencies import Foundation -class NewPostTracker: StandardTracker {} +enum NewFeedType { + case all +} + +class NewPostTracker: StandardTracker { + @Dependency(\.postRepository) var postRepository + + var unreadOnly: Bool + var feedType: NewFeedType + + // var cursor: + + init(internetSpeed: InternetSpeed, sortType: TrackerSortType, unreadOnly: Bool, feedType: NewFeedType) { + self.unreadOnly = unreadOnly + self.feedType = feedType + + super.init(internetSpeed: internetSpeed, sortType: sortType) + } + + // override func fetchPage( +} diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index 72e5854da..a1f66739d 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -8,7 +8,7 @@ import Foundation class ChildTracker: StandardTracker, ChildTrackerProtocol { private weak var parentTracker: (any ParentTrackerProtocol)? - private var cursor: Int = 0 + private var streamCursor: Int = 0 func toParent(item: Item) -> ParentItem { preconditionFailure("This method must be implemented by the inheriting class") @@ -22,11 +22,14 @@ class ChildTracker: StandardTracker< /// - Returns: next item in the feed stream /// - Warning: This is NOT a thread-safe function! Only one thread at a time may call this function! func consumeNextItem() -> ParentItem? { - assert(cursor < items.count, "consumeNextItem called on a tracker without a next item (cursor: \(cursor), count: \(items.count))!") + assert( + streamCursor < items.count, + "consumeNextItem called on a tracker without a next item (cursor: \(streamCursor), count: \(items.count))!" + ) - if cursor < items.count { - cursor += 1 - return toParent(item: items[cursor - 1]) + if streamCursor < items.count { + streamCursor += 1 + return toParent(item: items[streamCursor - 1]) } return nil @@ -39,8 +42,8 @@ class ChildTracker: StandardTracker< func nextItemSortVal(sortType: TrackerSortType) async throws -> TrackerSortVal? { assert(sortType == self.sortType, "Conflicting types for sortType! This will lead to unexpected sorting behavior.") - if cursor < items.count { - return items[cursor].sortVal(sortType: sortType) + if streamCursor < items.count { + return items[streamCursor].sortVal(sortType: sortType) } else { // if done loading, return nil if loadingState == .done { @@ -49,19 +52,19 @@ class ChildTracker: StandardTracker< // otherwise, wait for the next page to load and try to return the first value // if the next page is already loading, this call to loadNextPage will be noop, but still wait until that load completes thanks to the semaphore - await loadNextPage() - return cursor < items.count ? items[cursor].sortVal(sortType: sortType) : nil + await loadMoreItems() + return streamCursor < items.count ? items[streamCursor].sortVal(sortType: sortType) : nil } } /// Resets the cursor to 0 but does not unload any items func resetCursor() { - cursor = 0 + streamCursor = 0 } func refresh(clearBeforeRefresh: Bool, notifyParent: Bool = true) async throws { try await refresh(clearBeforeRefresh: clearBeforeRefresh) - cursor = 0 + streamCursor = 0 if notifyParent, let parentTracker { await parentTracker.refresh(clearBeforeFetch: clearBeforeRefresh) @@ -70,7 +73,7 @@ class ChildTracker: StandardTracker< func reset(notifyParent: Bool = true) async { await reset() - cursor = 0 + streamCursor = 0 if notifyParent, let parentTracker { await parentTracker.reset() } @@ -80,7 +83,7 @@ class ChildTracker: StandardTracker< let newItems = items.filter(filter) let removed = items.count - newItems.count - cursor = 0 + streamCursor = 0 await setItems(newItems) return removed diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index b6a0b6d52..23530efb2 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -30,12 +30,12 @@ class CoreTracker: ObservableObject { if loadingState == .idle, item.uid == threshold || item.uid == fallbackThreshold { // this is a synchronous function that wraps the loading as a task so that the task is attached to the tracker itself, not the view that calls it, and is therefore safe from being cancelled by view redraws Task(priority: .userInitiated) { - await loadNextPage() + await loadMoreItems() } } } - func loadNextPage() async { + func loadMoreItems() async { preconditionFailure("This method must be overridden by the inheriting class") } diff --git a/Mlem/Models/Trackers/Generics/ParentTracker.swift b/Mlem/Models/Trackers/Generics/ParentTracker.swift index abaa5b5e9..52146f59c 100644 --- a/Mlem/Models/Trackers/Generics/ParentTracker.swift +++ b/Mlem/Models/Trackers/Generics/ParentTracker.swift @@ -36,7 +36,7 @@ class ParentTracker: CoreTracker, ParentTrackerProtocol // MARK: loading methods /// Loads the next page of items - override func loadNextPage() async { + override func loadMoreItems() async { guard loadingState != .done else { return } diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 7f0024345..3b566a1f4 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -9,12 +9,30 @@ import Dependencies import Foundation import Semaphore +/// Enumeration of loading actions +enum LoadAction { + /// Clears the tracker + case clear + + /// Resets the tracker. If true, clears before resetting. + case refresh(Bool) + + /// Load the requested page + case loadPage(Int) + + /// Load the requested cursor + case loadCursor(String) +} + class StandardTracker: CoreTracker { @Dependency(\.errorHandler) var errorHandler - // loading state + /// loading state private var ids: Set = .init(minimumCapacity: 1000) - private(set) var page: Int = 0 // number of the most recently loaded page--0 indicates no content + /// number of the most recently loaded page. 0 indicates no content. + private(set) var page: Int = 0 + /// cursor of the most recently loaded page. nil indicates no content. + private(set) var loadingCursor: String? private let loadingSemaphore: AsyncSemaphore = .init(value: 1) // MARK: - Main actor methods @@ -30,21 +48,30 @@ class StandardTracker: CoreTracker { // MARK: - External methods - override func loadNextPage() async { + override func loadMoreItems() async { do { - try await loadPage(page + 1) + let pageToLoad = page + 1 + + if pageToLoad == 1 { + try await load(action: .refresh(false)) + } else { + try await load(action: .loadPage(pageToLoad)) + } + + // try await load(page + 1) } catch { errorHandler.handle(error) } } func refresh(clearBeforeRefresh: Bool) async throws { - try await loadPage(1, clearBeforeRefresh: clearBeforeRefresh) + try await load(action: .refresh(clearBeforeRefresh)) } func reset() async { do { - try await loadPage(0) + // try await load(0) + try await load(action: .clear) } catch { assertionFailure("Exception thrown when resetting, this should not be possible!") await clear() // this is not a thread-safe use of clear, but I'm using it here because we should never get here @@ -53,40 +80,74 @@ class StandardTracker: CoreTracker { // MARK: - Internal tracking methods - /// Loads the requested page. To account for the fact that multiple threads might request a load at the same time, this function requires that the caller pass in what it thinks is the next page to load. If that is not the next page by the time that call is allowed to execute, its request will be ignored. + /// Performs the requested loading operation. To account for the fact that multiple threads might request a load at the same time, this function requires that the caller pass in what it thinks is the next page or cursor to load. If that is not the next page/cursor by the time that call is allowed to execute, its request will be ignored. /// This grants this function an additional, extremely useful property: calling `await loadPage` while `loadPage` is already being executed will, practically speaking, await the in-flight request. /// There is additional logic to handle the reset case--because page is updated at the end of this call, if reset() set the page to 0 itself and a reset call were made while another loading call was in-flight, the in-flight call would update page before the reset call went through and the reset call's load would be aborted. Instead, this method takes on responsibility for resetting--calling it on page 0 clears the tracker, and page 1 refreshes it /// - Parameter page: page number to load - func loadPage(_ pageToLoad: Int, clearBeforeRefresh: Bool = false) async throws { - assert(!clearBeforeRefresh || pageToLoad == 1, "clearBeforeRefresh cannot be true if not loading page 1") - + func load(action: LoadAction) async throws { // only one thread may execute this function at a time await loadingSemaphore.wait() defer { loadingSemaphore.signal() } - - // special reset cases - if pageToLoad == 0 { + + switch action { + case .clear: print("[\(Item.self) tracker] clearing") await clear() return - } - - if pageToLoad == 1 { + case let .refresh(clearBeforeRefresh): print("[\(Item.self) tracker] refreshing") if clearBeforeRefresh { await clear() } else { - // if not clearing before reset, still clear these fields in order to sanitize the loading state--we just keep the items in place until we have received new ones, which will be set below + // if not clearing before reset, still clear these fields in order to sanitize the loading state--we just keep the items in place until we have received new ones, which will be set by loadPage/loadCursor page = 0 + loadingCursor = nil ids = .init(minimumCapacity: 1000) await setLoading(.idle) } - } - - if pageToLoad > 1 { + try await loadPageHelper(1) + case let .loadPage(pageToLoad): print("[\(Item.self) tracker] loading page \(pageToLoad)") + try await loadPageHelper(pageToLoad) + case .loadCursor: + print("[\(Item.self) tracker] loading cursor") + assertionFailure("Not implemented!") } + } + + // MARK: - Helpers + + /// Fetches the next page of items. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. + /// - Parameters: + /// - page: page number to fetch + /// - Returns: requested page of items + func fetchPage(page: Int) async throws -> [Item] { + preconditionFailure("This method must be implemented by the inheriting class") + } + + /// Filters out items according to the given filtering function. + /// - Parameter filter: function that, given an Item, returns true if the item should REMAIN in the tracker + @discardableResult func filter(with filter: @escaping (Item) -> Bool) async -> Int { + let newItems = items.filter(filter) + let removed = items.count - newItems.count + + await setItems(newItems) + return removed + } + + /// Given an array of Items, adds their ids to ids. Returns the input filtered to only items not previously present in ids. + /// - Parameter newMessages: array of MessageModel + /// - Returns: `newMessages`, filtered to only messages not already present in ids + private func storeIdsAndDedupe(newItems: [Item]) -> [Item] { + let accepted = newItems.filter { ids.insert($0.uid).inserted } + return accepted + } + + /// Loads a given page of items + /// - Parameter pageToLoad: page to load + /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! + private func loadPageHelper(_ pageToLoad: Int) async throws { // do not continue to load if done. this check has to come after the clear/refresh cases because those cases can be called on a .done tracker guard loadingState != .done else { print("[\(Item.self) tracker] done loading, will not continue") @@ -127,38 +188,8 @@ class StandardTracker: CoreTracker { } } - // MARK: - Helpers - - /// Fetches the next page of items. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. - /// - Parameters: - /// - page: page number to fetch - /// - Returns: requested page of items - func fetchPage(page: Int) async throws -> [Item] { - preconditionFailure("This method must be implemented by the inheriting class") - } - - /// Filters out items according to the given filtering function. - /// - Parameter filter: function that, given an Item, returns true if the item should REMAIN in the tracker - @discardableResult func filter(with filter: @escaping (Item) -> Bool) async -> Int { - let newItems = items.filter(filter) - let removed = items.count - newItems.count - - await setItems(newItems) - - return removed - } - - /// Given an array of Items, adds their ids to ids. Returns the input filtered to only items not previously present in ids. - /// - Parameter newMessages: array of MessageModel - /// - Returns: newMessages, filtered to only messages not already present in ids - private func storeIdsAndDedupe(newItems: [Item]) -> [Item] { - let accepted = newItems.filter { ids.insert($0.uid).inserted } - return accepted - } - /// Clears the tracker to an empty state. - /// **WARNING:** - /// **DO NOT** call this method from anywhere but loadPage! This is *purely* a helper function for loadPage and *will* lead to unexpected behavior if called elsewhere! + /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! private func clear() async { ids = .init(minimumCapacity: 1000) page = 0 diff --git a/Mlem/Views/Tabs/Inbox/Inbox View.swift b/Mlem/Views/Tabs/Inbox/Inbox View.swift index b7ee44a75..ee311aaf0 100644 --- a/Mlem/Views/Tabs/Inbox/Inbox View.swift +++ b/Mlem/Views/Tabs/Inbox/Inbox View.swift @@ -121,6 +121,7 @@ struct InboxView: View { .environmentObject(inboxTabNavigation) .environmentObject(inboxTracker) .onChange(of: shouldFilterRead) { newValue in + print("filtering read: \(newValue)") Task(priority: .userInitiated) { await handleShouldFilterReadChange(newShouldFilterRead: newValue) } From 79e54d1a9df80ddee3025546dd005d6d92715926 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 5 Jan 2024 22:15:24 -0500 Subject: [PATCH 04/69] fixed a bug where toggling to unread only in the inbox wasn't working properly --- .../Trackers/Generics/ChildTracker.swift | 5 ++ .../Generics/ChildTrackerProtocol.swift | 8 ++- .../Trackers/Generics/ParentTracker.swift | 21 ++++++-- .../Trackers/Generics/StandardTracker.swift | 49 ++++++++++--------- Mlem/Views/Tabs/Inbox/Inbox View.swift | 11 ++--- 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index a1f66739d..f99c4d41d 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -9,6 +9,8 @@ import Foundation class ChildTracker: StandardTracker, ChildTrackerProtocol { private weak var parentTracker: (any ParentTrackerProtocol)? private var streamCursor: Int = 0 + + var allItems: [ParentItem] { items.map { toParent(item: $0) }} func toParent(item: Item) -> ParentItem { preconditionFailure("This method must be implemented by the inheriting class") @@ -80,11 +82,14 @@ class ChildTracker: StandardTracker< } @discardableResult override func filter(with filter: @escaping (Item) -> Bool) async -> Int { + print("[\(Item.self) tracker] filtering \(items.count) items") let newItems = items.filter(filter) let removed = items.count - newItems.count + print("[\(Item.self) tracker] filtered \(removed) items") streamCursor = 0 await setItems(newItems) + print("[\(Item.self) tracker] now contains \(items.count) items") return removed } diff --git a/Mlem/Models/Trackers/Generics/ChildTrackerProtocol.swift b/Mlem/Models/Trackers/Generics/ChildTrackerProtocol.swift index 82d0140a4..cb97ea78c 100644 --- a/Mlem/Models/Trackers/Generics/ChildTrackerProtocol.swift +++ b/Mlem/Models/Trackers/Generics/ChildTrackerProtocol.swift @@ -9,8 +9,12 @@ import Foundation protocol ChildTrackerProtocol: AnyObject { associatedtype Item: TrackerItem associatedtype ParentItem: TrackerItem + + /// All items present in the tracker + /// - Warning: this should not be directly accessed by the parent except to perform filtering! + var allItems: [ParentItem] { get } - // stream support methods + // MARK: stream support methods func setParentTracker(_ newParent: any ParentTrackerProtocol) @@ -20,7 +24,7 @@ protocol ChildTrackerProtocol: AnyObject { func resetCursor() - // loading methods + // MARK: loading methods func reset(notifyParent: Bool) async diff --git a/Mlem/Models/Trackers/Generics/ParentTracker.swift b/Mlem/Models/Trackers/Generics/ParentTracker.swift index 52146f59c..7efe2e8cb 100644 --- a/Mlem/Models/Trackers/Generics/ParentTracker.swift +++ b/Mlem/Models/Trackers/Generics/ParentTracker.swift @@ -72,13 +72,24 @@ class ParentTracker: CoreTracker, ParentTrackerProtocol /// Filters out items according to the given filtering function. /// - Parameter filter: function that, given an Item, returns true if the item should REMAIN in the tracker func filter(with filter: @escaping (Item) -> Bool) async { - // build set of uids to remove + // build set of uids to remove. need to iterate through every item in every tracker because trackers may have items that should be filtered but are not present in the parent yet var uidsToFilter: Set = .init() - items.forEach { item in - if !filter(item) { - uidsToFilter.insert(item.uid) + childTrackers.forEach { child in + child.allItems.forEach { item in + guard let item = item as? Item else { + assertionFailure("Could not convert to parent type!") + return + } + if !filter(item) { + uidsToFilter.insert(item.uid) + } } } +// items.forEach { item in +// if !filter(item) { +// uidsToFilter.insert(item.uid) +// } +// } // function to remove items from child trackers based on uid--this makes the Item-specific filtering applied here generically applicable to any child tracker let filterFunc = { (item: any TrackerItem) in @@ -100,6 +111,8 @@ class ParentTracker: CoreTracker, ParentTrackerProtocol return removed } + print("[\(Item.self) tracker] removed \(removed) items, fetching more") + // reload all non-removed items let remaining = items.count - removed let newItems = await fetchNextItems(numItems: max(remaining, abs(AppConstants.infiniteLoadThresholdOffset) + 1)) diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 3b566a1f4..0d175fe05 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -74,7 +74,7 @@ class StandardTracker: CoreTracker { try await load(action: .clear) } catch { assertionFailure("Exception thrown when resetting, this should not be possible!") - await clear() // this is not a thread-safe use of clear, but I'm using it here because we should never get here + await clearHelper() // this is not a thread-safe use of clear, but I'm using it here because we should never get here } } @@ -92,20 +92,10 @@ class StandardTracker: CoreTracker { switch action { case .clear: print("[\(Item.self) tracker] clearing") - await clear() - return + await clearHelper() case let .refresh(clearBeforeRefresh): print("[\(Item.self) tracker] refreshing") - if clearBeforeRefresh { - await clear() - } else { - // if not clearing before reset, still clear these fields in order to sanitize the loading state--we just keep the items in place until we have received new ones, which will be set by loadPage/loadCursor - page = 0 - loadingCursor = nil - ids = .init(minimumCapacity: 1000) - await setLoading(.idle) - } - try await loadPageHelper(1) + try await refreshHelper(clearBeforeRefresh: clearBeforeRefresh) case let .loadPage(pageToLoad): print("[\(Item.self) tracker] loading page \(pageToLoad)") try await loadPageHelper(pageToLoad) @@ -143,6 +133,30 @@ class StandardTracker: CoreTracker { let accepted = newItems.filter { ids.insert($0.uid).inserted } return accepted } + + /// Clears the tracker to an empty state. + /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! + private func clearHelper() async { + ids = .init(minimumCapacity: 1000) + page = 0 + await setLoading(.idle) + await setItems(.init()) + } + + /// Clears + /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! + private func refreshHelper(clearBeforeRefresh: Bool) async throws { + if clearBeforeRefresh { + await clearHelper() + } else { + // if not clearing before reset, still clear these fields in order to sanitize the loading state--we just keep the items in place until we have received new ones, which will be set by loadPage/loadCursor + page = 0 + loadingCursor = nil + ids = .init(minimumCapacity: 1000) + await setLoading(.idle) + } + try await loadPageHelper(1) + } /// Loads a given page of items /// - Parameter pageToLoad: page to load @@ -187,13 +201,4 @@ class StandardTracker: CoreTracker { await setLoading(.idle) } } - - /// Clears the tracker to an empty state. - /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! - private func clear() async { - ids = .init(minimumCapacity: 1000) - page = 0 - await setLoading(.idle) - await setItems(.init()) - } } diff --git a/Mlem/Views/Tabs/Inbox/Inbox View.swift b/Mlem/Views/Tabs/Inbox/Inbox View.swift index ee311aaf0..b18496f74 100644 --- a/Mlem/Views/Tabs/Inbox/Inbox View.swift +++ b/Mlem/Views/Tabs/Inbox/Inbox View.swift @@ -120,18 +120,17 @@ struct InboxView: View { .handleLemmyViews() .environmentObject(inboxTabNavigation) .environmentObject(inboxTracker) - .onChange(of: shouldFilterRead) { newValue in - print("filtering read: \(newValue)") - Task(priority: .userInitiated) { - await handleShouldFilterReadChange(newShouldFilterRead: newValue) - } - } } .handleLemmyLinkResolution(navigationPath: .constant(inboxTabNavigation)) .environment(\.navigationPathWithRoutes, $inboxTabNavigation.path) .environment(\.navigation, navigation) .environment(\.scrollViewProxy, scrollProxy) } + .onChange(of: shouldFilterRead) { newValue in + Task(priority: .userInitiated) { + await handleShouldFilterReadChange(newShouldFilterRead: newValue) + } + } } @ViewBuilder private func contentView(scrollProxy: ScrollViewProxy) -> some View { From 31aad9a1899b58efff45736c3d4850772e5cbb8a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 6 Jan 2024 21:44:18 -0500 Subject: [PATCH 05/69] added cursor support to StandardTracker --- .../Trackers/Feeds/NEW PostTracker.swift | 39 ++++++++++- .../Trackers/Generics/ChildTracker.swift | 3 - .../Trackers/Generics/StandardTracker.swift | 64 ++++++++++++++++--- .../Trackers/Inbox/MentionTracker.swift | 6 +- .../Trackers/Inbox/MessageTracker.swift | 6 +- Mlem/Models/Trackers/Inbox/ReplyTracker.swift | 6 +- 6 files changed, 104 insertions(+), 20 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift index da4780d06..f3142bccc 100644 --- a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift @@ -10,6 +10,13 @@ import Foundation enum NewFeedType { case all + + var toLegacyFeedType: FeedType { + switch self { + case .all: + return .all + } + } } class NewPostTracker: StandardTracker { @@ -17,15 +24,41 @@ class NewPostTracker: StandardTracker { var unreadOnly: Bool var feedType: NewFeedType - - // var cursor: + var postSortType: PostSortType init(internetSpeed: InternetSpeed, sortType: TrackerSortType, unreadOnly: Bool, feedType: NewFeedType) { self.unreadOnly = unreadOnly self.feedType = feedType + // TODO: ERIC handle sort type + self.postSortType = .new + super.init(internetSpeed: internetSpeed, sortType: sortType) } - // override func fetchPage( + override func fetchPage(page: Int) async throws -> (items: [PostModel], cursor: String?) { + // TODO: ERIC migrate repository to use "items" + let (items, cursor) = try await postRepository.loadPage( + communityId: nil, + page: page, + cursor: nil, + sort: postSortType, + type: feedType.toLegacyFeedType, + limit: internetSpeed.pageSize + ) + return (items, cursor) + } + + override func fetchCursor(cursor: String?) async throws -> (items: [PostModel], cursor: String?) { + // TODO: ERIC migrate repository to use "items" + let (items, cursor) = try await postRepository.loadPage( + communityId: nil, + page: page, + cursor: cursor, + sort: postSortType, + type: feedType.toLegacyFeedType, + limit: internetSpeed.pageSize + ) + return (items, cursor) + } } diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index f99c4d41d..efa9621e8 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -82,14 +82,11 @@ class ChildTracker: StandardTracker< } @discardableResult override func filter(with filter: @escaping (Item) -> Bool) async -> Int { - print("[\(Item.self) tracker] filtering \(items.count) items") let newItems = items.filter(filter) let removed = items.count - newItems.count - print("[\(Item.self) tracker] filtered \(removed) items") streamCursor = 0 await setItems(newItems) - print("[\(Item.self) tracker] now contains \(items.count) items") return removed } diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 0d175fe05..9d471e858 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -50,15 +50,20 @@ class StandardTracker: CoreTracker { override func loadMoreItems() async { do { + // declare this once here to avoid nasty race conditions let pageToLoad = page + 1 if pageToLoad == 1 { + // for loading first page, always use refresh--functions identically for page and cursor try await load(action: .refresh(false)) } else { - try await load(action: .loadPage(pageToLoad)) + // for loading subsequent pages, use cursor if available, page otherwise + if let loadingCursor { + try await load(action: .loadCursor(loadingCursor)) + } else { + try await load(action: .loadPage(pageToLoad)) + } } - - // try await load(page + 1) } catch { errorHandler.handle(error) } @@ -99,9 +104,9 @@ class StandardTracker: CoreTracker { case let .loadPage(pageToLoad): print("[\(Item.self) tracker] loading page \(pageToLoad)") try await loadPageHelper(pageToLoad) - case .loadCursor: + case let .loadCursor(cursorToLoad): print("[\(Item.self) tracker] loading cursor") - assertionFailure("Not implemented!") + try await loadCursorHelper(cursorToLoad) } } @@ -111,7 +116,11 @@ class StandardTracker: CoreTracker { /// - Parameters: /// - page: page number to fetch /// - Returns: requested page of items - func fetchPage(page: Int) async throws -> [Item] { + func fetchPage(page: Int) async throws -> (items: [Item], cursor: String?) { + preconditionFailure("This method must be implemented by the inheriting class") + } + + func fetchCursor(cursor: String) async throws -> (items: [Item], cursor: String?) { preconditionFailure("This method must be implemented by the inheriting class") } @@ -162,7 +171,7 @@ class StandardTracker: CoreTracker { /// - Parameter pageToLoad: page to load /// - Warning: **DO NOT** call this method from anywhere but `load`! This is *purely* a helper function for `load` and *will* lead to unexpected behavior if called elsewhere! private func loadPageHelper(_ pageToLoad: Int) async throws { - // do not continue to load if done. this check has to come after the clear/refresh cases because those cases can be called on a .done tracker + // do not continue to load if done guard loadingState != .done else { print("[\(Item.self) tracker] done loading, will not continue") return @@ -176,8 +185,9 @@ class StandardTracker: CoreTracker { var newItems: [Item] = .init() while newItems.count < internetSpeed.pageSize { - let fetchedItems = try await fetchPage(page: page + 1) + let (fetchedItems, newLoadingCursor) = try await fetchPage(page: page + 1) page += 1 + loadingCursor = newLoadingCursor if fetchedItems.isEmpty { print("[\(Item.self) tracker] fetch returned no items, setting loading state to done") @@ -201,4 +211,42 @@ class StandardTracker: CoreTracker { await setLoading(.idle) } } + + private func loadCursorHelper(_ cursor: String) async throws { + // do not continue to load if done + guard loadingState != .done else { + print("[\(Item.self) tracker] done loading, will not continue") + return + } + + // do nothing if this is not the next page to load + guard cursor == loadingCursor else { + print("[\(Item.self) tracker] will not load cursor \(cursor) (current cursor is \(String(describing: loadingCursor))") + return + } + + var newItems: [Item] = .init() + while newItems.count < internetSpeed.pageSize { + let (fetchedItems, newLoadingCursor) = try await fetchCursor(cursor: cursor) + + if fetchedItems.isEmpty || newLoadingCursor == loadingCursor { + print("[\(Item.self) tracker] fetch returned no items or EOF cursor, setting loading state to done") + await setLoading(.done) + break + } + + loadingCursor = newLoadingCursor + page += 1 // not strictly necessary but good for tracking number of loaded pages + + newItems.append(contentsOf: fetchedItems) + } + + let allowedItems = storeIdsAndDedupe(newItems: newItems) + + await addItems(allowedItems) + + if loadingState != .done { + await setLoading(.idle) + } + } } diff --git a/Mlem/Models/Trackers/Inbox/MentionTracker.swift b/Mlem/Models/Trackers/Inbox/MentionTracker.swift index bcb62f005..f425b16e3 100644 --- a/Mlem/Models/Trackers/Inbox/MentionTracker.swift +++ b/Mlem/Models/Trackers/Inbox/MentionTracker.swift @@ -18,8 +18,10 @@ class MentionTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> [MentionModel] { - try await inboxRepository.loadMentions(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + override func fetchPage(page: Int) async throws -> (items: [MentionModel], cursor: String?) { + // TODO: can this return a cursor? + let newItems = try await inboxRepository.loadMentions(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + return (newItems, nil) } override func toParent(item: MentionModel) -> AnyInboxItem { diff --git a/Mlem/Models/Trackers/Inbox/MessageTracker.swift b/Mlem/Models/Trackers/Inbox/MessageTracker.swift index f62c88a86..08e003abf 100644 --- a/Mlem/Models/Trackers/Inbox/MessageTracker.swift +++ b/Mlem/Models/Trackers/Inbox/MessageTracker.swift @@ -17,8 +17,10 @@ class MessageTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> [MessageModel] { - try await inboxRepository.loadMessages(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + override func fetchPage(page: Int) async throws -> (items: [MessageModel], cursor: String?) { + // TODO: can this return a cursor? + let newItems = try await inboxRepository.loadMessages(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + return (newItems, nil) } override func toParent(item: MessageModel) -> AnyInboxItem { diff --git a/Mlem/Models/Trackers/Inbox/ReplyTracker.swift b/Mlem/Models/Trackers/Inbox/ReplyTracker.swift index 2854afa92..101bb1c03 100644 --- a/Mlem/Models/Trackers/Inbox/ReplyTracker.swift +++ b/Mlem/Models/Trackers/Inbox/ReplyTracker.swift @@ -18,8 +18,10 @@ class ReplyTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> [ReplyModel] { - try await inboxRepository.loadReplies(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + override func fetchPage(page: Int) async throws -> (items: [ReplyModel], cursor: String?) { + // TODO: can this return a cursor? + let newItems = try await inboxRepository.loadReplies(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) + return (newItems, nil) } override func toParent(item: ReplyModel) -> AnyInboxItem { From 2e4db5d12fdff5f099b68f4f247247cafc3a4a86 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sun, 7 Jan 2024 20:52:31 -0500 Subject: [PATCH 06/69] added most basic navigation back to feeds --- Mlem.xcodeproj/project.pbxproj | 4 ++ Mlem/ContentView.swift | 25 ++++----- .../Trackers/Feeds/NEW PostTracker.swift | 8 ++- .../Trackers/Generics/ChildTracker.swift | 2 +- .../Trackers/Generics/StandardTracker.swift | 17 ++++--- Mlem/Views/Tabs/Feeds/FeedsView.swift | 51 +++++++++++++++++++ 6 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 Mlem/Views/Tabs/Feeds/FeedsView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index c86baabb8..bf37a932e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -393,6 +393,7 @@ CD45BCEE2A75CA7200A2899C /* Thumbnail Image View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD45BCED2A75CA7200A2899C /* Thumbnail Image View.swift */; }; CD46C1F62B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */; }; CD46C1F82B0D0A8A00065953 /* View+ReselectAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */; }; + CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; @@ -933,6 +934,7 @@ CD45BCED2A75CA7200A2899C /* Thumbnail Image View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail Image View.swift"; sourceTree = ""; }; CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = ""; }; CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ReselectAction.swift"; sourceTree = ""; }; + CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; @@ -2368,6 +2370,7 @@ B11A1A772A4EFF2B00520DB4 /* Feed Root.swift */, 6332FDD427F080FA0009A98A /* Community List */, 63344C6F2A098054001BC616 /* Components */, + CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, ); path = Feeds; sourceTree = ""; @@ -3234,6 +3237,7 @@ CD391F962A535F5400E213B5 /* ResponseEditorView.swift in Sources */, 03EEEAF92ABB985D0087F8D8 /* CommunityModel.swift in Sources */, CD391F8B2A53371300E213B5 /* ExpandedPostLogic.swift in Sources */, + CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */, CDCBD7242A8D62FF00387A2C /* InstanceMetadata.swift in Sources */, CD18DC6B2A5202D4002C56BC /* MarkPersonMentionAsReadRequest.swift in Sources */, CD1824402AA8E24100D9BEB5 /* View+DestructiveConfirmation.swift in Sources */, diff --git a/Mlem/ContentView.swift b/Mlem/ContentView.swift index 564d22fcb..942cb660a 100644 --- a/Mlem/ContentView.swift +++ b/Mlem/ContentView.swift @@ -53,7 +53,8 @@ struct ContentView: View { var body: some View { FancyTabBar(selection: $tabSelection, navigationSelection: $tabNavigation, dragUpGestureCallback: showAccountSwitcherDragCallback) { Group { - FeedRoot() + // FeedRoot() + FeedsView() .fancyTabItem(tag: TabSelection.feeds) { FancyTabBarLabel( tag: TabSelection.feeds, @@ -76,18 +77,18 @@ struct ContentView: View { } ProfileView(user: myUser) - .fancyTabItem(tag: TabSelection.profile) { - FancyTabBarLabel( - tag: TabSelection.profile, - customText: appState.tabDisplayName, - symbolConfiguration: .init( - symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol, - activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol, - remoteSymbolUrl: appState.profileTabRemoteSymbolUrl + .fancyTabItem(tag: TabSelection.profile) { + FancyTabBarLabel( + tag: TabSelection.profile, + customText: appState.tabDisplayName, + symbolConfiguration: .init( + symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol, + activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol, + remoteSymbolUrl: appState.profileTabRemoteSymbolUrl + ) ) - ) - .simultaneousGesture(accountSwitchLongPress) - } + .simultaneousGesture(accountSwitchLongPress) + } SearchRoot() .fancyTabItem(tag: TabSelection.search) { diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift index f3142bccc..a8b5a0d44 100644 --- a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift @@ -9,12 +9,18 @@ import Dependencies import Foundation enum NewFeedType { - case all + case all, local, subscribed, saved var toLegacyFeedType: FeedType { switch self { case .all: return .all + case .local: + return .local + case .subscribed: + return .subscribed + case .saved: + return .all } } } diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index efa9621e8..d76535118 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -74,7 +74,7 @@ class ChildTracker: StandardTracker< } func reset(notifyParent: Bool = true) async { - await reset() + await clear() streamCursor = 0 if notifyParent, let parentTracker { await parentTracker.reset() diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 9d471e858..28923a70c 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -14,7 +14,7 @@ enum LoadAction { /// Clears the tracker case clear - /// Resets the tracker. If true, clears before resetting. + /// Refreshes the tracker, loading the first page of new items. If associated bool is true, clears the tracker before loading new items. case refresh(Bool) /// Load the requested page @@ -73,9 +73,8 @@ class StandardTracker: CoreTracker { try await load(action: .refresh(clearBeforeRefresh)) } - func reset() async { + func clear() async { do { - // try await load(0) try await load(action: .clear) } catch { assertionFailure("Exception thrown when resetting, this should not be possible!") @@ -83,7 +82,7 @@ class StandardTracker: CoreTracker { } } - // MARK: - Internal tracking methods + // MARK: - Internal methods /// Performs the requested loading operation. To account for the fact that multiple threads might request a load at the same time, this function requires that the caller pass in what it thinks is the next page or cursor to load. If that is not the next page/cursor by the time that call is allowed to execute, its request will be ignored. /// This grants this function an additional, extremely useful property: calling `await loadPage` while `loadPage` is already being executed will, practically speaking, await the in-flight request. @@ -109,10 +108,8 @@ class StandardTracker: CoreTracker { try await loadCursorHelper(cursorToLoad) } } - - // MARK: - Helpers - /// Fetches the next page of items. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. + /// Fetches the given page of items. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. /// - Parameters: /// - page: page number to fetch /// - Returns: requested page of items @@ -120,9 +117,15 @@ class StandardTracker: CoreTracker { preconditionFailure("This method must be implemented by the inheriting class") } + // Fetches items from the given cursor. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. + /// - Parameters: + /// - cursor: cursor to fetch + /// - Returns: requested list of items func fetchCursor(cursor: String) async throws -> (items: [Item], cursor: String?) { preconditionFailure("This method must be implemented by the inheriting class") } + + // MARK: - Helpers /// Filters out items according to the given filtering function. /// - Parameter filter: function that, given an Item, returns true if the item should REMAIN in the tracker diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift new file mode 100644 index 000000000..ccda2bf95 --- /dev/null +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -0,0 +1,51 @@ +// +// FeedsView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-07. +// + +import Foundation +import SwiftUI + +struct FeedsView: View { + @EnvironmentObject var appState: AppState + + var body: some View { + ScrollViewReader { _ in + NavigationSplitView { + List { + NavigationLink(value: NewFeedType.all) { + Text("All Communities") + } + + NavigationLink(value: NewFeedType.local) { + Text("Local Communities") + } + + NavigationLink(value: NewFeedType.subscribed) { + Text("Subscribed Communities") + } + + NavigationLink(value: NewFeedType.saved) { + Text("Saved Posts") + } + } + .navigationDestination(for: NewFeedType.self) { feedType in + switch feedType { + case .all: + Text("This is the all feed!") + case .local: + Text("This is the local feed!") + case .subscribed: + Text("This is the subscribed feed!") + case .saved: + Text("This is the saved feed!") + } + } + } detail: { + Text("Please select a community") + } + } + } +} From 2248d5a979fde1107ac2fcea3f1bbe38648249cb Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sun, 7 Jan 2024 21:52:02 -0500 Subject: [PATCH 07/69] incremental progress --- Mlem.xcodeproj/project.pbxproj | 4 ++++ ...vironmentValues+FeedColumnVisibility.swift | 20 +++++++++++++++++++ Mlem/Views/Tabs/Feeds/FeedsView.swift | 5 ++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index bf37a932e..034538426 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -394,6 +394,7 @@ CD46C1F62B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */; }; CD46C1F82B0D0A8A00065953 /* View+ReselectAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */; }; CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; + CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; @@ -935,6 +936,7 @@ CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = ""; }; CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ReselectAction.swift"; sourceTree = ""; }; CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; + CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; @@ -2278,6 +2280,7 @@ CDDCF6462A663849003DA3AC /* EnvironmentValues+TabSelectionHashValue.swift */, CD9A03C52B34D20500C16276 /* EnvironmentValues+Navigation.swift */, CD9A03C72B389F7000C16276 /* EnvironmentValues+FeedType.swift */, + CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */, ); path = EnvironmentValues; sourceTree = ""; @@ -3407,6 +3410,7 @@ CDCBD7262A8D69A200387A2C /* Instance Picker View.swift in Sources */, 03C905CE2B3C8DC400B9082F /* UserView+Logic.swift in Sources */, 6372185B2A3A2AAD008C4816 /* APICommunityView.swift in Sources */, + CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */, 030E86442AC6F6D5000283A6 /* SearchBar+NavigationView.swift in Sources */, 637218552A3A2AAD008C4816 /* APITagline.swift in Sources */, 6322A5CB27F77A4D00135D4F /* Loading View.swift in Sources */, diff --git a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift new file mode 100644 index 000000000..9d32c46fa --- /dev/null +++ b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift @@ -0,0 +1,20 @@ +// +// EnvironmentValues+FeedColumnVisibility.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-07. +// + +import Foundation +import SwiftUI + +private struct FeedColumnVisibility: EnvironmentKey { + static let defaultValue: NavigationSplitViewVisibility = .automatic +} + +extension EnvironmentValues { + var feedColumnVisibility: NavigationSplitViewVisibility { + get { self[FeedColumnVisibility.self] } + set { self[FeedColumnVisibility.self] = newValue } + } +} diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index ccda2bf95..b65e1cf4e 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -11,9 +11,11 @@ import SwiftUI struct FeedsView: View { @EnvironmentObject var appState: AppState + @State private var feedColumnVisibility: NavigationSplitViewVisibility = .automatic + var body: some View { ScrollViewReader { _ in - NavigationSplitView { + NavigationSplitView(columnVisibility: $feedColumnVisibility) { List { NavigationLink(value: NewFeedType.all) { Text("All Communities") @@ -46,6 +48,7 @@ struct FeedsView: View { } detail: { Text("Please select a community") } + .environment(\.feedColumnVisibility, feedColumnVisibility) } } } From 6261ce3a98e66315ccf81cf5a990667214e64b10 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 8 Jan 2024 16:02:22 -0500 Subject: [PATCH 08/69] got rudimentary nav linked up to community list --- Mlem.xcodeproj/project.pbxproj | 30 ++++- Mlem/Enums/NEW FeedType.swift | 99 ++++++++++++++ .../Trackers/Feeds/NEW PostTracker.swift | 17 --- Mlem/Navigation/Routes/AppRoutes.swift | 9 +- Mlem/Views/Tabs/Feeds/FeedsView.swift | 54 -------- .../NEW Feeds/Components/FeedRowView.swift | 126 ++++++++++++++++++ .../Components/NEW CommunityListView.swift | 91 +++++++++++++ Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 82 ++++++++++++ 8 files changed, 435 insertions(+), 73 deletions(-) create mode 100644 Mlem/Enums/NEW FeedType.swift delete mode 100644 Mlem/Views/Tabs/Feeds/FeedsView.swift create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift create mode 100644 Mlem/Views/Tabs/NEW Feeds/FeedsView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 034538426..cf13e4f27 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -395,6 +395,9 @@ CD46C1F82B0D0A8A00065953 /* View+ReselectAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */; }; CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; + CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */; }; + CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */; }; + CD4BAD412B4C721A00A1E726 /* NEW CommunityListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; @@ -937,6 +940,9 @@ CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ReselectAction.swift"; sourceTree = ""; }; CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; + CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRowView.swift; sourceTree = ""; }; + CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedType.swift"; sourceTree = ""; }; + CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityListView.swift"; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; @@ -1783,6 +1789,7 @@ 6363D5F427EE1BAE00E34822 /* Tabs */ = { isa = PBXGroup; children = ( + CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */, CD2E14782A6B283D004198DE /* Feeds */, 6DA61F7F2A55B831001EA633 /* Search */, 6DE1183A2A4A215F00810C7E /* Profile */, @@ -2069,6 +2076,7 @@ CD2053132ACBAF150000AA38 /* AvatarType.swift */, CD4368BD2AE23FA600BD8BD1 /* LoadingState.swift */, CD4368C92AE2428C00BD8BD1 /* ContentIdentifiable.swift */, + CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */, ); path = Enums; sourceTree = ""; @@ -2373,7 +2381,6 @@ B11A1A772A4EFF2B00520DB4 /* Feed Root.swift */, 6332FDD427F080FA0009A98A /* Community List */, 63344C6F2A098054001BC616 /* Components */, - CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, ); path = Feeds; sourceTree = ""; @@ -2471,6 +2478,24 @@ path = "Tracker Items"; sourceTree = ""; }; + CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */ = { + isa = PBXGroup; + children = ( + CD4BAD392B4C6C2500A1E726 /* Components */, + CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, + ); + path = "NEW Feeds"; + sourceTree = ""; + }; + CD4BAD392B4C6C2500A1E726 /* Components */ = { + isa = PBXGroup; + children = ( + CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, + CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */, + ); + path = Components; + sourceTree = ""; + }; CD525F662A4B892900BCA794 /* Links */ = { isa = PBXGroup; children = ( @@ -3068,6 +3093,7 @@ ADDC9E3A2A5CEAA100383D58 /* BlockPerson.swift in Sources */, CD6F29A82A77FF1700F20B6B /* MarkPostRead.swift in Sources */, 031A617E2B1CE90F00ABF23B /* ChangePasswordView.swift in Sources */, + CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */, 6372186B2A3A2AAD008C4816 /* GetComments.swift in Sources */, B1DD00BD2A62DDEC002A7B39 /* RecognizedLemmyInstances.swift in Sources */, 6DA61F892A575DF1001EA633 /* URL+WithIconSize.swift in Sources */, @@ -3243,8 +3269,10 @@ CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */, CDCBD7242A8D62FF00387A2C /* InstanceMetadata.swift in Sources */, CD18DC6B2A5202D4002C56BC /* MarkPersonMentionAsReadRequest.swift in Sources */, + CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */, CD1824402AA8E24100D9BEB5 /* View+DestructiveConfirmation.swift in Sources */, CD82A2502A7162D400111034 /* GetPersonUnreadCount.swift in Sources */, + CD4BAD412B4C721A00A1E726 /* NEW CommunityListView.swift in Sources */, 030D00882AD1BB2600953B1D /* UserModel+ContentModel.swift in Sources */, CD82A24C2A70A26900111034 /* View+CustomBadge.swift in Sources */, B1CB6E752A4C729D00DA9675 /* Bundle+IconFileName.swift in Sources */, diff --git a/Mlem/Enums/NEW FeedType.swift b/Mlem/Enums/NEW FeedType.swift new file mode 100644 index 000000000..7ed88589e --- /dev/null +++ b/Mlem/Enums/NEW FeedType.swift @@ -0,0 +1,99 @@ +// +// NEW FeedType.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-08. +// + +import Foundation +import SwiftUI + +enum NewFeedType: String, CaseIterable { + case all, local, subscribed, saved + + var label: String { + rawValue.capitalized + } + + static func fromShortcut(shortcut: String?) -> NewFeedType? { + switch shortcut { + case "All": + return .all + case "Local": + return .local + case "Subscribed": + return .subscribed + case "Saved": + return .saved + default: + return nil + } + } + + var toLegacyFeedType: FeedType { + switch self { + case .all: + return .all + case .local: + return .local + case .subscribed: + return .subscribed + case .saved: + return .all + } + } +} + +extension NewFeedType: Identifiable { + var id: Self { self } +} + +extension NewFeedType: AssociatedIcon { + var iconName: String { + switch self { + case .all: Icons.federatedFeed + case .local: Icons.localFeed + case .subscribed: Icons.subscribedFeed + case .saved: Icons.savedFeed + } + } + + var iconNameFill: String { + switch self { + case .all: Icons.federatedFeedFill + case .local: Icons.localFeedFill + case .subscribed: Icons.subscribedFeedFill + case .saved: Icons.savedFeedFill + } + } + + var iconNameCircle: String { + switch self { + case .all: Icons.federatedFeedCircle + case .local: Icons.localFeedCircle + case .subscribed: Icons.subscribedFeedCircle + case .saved: Icons.savedFeedCircle + } + } + + /// Icon to use in system settings. This should be removed when the "unified symbol handling" is closed + var settingsIconName: String { + switch self { + case .all: "circle.hexagongrid" + case .local: "house" + case .subscribed: "newspaper" + case .saved: Icons.save + } + } +} + +extension NewFeedType: AssociatedColor { + var color: Color? { + switch self { + case .all: .blue + case .local: .red + case .subscribed: .red + case .saved: .green + } + } +} diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift index a8b5a0d44..dd9278df4 100644 --- a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift @@ -8,23 +8,6 @@ import Dependencies import Foundation -enum NewFeedType { - case all, local, subscribed, saved - - var toLegacyFeedType: FeedType { - switch self { - case .all: - return .all - case .local: - return .local - case .subscribed: - return .subscribed - case .saved: - return .all - } - } -} - class NewPostTracker: StandardTracker { @Dependency(\.postRepository) var postRepository diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index 23543b59d..b1da264b4 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -12,6 +12,9 @@ import Foundation /// For simple (i.e. linear) navigation flows, you may wish to define a separate set of routes. For example, see `OnboardingRoutes`. /// enum AppRoute: Routable { + // case feed(NewFeedType) + + // TODO: ERIC remove this case communityLinkWithContext(CommunityLinkWithContext) case apiPostView(APIPostView) @@ -27,6 +30,7 @@ enum AppRoute: Routable { case lazyLoadPostLinkWithContext(LazyLoadPostLinkWithContext) // MARK: - Settings + case settings(SettingsPage) case aboutSettings(AboutSettingsPage) case appearanceSettings(AppearanceSettingsPage) @@ -35,8 +39,11 @@ enum AppRoute: Routable { case licenseSettings(LicensesSettingsPage) // swiftlint:disable cyclomatic_complexity - static func makeRoute(_ value: V) throws -> AppRoute where V: Hashable { + static func makeRoute(_ value: some Hashable) throws -> AppRoute { switch value { +// case let value as NewFeedType: +// print("Navigating to new feed type value!") +// return .feed(value) case let value as CommunityLinkWithContext: return .communityLinkWithContext(value) case let value as APIPostView: diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift deleted file mode 100644 index b65e1cf4e..000000000 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// FeedsView.swift -// Mlem -// -// Created by Eric Andrews on 2024-01-07. -// - -import Foundation -import SwiftUI - -struct FeedsView: View { - @EnvironmentObject var appState: AppState - - @State private var feedColumnVisibility: NavigationSplitViewVisibility = .automatic - - var body: some View { - ScrollViewReader { _ in - NavigationSplitView(columnVisibility: $feedColumnVisibility) { - List { - NavigationLink(value: NewFeedType.all) { - Text("All Communities") - } - - NavigationLink(value: NewFeedType.local) { - Text("Local Communities") - } - - NavigationLink(value: NewFeedType.subscribed) { - Text("Subscribed Communities") - } - - NavigationLink(value: NewFeedType.saved) { - Text("Saved Posts") - } - } - .navigationDestination(for: NewFeedType.self) { feedType in - switch feedType { - case .all: - Text("This is the all feed!") - case .local: - Text("This is the local feed!") - case .subscribed: - Text("This is the subscribed feed!") - case .saved: - Text("This is the saved feed!") - } - } - } detail: { - Text("Please select a community") - } - .environment(\.feedColumnVisibility, feedColumnVisibility) - } - } -} diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift new file mode 100644 index 000000000..9dc3bd460 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift @@ -0,0 +1,126 @@ +// +// FeedRowView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-08. +// + +import Dependencies +import Foundation +import SwiftUI + +struct FeedRowView: View { + let feedType: NewFeedType + + var body: some View { + HStack { + Image(systemName: feedType.iconNameCircle) + .resizable() + .frame(width: 36, height: 36) + .foregroundColor(feedType.color) + + VStack(alignment: .leading) { + Text(feedType.label) + } + } + } +} + +struct CommunityFeedRowView: View { + @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker + @Dependency(\.hapticManager) var hapticManager + @Dependency(\.notifier) var notifier + + let community: APICommunity + let subscribed: Bool + let communitySubscriptionChanged: (APICommunity, Bool) -> Void + let navigationContext: NavigationContext + + var body: some View { + HStack { + communityNameLabel + + Spacer() + + Button("Favorite Community") { + hapticManager.play(haptic: .success, priority: .high) + toggleFavorite() + } + .buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) + .accessibilityHidden(true) + }.swipeActions { + if subscribed { + Button("Unsubscribe") { + Task(priority: .userInitiated) { + await subscribe(communityId: community.id, shouldSubscribe: false) + } + }.tint(.red) // Destructive role seems to remove from list so just make it red + } else { + Button("Subscribe") { + Task(priority: .userInitiated) { + await subscribe(communityId: community.id, shouldSubscribe: true) + } + }.tint(.blue) + } + } + .accessibilityAction(named: "Toggle favorite") { + toggleFavorite() + } + .accessibilityElement(children: .combine) + .accessibilityLabel(communityLabel) + } + + private var communityNameText: Text { + Text(community.name) + } + + @ViewBuilder + private var communityNameLabel: some View { + if let website = community.actorId.host(percentEncoded: false) { + communityNameText + + Text("@\(website)") + .font(.footnote) + .foregroundColor(.gray.opacity(0.5)) + } else { + communityNameText + } + } + + private var communityLabel: String { + var label = community.name + + if let website = community.actorId.host(percentEncoded: false) { + label += "@\(website)" + } + + if isFavorited() { + label += ", is a favorite" + } + + return label + } + + private func toggleFavorite() { + if isFavorited() { + favoriteCommunitiesTracker.unfavorite(community) + UIAccessibility.post(notification: .announcement, argument: "Unfavorited \(community.name)") + Task { + await notifier.add(.success("Unfavorited \(community.name)")) + } + } else { + favoriteCommunitiesTracker.favorite(community) + UIAccessibility.post(notification: .announcement, argument: "Favorited \(community.name)") + Task { + await notifier.add(.success("Favorited \(community.name)")) + } + } + } + + private func isFavorited() -> Bool { + favoriteCommunitiesTracker.isFavorited(community) + } + + private func subscribe(communityId: Int, shouldSubscribe: Bool) async { + communitySubscriptionChanged(community, shouldSubscribe) + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift new file mode 100644 index 000000000..74d5a64a3 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift @@ -0,0 +1,91 @@ +// +// Community List View.swift +// Mlem +// +// Created by Jake Shirey on 17.06.2023. +// + +import Dependencies +import SwiftUI + +struct NewCommunityListView: View { + @StateObject private var model: CommunityListModel = .init() + + @Binding var selectedFeedType: NewFeedType? + + /// Set to `false` on disappear. + @State private var appeared: Bool = false + + init(selectedCommunity: Binding) { + self._selectedFeedType = selectedCommunity + } + + // MARK: - Body + + var body: some View { + ScrollViewReader { scrollProxy in + HStack { + List(selection: $selectedFeedType) { + HomepageFeedRowView(.subscribed) + .padding(.top, 5) + .id("top") // For "scroll to top" sidebar item + HomepageFeedRowView(.local) + HomepageFeedRowView(.all) + + ForEach(model.visibleSections) { section in + Section(header: headerView(for: section)) { + ForEach(model.communities(for: section)) { community in + CommuntiyFeedRowView( + community: community, + subscribed: model.isSubscribed(to: community), + communitySubscriptionChanged: model.updateSubscriptionStatus, + navigationContext: .sidebar + ) + } + } + } + } + .fancyTabScrollCompatible() + .navigationTitle("Communities") + .navigationBarColor() + .listStyle(PlainListStyle()) + .scrollIndicators(.hidden) + .onAppear { + appeared = true + } + .onDisappear { + appeared = false + } + + SectionIndexTitles(proxy: scrollProxy, communitySections: model.allSections()) + } + .reselectAction(tab: .feeds) { + guard appeared else { + return + } + withAnimation { + scrollProxy.scrollTo("top", anchor: .bottom) + } + } + } + .refreshable { + await model.load() + } + .onAppear { + Task(priority: .high) { + await model.load() + } + } + } + + // MARK: - Subviews + + private func headerView(for section: CommunitySection) -> some View { + HStack { + Text(section.inlineHeaderLabel!) + .accessibilityLabel(section.accessibilityLabel) + Spacer() + } + .id(section.viewId) + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift new file mode 100644 index 000000000..482015378 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -0,0 +1,82 @@ +// +// FeedsView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-07. +// + +import Foundation +import SwiftUI + +struct FeedsView: View { + @Environment(\.scenePhase) var scenePhase + + @EnvironmentObject var appState: AppState + + @State private var selectedFeed: NewFeedType? + + @StateObject private var communityListModel: CommunityListModel = .init() + + var body: some View { + content + .onAppear { + Task(priority: .high) { + await communityListModel.load() + } + } + .onChange(of: scenePhase) { newPhase in + if newPhase == .active, let shortcutItem = NewFeedType.fromShortcut(shortcut: shortcutItemToProcess?.type) { + selectedFeed = shortcutItem + } + } + } + + var content: some View { + ScrollViewReader { _ in + NavigationSplitView { + List(selection: $selectedFeed) { + ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in + FeedRowView(feedType: feedType) + } + + ForEach(communityListModel.visibleSections) { section in + Section(header: headerView(for: section)) { + ForEach(communityListModel.communities(for: section)) { community in + NavigationLink(value: NewFeedType.all) { + CommunityFeedRowView( + community: community, + subscribed: communityListModel.isSubscribed(to: community), + communitySubscriptionChanged: communityListModel.updateSubscriptionStatus, + navigationContext: .sidebar + ) + } + } + } + } + } + } detail: { + switch selectedFeed { + case .all: + Text("This is the all feed!") + case .local: + Text("This is the local feed!") + case .subscribed: + Text("This is the subscribed feed!") + case .saved: + Text("This is the saved feed!") + case .none: + Text("Please select a feed") + } + } + } + } + + private func headerView(for section: CommunitySection) -> some View { + HStack { + Text(section.inlineHeaderLabel!) + .accessibilityLabel(section.accessibilityLabel) + Spacer() + } + .id(section.viewId) + } +} From 0f50aa54762c975b3d33d183acb3a7c42f01005c Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 12 Jan 2024 12:41:09 -0500 Subject: [PATCH 09/69] rebuilding aggregate feed view --- Mlem.xcodeproj/project.pbxproj | 16 ++-- ...racker.swift => StandardPostTracker.swift} | 15 ++- .../Trackers/Generics/ChildTracker.swift | 7 ++ .../Trackers/Generics/CoreTracker.swift | 9 +- .../Trackers/Generics/ParentTracker.swift | 10 +- Mlem/Repositories/PostRepository.swift | 2 + .../Components/NEW CommunityListView.swift | 91 ------------------- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 15 +-- Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift | 52 +++++++++++ 9 files changed, 95 insertions(+), 122 deletions(-) rename Mlem/Models/Trackers/Feeds/{NEW PostTracker.swift => StandardPostTracker.swift} (73%) delete mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift create mode 100644 Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index cf13e4f27..4de2d9009 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -327,7 +327,7 @@ CD05E7792A4E381A0081D102 /* PostSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD05E7782A4E381A0081D102 /* PostSize.swift */; }; CD05E77F2A4F263B0081D102 /* Menu Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD05E77E2A4F263B0081D102 /* Menu Function.swift */; }; CD0BE42F2A65A73600314B24 /* Haptic Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0BE42E2A65A73600314B24 /* Haptic Manager.swift */; }; - CD12627A2B4759BC007549F9 /* NEW PostTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1262792B4759BC007549F9 /* NEW PostTracker.swift */; }; + CD12627A2B4759BC007549F9 /* StandardPostTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1262792B4759BC007549F9 /* StandardPostTracker.swift */; }; CD12627D2B475E45007549F9 /* PostModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD12627C2B475E45007549F9 /* PostModel+TrackerItem.swift */; }; CD1446182A58FC3B00610EF1 /* InfoStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446172A58FC3B00610EF1 /* InfoStackView.swift */; }; CD14461B2A5A4B6D00610EF1 /* PostSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD14461A2A5A4B6D00610EF1 /* PostSettingsView.swift */; }; @@ -397,7 +397,7 @@ CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */; }; CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */; }; - CD4BAD412B4C721A00A1E726 /* NEW CommunityListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */; }; + CD4BAD432B507F2B00A1E726 /* NEW FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; @@ -872,7 +872,7 @@ CD05E7782A4E381A0081D102 /* PostSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSize.swift; sourceTree = ""; }; CD05E77E2A4F263B0081D102 /* Menu Function.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Function.swift"; sourceTree = ""; }; CD0BE42E2A65A73600314B24 /* Haptic Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Haptic Manager.swift"; sourceTree = ""; }; - CD1262792B4759BC007549F9 /* NEW PostTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostTracker.swift"; sourceTree = ""; }; + CD1262792B4759BC007549F9 /* StandardPostTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardPostTracker.swift; sourceTree = ""; }; CD12627B2B475A80007549F9 /* README - Generic Trackers.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "README - Generic Trackers.md"; sourceTree = ""; }; CD12627C2B475E45007549F9 /* PostModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostModel+TrackerItem.swift"; sourceTree = ""; }; CD1446172A58FC3B00610EF1 /* InfoStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoStackView.swift; sourceTree = ""; }; @@ -942,7 +942,7 @@ CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRowView.swift; sourceTree = ""; }; CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedType.swift"; sourceTree = ""; }; - CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityListView.swift"; sourceTree = ""; }; + CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedView.swift"; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; @@ -2182,7 +2182,7 @@ CD1262782B47597E007549F9 /* Feeds */ = { isa = PBXGroup; children = ( - CD1262792B4759BC007549F9 /* NEW PostTracker.swift */, + CD1262792B4759BC007549F9 /* StandardPostTracker.swift */, ); path = Feeds; sourceTree = ""; @@ -2483,6 +2483,7 @@ children = ( CD4BAD392B4C6C2500A1E726 /* Components */, CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, + CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */, ); path = "NEW Feeds"; sourceTree = ""; @@ -2491,7 +2492,6 @@ isa = PBXGroup; children = ( CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, - CD4BAD402B4C721A00A1E726 /* NEW CommunityListView.swift */, ); path = Components; sourceTree = ""; @@ -3106,7 +3106,7 @@ 507573942A5AD59E00AA7ABD /* EquatableError.swift in Sources */, CDE6A81A2A490B970062D161 /* Inbox ReplyBodyView.swift in Sources */, 50811B3C2A92059C006BA3F2 /* BlockCommunityResponse+Mock.swift in Sources */, - CD12627A2B4759BC007549F9 /* NEW PostTracker.swift in Sources */, + CD12627A2B4759BC007549F9 /* StandardPostTracker.swift in Sources */, CD391F9E2A539F1800E213B5 /* ReplyToMention.swift in Sources */, CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */, 500C168E2A66FAAB006F243B /* HapticManager+Dependency.swift in Sources */, @@ -3168,6 +3168,7 @@ CDDB08782A5DF1330075BFEE /* CommentSettingsView.swift in Sources */, 6386E02C2A03D1EC006B3C1D /* App State.swift in Sources */, 504106CD2A744D7F000AAEF8 /* CommentRepository+Dependency.swift in Sources */, + CD4BAD432B507F2B00A1E726 /* NEW FeedView.swift in Sources */, 6372186F2A3A2AAD008C4816 /* SearchRequest.swift in Sources */, 03EC92952AC064AE007BBE7E /* SearchHomeView.swift in Sources */, CD46C1F62B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */, @@ -3272,7 +3273,6 @@ CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */, CD1824402AA8E24100D9BEB5 /* View+DestructiveConfirmation.swift in Sources */, CD82A2502A7162D400111034 /* GetPersonUnreadCount.swift in Sources */, - CD4BAD412B4C721A00A1E726 /* NEW CommunityListView.swift in Sources */, 030D00882AD1BB2600953B1D /* UserModel+ContentModel.swift in Sources */, CD82A24C2A70A26900111034 /* View+CustomBadge.swift in Sources */, B1CB6E752A4C729D00DA9675 /* Bundle+IconFileName.swift in Sources */, diff --git a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift similarity index 73% rename from Mlem/Models/Trackers/Feeds/NEW PostTracker.swift rename to Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index dd9278df4..38d38d392 100644 --- a/Mlem/Models/Trackers/Feeds/NEW PostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -1,5 +1,5 @@ // -// NEW PostTracker.swift +// StandardPostTracker.swift // Mlem // // Created by Eric Andrews on 2024-01-04. @@ -8,21 +8,20 @@ import Dependencies import Foundation -class NewPostTracker: StandardTracker { +/// Post tracker for use with single feeds. Supports all post sorting types, but is not suitable for multi-feed use. +class StandardPostTracker: StandardTracker { @Dependency(\.postRepository) var postRepository var unreadOnly: Bool var feedType: NewFeedType - var postSortType: PostSortType + private(set) var postSortType: PostSortType - init(internetSpeed: InternetSpeed, sortType: TrackerSortType, unreadOnly: Bool, feedType: NewFeedType) { + init(internetSpeed: InternetSpeed, sortType: PostSortType, unreadOnly: Bool, feedType: NewFeedType) { self.unreadOnly = unreadOnly self.feedType = feedType + self.postSortType = sortType - // TODO: ERIC handle sort type - self.postSortType = .new - - super.init(internetSpeed: internetSpeed, sortType: sortType) + super.init(internetSpeed: internetSpeed) } override func fetchPage(page: Int) async throws -> (items: [PostModel], cursor: String?) { diff --git a/Mlem/Models/Trackers/Generics/ChildTracker.swift b/Mlem/Models/Trackers/Generics/ChildTracker.swift index d76535118..b56666bc2 100644 --- a/Mlem/Models/Trackers/Generics/ChildTracker.swift +++ b/Mlem/Models/Trackers/Generics/ChildTracker.swift @@ -10,7 +10,14 @@ class ChildTracker: StandardTracker< private weak var parentTracker: (any ParentTrackerProtocol)? private var streamCursor: Int = 0 + private(set) var sortType: TrackerSortType + var allItems: [ParentItem] { items.map { toParent(item: $0) }} + + init(internetSpeed: InternetSpeed, sortType: TrackerSortType) { + self.sortType = sortType + super.init(internetSpeed: internetSpeed) + } func toParent(item: Item) -> ParentItem { preconditionFailure("This method must be implemented by the inheriting class") diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index 23530efb2..96a8078ba 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -17,11 +17,14 @@ class CoreTracker: ObservableObject { private(set) var fallbackThreshold: ContentModelIdentifier? private(set) var internetSpeed: InternetSpeed - private(set) var sortType: TrackerSortType + // private(set) var sortType: TrackerSortType - init(internetSpeed: InternetSpeed, sortType: TrackerSortType) { +// init(internetSpeed: InternetSpeed, sortType: TrackerSortType) { +// self.internetSpeed = internetSpeed +// self.sortType = sortType +// } + init(internetSpeed: InternetSpeed) { self.internetSpeed = internetSpeed - self.sortType = sortType } /// If the given item is the loading threshold item, loads more content diff --git a/Mlem/Models/Trackers/Generics/ParentTracker.swift b/Mlem/Models/Trackers/Generics/ParentTracker.swift index 7efe2e8cb..74bbe402a 100644 --- a/Mlem/Models/Trackers/Generics/ParentTracker.swift +++ b/Mlem/Models/Trackers/Generics/ParentTracker.swift @@ -14,11 +14,14 @@ class ParentTracker: CoreTracker, ParentTrackerProtocol private var childTrackers: [any ChildTrackerProtocol] = .init() private let loadingSemaphore: AsyncSemaphore = .init(value: 1) + + private(set) var sortType: TrackerSortType init(internetSpeed: InternetSpeed, sortType: TrackerSortType, childTrackers: [any ChildTrackerProtocol]) { self.childTrackers = childTrackers + self.sortType = sortType - super.init(internetSpeed: internetSpeed, sortType: sortType) + super.init(internetSpeed: internetSpeed) for child in self.childTrackers { child.setParentTracker(self) @@ -85,11 +88,6 @@ class ParentTracker: CoreTracker, ParentTrackerProtocol } } } -// items.forEach { item in -// if !filter(item) { -// uidsToFilter.insert(item.uid) -// } -// } // function to remove items from child trackers based on uid--this makes the Item-specific filtering applied here generically applicable to any child tracker let filterFunc = { (item: any TrackerItem) in diff --git a/Mlem/Repositories/PostRepository.swift b/Mlem/Repositories/PostRepository.swift index ef31e540c..03f76db54 100644 --- a/Mlem/Repositories/PostRepository.swift +++ b/Mlem/Repositories/PostRepository.swift @@ -33,6 +33,8 @@ class PostRepository { communityName: communityName ) + print(response.nextPage) + let posts = response.posts.map { PostModel(from: $0) } return (posts, response.nextPage) } diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift deleted file mode 100644 index 74d5a64a3..000000000 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW CommunityListView.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Community List View.swift -// Mlem -// -// Created by Jake Shirey on 17.06.2023. -// - -import Dependencies -import SwiftUI - -struct NewCommunityListView: View { - @StateObject private var model: CommunityListModel = .init() - - @Binding var selectedFeedType: NewFeedType? - - /// Set to `false` on disappear. - @State private var appeared: Bool = false - - init(selectedCommunity: Binding) { - self._selectedFeedType = selectedCommunity - } - - // MARK: - Body - - var body: some View { - ScrollViewReader { scrollProxy in - HStack { - List(selection: $selectedFeedType) { - HomepageFeedRowView(.subscribed) - .padding(.top, 5) - .id("top") // For "scroll to top" sidebar item - HomepageFeedRowView(.local) - HomepageFeedRowView(.all) - - ForEach(model.visibleSections) { section in - Section(header: headerView(for: section)) { - ForEach(model.communities(for: section)) { community in - CommuntiyFeedRowView( - community: community, - subscribed: model.isSubscribed(to: community), - communitySubscriptionChanged: model.updateSubscriptionStatus, - navigationContext: .sidebar - ) - } - } - } - } - .fancyTabScrollCompatible() - .navigationTitle("Communities") - .navigationBarColor() - .listStyle(PlainListStyle()) - .scrollIndicators(.hidden) - .onAppear { - appeared = true - } - .onDisappear { - appeared = false - } - - SectionIndexTitles(proxy: scrollProxy, communitySections: model.allSections()) - } - .reselectAction(tab: .feeds) { - guard appeared else { - return - } - withAnimation { - scrollProxy.scrollTo("top", anchor: .bottom) - } - } - } - .refreshable { - await model.load() - } - .onAppear { - Task(priority: .high) { - await model.load() - } - } - } - - // MARK: - Subviews - - private func headerView(for section: CommunitySection) -> some View { - HStack { - Text(section.inlineHeaderLabel!) - .accessibilityLabel(section.accessibilityLabel) - Spacer() - } - .id(section.viewId) - } -} diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index 482015378..c54997e88 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -34,14 +34,17 @@ struct FeedsView: View { var content: some View { ScrollViewReader { _ in NavigationSplitView { + // Note on navigation: nesting List(selection: $selectedFeed) inside a NavigationSplitView here automagically sets up navigation so that nav links inside this block update selectedFeed, which is then handled by the switch in the detail. This can also be achieved by defining .navigationDestinations on the List; those will then propagate to the detail when selected, but due to the amount of manual navigation stuff we're doing this approach seems less troublesome [Eric 2023.01.11] List(selection: $selectedFeed) { ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in + // These are automagically turned into NavigationLinks FeedRowView(feedType: feedType) } ForEach(communityListModel.visibleSections) { section in - Section(header: headerView(for: section)) { + Section(header: communitySectionHeaderView(for: section)) { ForEach(communityListModel.communities(for: section)) { community in + // These are not automagically turned into NavigationLinks, so we do it manually NavigationLink(value: NewFeedType.all) { CommunityFeedRowView( community: community, @@ -57,13 +60,13 @@ struct FeedsView: View { } detail: { switch selectedFeed { case .all: - Text("This is the all feed!") + AggregateFeedView(feedType: .all) case .local: - Text("This is the local feed!") + AggregateFeedView(feedType: .local) case .subscribed: - Text("This is the subscribed feed!") + AggregateFeedView(feedType: .subscribed) case .saved: - Text("This is the saved feed!") + AggregateFeedView(feedType: .saved) case .none: Text("Please select a feed") } @@ -71,7 +74,7 @@ struct FeedsView: View { } } - private func headerView(for section: CommunitySection) -> some View { + private func communitySectionHeaderView(for section: CommunitySection) -> some View { HStack { Text(section.inlineHeaderLabel!) .accessibilityLabel(section.accessibilityLabel) diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift new file mode 100644 index 000000000..a29ff44c0 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift @@ -0,0 +1,52 @@ +// +// NEW FeedView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-11. +// + +import Dependencies +import Foundation +import SwiftUI + +/// View for post feeds aggregating multiple communities (all, local, subscribed, saved) +struct AggregateFeedView: View { + @StateObject var postTracker: StandardPostTracker + + // TODO: sorting + + init(feedType: NewFeedType) { + // need to grab some stuff from app storage to initialize post tracker with + @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast + @AppStorage("upvoteOnSave") var upvoteOnSave = false + + // TODO: ERIC handle sort type + + self._postTracker = .init(wrappedValue: .init( + internetSpeed: internetSpeed, + sortType: .hot, + unreadOnly: false, + feedType: feedType + )) + } + + var body: some View { + content + .onAppear { + Task { + await postTracker.loadMoreItems() + } + } + } + + @ViewBuilder + var content: some View { + Text("I'm a general feed!") + Text("The post tracker contains \(postTracker.items.count) items") + Button("More") { + Task { + await postTracker.loadMoreItems() + } + } + } +} From c9d741586b998a080413260b37804569a368f357 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 12 Jan 2024 13:03:20 -0500 Subject: [PATCH 10/69] tested page and cursor loading --- Mlem/Repositories/PostRepository.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mlem/Repositories/PostRepository.swift b/Mlem/Repositories/PostRepository.swift index 03f76db54..ef31e540c 100644 --- a/Mlem/Repositories/PostRepository.swift +++ b/Mlem/Repositories/PostRepository.swift @@ -33,8 +33,6 @@ class PostRepository { communityName: communityName ) - print(response.nextPage) - let posts = response.posts.map { PostModel(from: $0) } return (posts, response.nextPage) } From 5c6de5da2aa0e22554a01dfd6397afcf46ced415 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 13 Jan 2024 21:36:31 -0500 Subject: [PATCH 11/69] implemented core feed --- Mlem.xcodeproj/project.pbxproj | 4 ++ .../Trackers/Generics/CoreTracker.swift | 7 +--- .../Trackers/Generics/StandardTracker.swift | 4 ++ .../Components/NEW PostFeedView.swift | 42 +++++++++++++++++++ Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift | 15 +++---- 5 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 4de2d9009..2c7684df3 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -455,6 +455,7 @@ CDB45C602AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C5F2AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift */; }; CDB45C622AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */; }; CDB45C642AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */; }; + CDBCBA202B537A4B0070F60D /* NEW PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */; }; CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */; }; CDC1C93F2A7AB8C700072E3D /* AccessibilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */; }; CDC1C9412A7ABA9C00072E3D /* ReadMarkStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */; }; @@ -1000,6 +1001,7 @@ CDB45C5F2AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MentionModel+TrackerItem.swift"; sourceTree = ""; }; CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReplyModel+TrackerItem.swift"; sourceTree = ""; }; CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageModel+TrackerItem.swift"; sourceTree = ""; }; + CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView.swift"; sourceTree = ""; }; CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetSpeed.swift; sourceTree = ""; }; CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilitySettingsView.swift; sourceTree = ""; }; CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkStyle.swift; sourceTree = ""; }; @@ -2492,6 +2494,7 @@ isa = PBXGroup; children = ( CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, + CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, ); path = Components; sourceTree = ""; @@ -3250,6 +3253,7 @@ CD29ED3B2B2E8624006937CE /* String+IsNotEmpty.swift in Sources */, CD391F9A2A537EF900E213B5 /* CommentBodyView.swift in Sources */, 63344C562A07D81D001BC616 /* Array+Prepend.swift in Sources */, + CDBCBA202B537A4B0070F60D /* NEW PostFeedView.swift in Sources */, CDDCF64F2A672C0A003DA3AC /* FancyTabBarLabel.swift in Sources */, CD04D5D92A3614BE008EF95B /* Large Post.swift in Sources */, CDF8425E2A49E61A00723DA0 /* APIPersonMention.swift in Sources */, diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index 96a8078ba..dc0f9e67b 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -17,12 +17,7 @@ class CoreTracker: ObservableObject { private(set) var fallbackThreshold: ContentModelIdentifier? private(set) var internetSpeed: InternetSpeed - // private(set) var sortType: TrackerSortType - -// init(internetSpeed: InternetSpeed, sortType: TrackerSortType) { -// self.internetSpeed = internetSpeed -// self.sortType = sortType -// } + init(internetSpeed: InternetSpeed) { self.internetSpeed = internetSpeed } diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 28923a70c..81fdbac14 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -186,6 +186,8 @@ class StandardTracker: CoreTracker { return } + await setLoading(.loading) + var newItems: [Item] = .init() while newItems.count < internetSpeed.pageSize { let (fetchedItems, newLoadingCursor) = try await fetchPage(page: page + 1) @@ -228,6 +230,8 @@ class StandardTracker: CoreTracker { return } + await setLoading(.loading) + var newItems: [Item] = .init() while newItems.count < internetSpeed.pageSize { let (fetchedItems, newLoadingCursor) = try await fetchCursor(cursor: cursor) diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift new file mode 100644 index 000000000..994ba927a --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -0,0 +1,42 @@ +// +// NEW PostFeedView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-13. +// + +import Foundation +import SwiftUI + +struct NewPostFeedView: View { + @AppStorage("shouldShowPostCreator") var shouldShowPostCreator: Bool = true + + @ObservedObject var postTracker: StandardPostTracker + + var body: some View { + if postTracker.items.isEmpty { + Text("No posts!") + } else { + LazyVStack(spacing: 0) { + ForEach(postTracker.items, id: \.uid) { feedPost(for: $0) } + EndOfFeedView(loadingState: postTracker.loadingState, viewType: .hobbit) + } + } + } + + @ViewBuilder + private func feedPost(for post: PostModel) -> some View { + VStack(spacing: 0) { + // TODO: reenable nav + FeedPost( + post: post, + community: post.community, + showPostCreator: shouldShowPostCreator, + showCommunity: false // TODO: show community + ) + // .onAppear { postTracker.loadIfThreshold(post) } + Divider() + } + .buttonStyle(EmptyButtonStyle()) // Make it so that the link doesn't mess with the styling + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift index a29ff44c0..bd8a1944f 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift @@ -33,20 +33,17 @@ struct AggregateFeedView: View { var body: some View { content .onAppear { - Task { - await postTracker.loadMoreItems() - } + Task { await postTracker.loadMoreItems() } } + .background(Color.secondarySystemBackground) + .fancyTabScrollCompatible() } @ViewBuilder var content: some View { - Text("I'm a general feed!") - Text("The post tracker contains \(postTracker.items.count) items") - Button("More") { - Task { - await postTracker.loadMoreItems() - } + ScrollView { + NewPostFeedView(postTracker: postTracker) + .background(Color.secondarySystemBackground) } } } From 33f67aa1c9e906decce2a652f7278668dc7bdd4a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 15 Jan 2024 17:46:37 -0500 Subject: [PATCH 12/69] progress towards no posts view --- .../Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift new file mode 100644 index 000000000..5c1ad738b --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift @@ -0,0 +1,8 @@ +// +// NEW NoPostsView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-14. +// + +import Foundation From 5ad862dfc161496225293885e0ca02e397add167 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 15 Jan 2024 17:46:46 -0500 Subject: [PATCH 13/69] progress towards no posts view --- Mlem.xcodeproj/project.pbxproj | 4 + .../Trackers/Feeds/StandardPostTracker.swift | 3 + .../Components/NEW NoPostsView.swift | 79 ++++++++++++++++++- .../Components/NEW PostFeedView.swift | 37 +++++++-- Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift | 3 +- 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 2c7684df3..d315b725f 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -456,6 +456,7 @@ CDB45C622AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */; }; CDB45C642AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */; }; CDBCBA202B537A4B0070F60D /* NEW PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */; }; + CDBCBA242B54A5F40070F60D /* NEW NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */; }; CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */; }; CDC1C93F2A7AB8C700072E3D /* AccessibilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */; }; CDC1C9412A7ABA9C00072E3D /* ReadMarkStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */; }; @@ -1002,6 +1003,7 @@ CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReplyModel+TrackerItem.swift"; sourceTree = ""; }; CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageModel+TrackerItem.swift"; sourceTree = ""; }; CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView.swift"; sourceTree = ""; }; + CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW NoPostsView.swift"; sourceTree = ""; }; CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetSpeed.swift; sourceTree = ""; }; CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilitySettingsView.swift; sourceTree = ""; }; CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkStyle.swift; sourceTree = ""; }; @@ -2495,6 +2497,7 @@ children = ( CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, + CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */, ); path = Components; sourceTree = ""; @@ -3142,6 +3145,7 @@ CDB45C5E2AF1A96C00A1FF08 /* AssociatedColorProtocol.swift in Sources */, CD3FBCE92A4B482700B2063F /* Generic Merge.swift in Sources */, E47B2B762A902DE200629AF7 /* SettingsValues.swift in Sources */, + CDBCBA242B54A5F40070F60D /* NEW NoPostsView.swift in Sources */, CDA145ED2A510AC100DDAFC9 /* MarkCommentReplyAsReadRequest.swift in Sources */, CD391F982A537E8E00E213B5 /* ReplyToComment.swift in Sources */, 5064D03D2A6DE0AA00B22EE3 /* Notifier.swift in Sources */, diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 38d38d392..d1c6851f0 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -8,6 +8,9 @@ import Dependencies import Foundation +// TODO: +// - re-enable hidden item counts + /// Post tracker for use with single feeds. Supports all post sorting types, but is not suitable for multi-feed use. class StandardPostTracker: StandardTracker { @Dependency(\.postRepository) var postRepository diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift index 5c1ad738b..ecd3b37f1 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift @@ -1,8 +1,81 @@ // -// NEW NoPostsView.swift +// NoPostsView.swift // Mlem // -// Created by Eric Andrews on 2024-01-14. +// Created by Sjmarf on 10/10/2023. // -import Foundation +import SwiftUI + +struct NewNoPostsView: View { + @EnvironmentObject var postTracker: PostTracker + + let loadingState: LoadingState + @Binding var postSortType: PostSortType + @Binding var showReadPosts: Bool + + var body: some View { + VStack { + if loadingState != .loading { + VStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { +// let unreadItems = postTracker.hiddenItems[.read, default: 0] + + Image(systemName: Icons.noPosts) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 35) + .padding(.bottom, 12) + // .frame(width: unreadItems == 0 ? 35 : 50) + // .padding(.bottom, unreadItems == 0 ? 8: 12) + Text(title) +// +// if unreadItems != 0 { +// Text( +// "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." +// ) +// .foregroundStyle(.tertiary) +// .multilineTextAlignment(.center) +// .fixedSize(horizontal: false, vertical: true) +// .padding(.horizontal, 20) +// +// } +// buttons + } + .foregroundStyle(.secondary) + } + } + } + + var title: String { + if PostSortType.topTypes.contains(postSortType), postSortType != .topAll { + return "No posts found from the last \(postSortType.label.lowercased())." + } + return "No posts found." + } + + @ViewBuilder + var buttons: some View { + VStack { + if postSortType != .hot { + Button { + postSortType = .hot + } label: { + Label("Switch to Hot", systemImage: Icons.hotSort) + } + } + if postTracker.hiddenItems[.read, default: 0] > 0 { + Button { + if !showReadPosts { + showReadPosts = true + } + } label: { + Text("Show read posts") + } + } + } + .foregroundStyle(.secondary) + .buttonStyle(.bordered) + .padding(.top) + .padding(.horizontal, 20) + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 994ba927a..cc81d6931 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -9,15 +9,21 @@ import Foundation import SwiftUI struct NewPostFeedView: View { + @AppStorage("shouldShowPostCreator") var shouldShowPostCreator: Bool = true + @AppStorage("showReadPosts") var showReadPosts: Bool = true @ObservedObject var postTracker: StandardPostTracker + @Binding var postSortType: PostSortType + let showCommunity: Bool + + @State var errorDetails: ErrorDetails? var body: some View { - if postTracker.items.isEmpty { - Text("No posts!") - } else { - LazyVStack(spacing: 0) { + LazyVStack(spacing: 0) { + if postTracker.items.isEmpty { // && !(postTracker.loadingState == .loading) { + noPostsView() + } else { ForEach(postTracker.items, id: \.uid) { feedPost(for: $0) } EndOfFeedView(loadingState: postTracker.loadingState, viewType: .hobbit) } @@ -32,11 +38,30 @@ struct NewPostFeedView: View { post: post, community: post.community, showPostCreator: shouldShowPostCreator, - showCommunity: false // TODO: show community + showCommunity: showCommunity ) - // .onAppear { postTracker.loadIfThreshold(post) } + .onAppear { postTracker.loadIfThreshold(post) } Divider() } .buttonStyle(EmptyButtonStyle()) // Make it so that the link doesn't mess with the styling } + + @ViewBuilder + private func noPostsView() -> some View { + VStack { + if postTracker.loadingState == .loading { // don't show posts until site information loads to avoid jarring redraw + LoadingView(whatIsLoading: .posts) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .transition(.opacity) + } else if let errorDetails { + ErrorView(errorDetails) + .frame(maxWidth: .infinity) + } else { + NewNoPostsView(loadingState: postTracker.loadingState, postSortType: $postSortType, showReadPosts: $showReadPosts) + .transition(.scale(scale: 0.9).combined(with: .opacity)) + .padding(.top, 25) + } + } + .animation(.easeOut(duration: 0.1), value: postTracker.loadingState) + } } diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift index bd8a1944f..e3da6bc89 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift @@ -14,6 +14,7 @@ struct AggregateFeedView: View { @StateObject var postTracker: StandardPostTracker // TODO: sorting + @State var postSortType: PostSortType = .hot init(feedType: NewFeedType) { // need to grab some stuff from app storage to initialize post tracker with @@ -42,7 +43,7 @@ struct AggregateFeedView: View { @ViewBuilder var content: some View { ScrollView { - NewPostFeedView(postTracker: postTracker) + NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) .background(Color.secondarySystemBackground) } } From bdf5b06e7a306ec36d3a637720833d24e1c2dc79 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 15 Jan 2024 17:47:04 -0500 Subject: [PATCH 14/69] new PostFeedView --- Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index cc81d6931..2e5cb85ee 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -9,7 +9,6 @@ import Foundation import SwiftUI struct NewPostFeedView: View { - @AppStorage("shouldShowPostCreator") var shouldShowPostCreator: Bool = true @AppStorage("showReadPosts") var showReadPosts: Bool = true From 0eea1d53a34e085e36bdc5544d2d210e93ac9afd Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 15 Jan 2024 18:08:19 -0500 Subject: [PATCH 15/69] moved voting into PostModel --- Mlem/Models/Content/Post Model.swift | 98 +++++++++++++++++++++---- Mlem/Views/Shared/Posts/Feed Post.swift | 4 +- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index b5767a3b6..f7ec2603c 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -5,25 +5,33 @@ // Created by Eric Andrews on 2023-08-26. // +import Dependencies import Foundation /// Internal model to represent a post /// Note: this is just the first pass at decoupling the internal models from the API models--to avoid massive merge conflicts and an unreviewably large PR, I've kept the structure practically identical, and will slowly morph it over the course of several PRs. Eventually all of the API types that this model uses will go away and everything downstream of the repositories won't ever know there's an API at all :) -struct PostModel { - let postId: Int - let post: APIPost - let creator: UserModel - let community: CommunityModel - var votes: VotesModel - let numReplies: Int - let saved: Bool - let read: Bool - let published: Date - let updated: Date? - let links: [LinkType] +class PostModel: ContentIdentifiable, ObservableObject { + @Dependency(\.hapticManager) var hapticManager + @Dependency(\.errorHandler) var errorHandler + @Dependency(\.postRepository) var postRepository + + var postId: Int + var post: APIPost + var creator: UserModel + var community: CommunityModel + @Published var votes: VotesModel + var numReplies: Int + var saved: Bool + var read: Bool + var published: Date + var updated: Date? + var links: [LinkType] var uid: ContentModelIdentifier { .init(contentType: .post, contentId: postId) } + // prevents a voting operation from ocurring while another is ocurring + var voting: Bool = false + /// Creates a PostModel from an APIPostView /// - Parameter apiPostView: APIPostView to create a PostModel representation of init(from apiPostView: APIPostView) { @@ -80,6 +88,58 @@ struct PostModel { self.links = PostModel.parseLinks(from: self.post.body) } + // MARK: Main Actor State Change Methods + + @MainActor func reinit(from postModel: PostModel) { + postId = postModel.postId + post = postModel.post + creator = postModel.creator + community = postModel.community + votes = postModel.votes + numReplies = postModel.numReplies + saved = postModel.saved + read = postModel.read + published = postModel.published + updated = postModel.updated + links = postModel.links + } + + @MainActor + func setVotes(_ newVotes: VotesModel) { + votes = newVotes + } + + // MARK: Interaction Methods + + func vote(inputOp: ScoringOperation) async { + guard !voting else { + return + } + + voting = true + defer { voting = false } + + hapticManager.play(haptic: .lightSuccess, priority: .low) + let operation = votes.myVote == inputOp ? ScoringOperation.resetVote : inputOp + + let original: PostModel = .init(from: self) + + // state fake + await setVotes(votes.applyScoringOperation(operation: operation)) + hapticManager.play(haptic: .lightSuccess, priority: .low) + + do { + let updatedPost = try await postRepository.ratePost(postId: postId, operation: operation) + await reinit(from: updatedPost) + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + await reinit(from: original) + } + } + + // MARK: Utility Methods + var postType: PostType { // post with URL: either image or link if let postUrl = post.linkUrl { @@ -103,10 +163,6 @@ struct PostModel { } } -extension PostModel: Identifiable { - var id: Int { hashValue } -} - extension PostModel: Hashable { /// Hashes all fields for which state changes should trigger view updates. func hash(into hasher: inout Hasher) { @@ -117,3 +173,13 @@ extension PostModel: Hashable { hasher.combine(post.updated) } } + +extension PostModel: Identifiable { + var id: Int { hashValue } +} + +extension PostModel: Equatable { + static func == (lhs: PostModel, rhs: PostModel) -> Bool { + lhs.id == rhs.id + } +} diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index 3920122bb..388a13794 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -55,7 +55,7 @@ struct FeedPost: View { // MARK: Parameters - let post: PostModel + @ObservedObject var post: PostModel let community: CommunityModel? let showPostCreator: Bool let showCommunity: Bool @@ -285,7 +285,7 @@ struct FeedPost: View { /// Votes on a post /// - Parameter inputOp: The vote operation to perform func voteOnPost(inputOp: ScoringOperation) async { - await postTracker.voteOnPost(post: post, inputOp: inputOp) + await post.vote(inputOp: inputOp) } func savePost() async { From 38bd0b39b754ccf37db68d37379bed331410c8ea Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Tue, 16 Jan 2024 20:03:36 -0500 Subject: [PATCH 16/69] moved most post methods into PostModel --- Mlem/Models/Content/Post Model.swift | 97 ++++++++++++++++--- .../Trackers/Feeds/StandardPostTracker.swift | 45 +++++++++ Mlem/Repositories/PostRepository.swift | 6 +- .../Components/Thumbnail Image View.swift | 3 +- Mlem/Views/Shared/Posts/Feed Post.swift | 4 +- Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift | 25 ++++- 6 files changed, 162 insertions(+), 18 deletions(-) diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index f7ec2603c..247f35d9b 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -21,8 +21,8 @@ class PostModel: ContentIdentifiable, ObservableObject { var community: CommunityModel @Published var votes: VotesModel var numReplies: Int - var saved: Bool - var read: Bool + @Published var saved: Bool + @Published var read: Bool var published: Date var updated: Date? var links: [LinkType] @@ -109,25 +109,28 @@ class PostModel: ContentIdentifiable, ObservableObject { votes = newVotes } + @MainActor + func setRead(_ newRead: Bool) { + read = newRead + } + + @MainActor + func setSaved(_ newSaved: Bool) { + saved = newSaved + } + // MARK: Interaction Methods func vote(inputOp: ScoringOperation) async { - guard !voting else { - return - } - - voting = true - defer { voting = false } - hapticManager.play(haptic: .lightSuccess, priority: .low) let operation = votes.myVote == inputOp ? ScoringOperation.resetVote : inputOp - let original: PostModel = .init(from: self) - // state fake + let original: PostModel = .init(from: self) await setVotes(votes.applyScoringOperation(operation: operation)) - hapticManager.play(haptic: .lightSuccess, priority: .low) + await setRead(true) + // API call do { let updatedPost = try await postRepository.ratePost(postId: postId, operation: operation) await reinit(from: updatedPost) @@ -138,6 +141,76 @@ class PostModel: ContentIdentifiable, ObservableObject { } } + func markRead(_ newRead: Bool) async { + // state fake + let original: PostModel = .init(from: self) + await setRead(newRead) + + // API call + do { + let updatedPost = try await postRepository.markRead(post: self, read: newRead) + await reinit(from: updatedPost) + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + await reinit(from: original) + } + } + + func toggleSave(upvoteOnSave: Bool) async { + let shouldSave: Bool = !saved + + // state fake + let original: PostModel = .init(from: self) + await setSaved(shouldSave) + await setRead(true) + if upvoteOnSave, votes.myVote != .upvote { + await setVotes(votes.applyScoringOperation(operation: .upvote)) + } + + // API call + do { + let saveResponse = try await postRepository.savePost(postId: postId, shouldSave: shouldSave) + + if shouldSave, upvoteOnSave { + let voteResponse = try await postRepository.ratePost(postId: postId, operation: .upvote) + await reinit(from: voteResponse) + } else { + await reinit(from: saveResponse) + } + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + await reinit(from: original) + } + } + + func edit( + name: String?, + url: String?, + body: String?, + nsfw: Bool? + ) async { + // TODO: state fake + + // API call + do { + hapticManager.play(haptic: .success, priority: .high) + let response = try await postRepository.editPost(postId: postId, name: name, url: url, body: body, nsfw: nsfw) + await reinit(from: response) + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + } + } + + // TODO: implement + func delete(updateTrackers: (() async -> Void)?) async { + if let updateTrackers { + await updateTrackers() + } + } + // MARK: Utility Methods var postType: PostType { diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index d1c6851f0..11462d258 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -7,6 +7,7 @@ import Dependencies import Foundation +import Nuke // TODO: // - re-enable hidden item counts @@ -19,6 +20,13 @@ class StandardPostTracker: StandardTracker { var feedType: NewFeedType private(set) var postSortType: PostSortType + // prefetching + private let prefetcher = ImagePrefetcher( + pipeline: ImagePipeline.shared, + destination: .memoryCache, + maxConcurrentRequestCount: 40 + ) + init(internetSpeed: InternetSpeed, sortType: PostSortType, unreadOnly: Bool, feedType: NewFeedType) { self.unreadOnly = unreadOnly self.feedType = feedType @@ -37,6 +45,7 @@ class StandardPostTracker: StandardTracker { type: feedType.toLegacyFeedType, limit: internetSpeed.pageSize ) + preloadImages(items) return (items, cursor) } @@ -50,6 +59,42 @@ class StandardPostTracker: StandardTracker { type: feedType.toLegacyFeedType, limit: internetSpeed.pageSize ) + preloadImages(items) return (items, cursor) } + + private func preloadImages(_ newPosts: [PostModel]) { + URLSession.shared.configuration.urlCache = AppConstants.urlCache + var imageRequests: [ImageRequest] = [] + for post in newPosts { + // preload user and community avatars--fetching both because we don't know which we'll need, but these are super tiny + // so it's probably not an API crime, right? + if let communityAvatarLink = post.community.avatar { + imageRequests.append(ImageRequest(url: communityAvatarLink.withIconSize(Int(AppConstants.smallAvatarSize * 2)))) + } + + if let userAvatarLink = post.creator.avatar { + imageRequests.append(ImageRequest(url: userAvatarLink.withIconSize(Int(AppConstants.largeAvatarSize * 2)))) + } + + switch post.postType { + case let .image(url): + // images: only load the image + imageRequests.append(ImageRequest(url: url, priority: .high)) + case let .link(url): + // websites: load image and favicon + if let baseURL = post.post.linkUrl?.host, + let favIconURL = URL(string: "https://www.google.com/s2/favicons?sz=64&domain=\(baseURL)") { + imageRequests.append(ImageRequest(url: favIconURL)) + } + if let url { + imageRequests.append(ImageRequest(url: url, priority: .high)) + } + default: + break + } + } + + prefetcher.startPrefetching(with: imageRequests) + } } diff --git a/Mlem/Repositories/PostRepository.swift b/Mlem/Repositories/PostRepository.swift index ef31e540c..b0e3165fb 100644 --- a/Mlem/Repositories/PostRepository.swift +++ b/Mlem/Repositories/PostRepository.swift @@ -56,7 +56,7 @@ class PostRepository { return PostModel(from: post, read: success ? read : post.read) } - /// Rates a given post. Does not care what the current vote state is; sends the given request no matter what (i.e., calling this with operation .upvote on an already upvoted post will not send a .resetVote, but will instead send a second idempotent .upvote + /// Rates a given post. Does not care what the current vote state is; sends the given request no matter what (i.e., calling this with operation `.upvote` on an already upvoted post will not send a `.resetVote`, but will instead send a second idempotent `.upvote`) /// - Parameters: /// - postId: id of the post to rate /// - operation: ScoringOperation to apply to the given post id @@ -73,7 +73,9 @@ class PostRepository { /// - Returns: PostModel representing the new state of the post func savePost(postId: Int, shouldSave: Bool) async throws -> PostModel { let postView = try await apiClient.savePost(id: postId, shouldSave: shouldSave) - return PostModel(from: postView) + let ret: PostModel = .init(from: postView) + ret.read = true // the API call sets read to true but doesn't include that in the response so we do it here + return ret } func deletePost(postId: Int, shouldDelete: Bool) async throws -> PostModel { diff --git a/Mlem/Views/Shared/Components/Thumbnail Image View.swift b/Mlem/Views/Shared/Components/Thumbnail Image View.swift index a8faaf881..a99232325 100644 --- a/Mlem/Views/Shared/Components/Thumbnail Image View.swift +++ b/Mlem/Views/Shared/Components/Thumbnail Image View.swift @@ -11,7 +11,6 @@ import SwiftUI struct ThumbnailImageView: View { @AppStorage("shouldBlurNsfw") var shouldBlurNsfw: Bool = true - @EnvironmentObject var postTracker: PostTracker @Dependency(\.errorHandler) var errorHandler @Dependency(\.postRepository) var postRepository @@ -76,7 +75,7 @@ struct ThumbnailImageView: View { /// Synchronous void wrapper for postTracker.markRead to pass into CachedImage as dismiss callback func markPostAsRead() { Task(priority: .userInitiated) { - await postTracker.markRead(post: post) + await post.markRead(true) } } } diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index 388a13794..771c046ed 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -41,6 +41,8 @@ struct FeedPost: View { @AppStorage("reakMarkStyle") var readMarkStyle: ReadMarkStyle = .bar @AppStorage("readBarThickness") var readBarThickness: Int = 3 + + @AppStorage("upvoteOnSave") var upvoteOnSave: Bool = false @EnvironmentObject var postTracker: PostTracker @EnvironmentObject var editorTracker: EditorTracker @@ -289,7 +291,7 @@ struct FeedPost: View { } func savePost() async { - await postTracker.toggleSave(post: post) + await post.toggleSave(upvoteOnSave: upvoteOnSave) } func reportPost() { diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift index e3da6bc89..e0adb3270 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift @@ -11,6 +11,8 @@ import SwiftUI /// View for post feeds aggregating multiple communities (all, local, subscribed, saved) struct AggregateFeedView: View { + @Dependency(\.errorHandler) var errorHandler + @StateObject var postTracker: StandardPostTracker // TODO: sorting @@ -36,8 +38,29 @@ struct AggregateFeedView: View { .onAppear { Task { await postTracker.loadMoreItems() } } - .background(Color.secondarySystemBackground) + .refreshable { + await Task { + do { + _ = try await postTracker.refresh(clearBeforeRefresh: false) + } catch { + errorHandler.handle(error) + } + }.value + } + .background { + VStack(spacing: 0) { + Color.systemBackground + Color.secondarySystemBackground + } + } .fancyTabScrollCompatible() + .toolbar { + ToolbarItem(placement: .principal) { + Text("Feed!") + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarColor(visibility: .automatic) } @ViewBuilder From 2cf8c5ea8559e45ab9f2ac345392e19e4b7da329 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 17 Jan 2024 11:04:37 -0500 Subject: [PATCH 17/69] moved editing to PostModel --- Mlem/Views/Shared/Composer/PostComposerView+Logic.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift index c1186399c..350d85051 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift @@ -6,8 +6,8 @@ // import Foundation -import SwiftUI import PhotosUI +import SwiftUI extension PostComposerView { var hasPostContent: Bool { @@ -25,7 +25,7 @@ extension PostComposerView { var isValidURL: Bool { guard attachmentModel.url.lowercased().hasPrefix("http://") || - attachmentModel.url.lowercased().hasPrefix("https://") else { + attachmentModel.url.lowercased().hasPrefix("https://") else { return false // URL protocol is missing } @@ -53,10 +53,10 @@ extension PostComposerView { isSubmitting = true if let post = editModel.editPost { - let editedPost = await postTracker.edit(post: post, name: postTitle, url: attachmentModel.url, body: postBody, nsfw: isNSFW) + await post.edit(name: postTitle, url: attachmentModel.url, body: postBody, nsfw: isNSFW) if let responseCallback = editModel.responseCallback { - responseCallback(editedPost) + responseCallback(post) } } else { From 950bc402821b372e30d28d6ce6ae151ec6f89b81 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 17 Jan 2024 11:29:11 -0500 Subject: [PATCH 18/69] removed unnecessary calls to postTracker from post editing --- Mlem/Models/Composers/PostEditor.swift | 31 +++++-------------- .../CommunityModel+MenuFunctions.swift | 25 ++++++++------- Mlem/Models/Content/Post Model.swift | 4 +-- .../Trackers/Generics/CoreTracker.swift | 11 +++++++ .../Trackers/Generics/StandardTracker.swift | 2 ++ .../Composer/PostComposerView+Logic.swift | 10 ++---- .../Shared/Composer/PostComposerView.swift | 10 +++--- .../Shared/Posts/ExpandedPostLogic.swift | 10 +----- Mlem/Views/Shared/Posts/Feed Post.swift | 3 +- 9 files changed, 44 insertions(+), 62 deletions(-) diff --git a/Mlem/Models/Composers/PostEditor.swift b/Mlem/Models/Composers/PostEditor.swift index 8509888d6..eab90c22f 100644 --- a/Mlem/Models/Composers/PostEditor.swift +++ b/Mlem/Models/Composers/PostEditor.swift @@ -12,38 +12,23 @@ struct PostEditorModel: Identifiable { var id: Int { community.communityId } let community: CommunityModel - var postTracker: PostTracker! + let postTracker: StandardPostTracker? let editPost: PostModel? - var responseCallback: ((PostModel) -> Void)? + /// Initializer for creating a post. If `postTracker` is provided, the new post will be prepended to it. init( community: CommunityModel, - postTracker: PostTracker? = nil, - responseCallback: ((PostModel) -> Void)? = nil + postTracker: StandardPostTracker? ) { self.community = community + self.postTracker = postTracker self.editPost = nil - self.responseCallback = responseCallback - self.initialiseTracker(postTracker) } - init( - post: PostModel, - postTracker: PostTracker? = nil, - responseCallback: ((PostModel) -> Void)? = nil - ) { - self.editPost = post + /// Initializer for editing a post + init(post: PostModel) { self.community = post.community - self.responseCallback = responseCallback - self.initialiseTracker(postTracker) - } - - private mutating func initialiseTracker(_ postTracker: PostTracker?) { - @AppStorage("upvoteOnSave") var upvoteOnSave = false - if let postTracker { - self.postTracker = postTracker - } else { - self.postTracker = .init(shouldPerformMergeSorting: false, internetSpeed: .slow, upvoteOnSave: upvoteOnSave) - } + self.postTracker = nil + self.editPost = post } } diff --git a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift index aee6f65d7..5d6855d26 100644 --- a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift +++ b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift @@ -10,17 +10,18 @@ import SwiftUI extension CommunityModel { func newPostMenuFunction(editorTracker: EditorTracker, postTracker: PostTracker? = nil) -> MenuFunction { - return .standardMenuFunction( - text: "New Post", - imageName: Icons.sendFill, - destructiveActionPrompt: nil, - enabled: true - ) { - editorTracker.openEditor(with: PostEditorModel( - community: self, - postTracker: postTracker - )) - } + .standardMenuFunction( + text: "New Post", + imageName: Icons.sendFill, + destructiveActionPrompt: nil, + enabled: true + ) { + assertionFailure("ERIC RE-IMPLEMENT THIS") +// editorTracker.openEditor(with: PostEditorModel( +// community: self, +// postTracker: postTracker +// )) + } } func subscribeMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) throws -> StandardMenuFunction { @@ -46,7 +47,7 @@ extension CommunityModel { } func favoriteMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> StandardMenuFunction { - return .init( + .init( text: favorited ? "Unfavorite" : "Favorite", imageName: favorited ? Icons.unfavorite : Icons.favorite, destructiveActionPrompt: favorited ? "Really unfavorite \(community.name)?" : nil, diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index f9f23d7b9..3b4d8afb6 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -196,9 +196,7 @@ class PostModel: ContentIdentifiable, ObservableObject { body: String?, nsfw: Bool? ) async { - // TODO: state fake - - // API call + // no need to state fake because editor spins until call completes do { hapticManager.play(haptic: .success, priority: .high) let response = try await postRepository.editPost(postId: postId, name: name, url: url, body: body, nsfw: nsfw) diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index dc0f9e67b..f82a15a82 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -58,6 +58,17 @@ class CoreTracker: ObservableObject { updateThresholds() } + func synchronousPrependItem(_ newItem: Item) { + Task { + await prependItem(newItem) + } + } + + @MainActor + func prependItem(_ newItem: Item) async { + items.prepend(newItem) + } + private func updateThresholds() { if items.isEmpty { threshold = nil diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 81fdbac14..3846fdadb 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -81,6 +81,8 @@ class StandardTracker: CoreTracker { await clearHelper() // this is not a thread-safe use of clear, but I'm using it here because we should never get here } } + + func prepend(_ item: Item) async {} // MARK: - Internal methods diff --git a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift index 350d85051..aebe0e7c4 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift @@ -54,11 +54,6 @@ extension PostComposerView { if let post = editModel.editPost { await post.edit(name: postTitle, url: attachmentModel.url, body: postBody, nsfw: isNSFW) - - if let responseCallback = editModel.responseCallback { - responseCallback(post) - } - } else { let response = try await apiClient.createPost( communityId: editModel.community.communityId, @@ -70,9 +65,10 @@ extension PostComposerView { hapticManager.play(haptic: .success, priority: .high) - await MainActor.run { + // TODO: ERIC test this + if let postTracker = editModel.postTracker { withAnimation { - postTracker.prepend(PostModel(from: response.postView)) + postTracker.synchronousPrependItem(_:)(PostModel(from: response.postView)) } } } diff --git a/Mlem/Views/Shared/Composer/PostComposerView.swift b/Mlem/Views/Shared/Composer/PostComposerView.swift index 1a3bd0daf..63bd03c7b 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView.swift @@ -6,8 +6,8 @@ // import Dependencies -import SwiftUI import PhotosUI +import SwiftUI extension HorizontalAlignment { enum LabelStart: AlignmentID { @@ -32,7 +32,7 @@ struct PostComposerView: View { @Environment(\.dismiss) var dismiss - let postTracker: PostTracker + // let postTracker: PostTracker let editModel: PostEditorModel @AppStorage("promptUser.permission.privacy.allowImageUploads") var askedForPermissionToUploadImages: Bool = false @@ -49,14 +49,14 @@ struct PostComposerView: View { @State var isShowingErrorDialog: Bool = false @State var errorDialogMessage: String = "" - @State var uploadTask: Task<(), any Error>? + @State var uploadTask: Task? @Environment(\.layoutDirection) var layoutDirection @FocusState private var focusedField: Field? init(editModel: PostEditorModel) { - self.postTracker = editModel.postTracker + // self.postTracker = editModel.postTracker self.editModel = editModel self._postTitle = State(initialValue: editModel.editPost?.post.name ?? "") @@ -143,7 +143,6 @@ struct PostComposerView: View { .frame(width: AppConstants.thumbnailSize, height: AppConstants.thumbnailSize) } VStack(alignment: .leading) { - if attachmentModel.imageModel?.state == nil { Text("Attached Image") } else { @@ -236,7 +235,6 @@ struct PostComposerView: View { } } .accessibilityLabel("Toggle NSFW") - } ToolbarItem(placement: .navigationBarTrailing) { LinkUploadOptionsView(model: attachmentModel) { diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index 3088fe52d..c2a11f880 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -157,11 +157,7 @@ extension ExpandedPost { destructiveActionPrompt: nil, enabled: true ) { - editorTracker.openEditor(with: PostEditorModel( - post: post, - postTracker: postTracker, - responseCallback: updatePost - )) + editorTracker.openEditor(with: PostEditorModel(post: post)) }) // delete @@ -263,8 +259,4 @@ extension ExpandedPost { return newComment } } - - func updatePost(newPost: PostModel) { - post = newPost - } } diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index d47afaa91..df0e43742 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -280,8 +280,7 @@ struct FeedPost: View { func editPost() { editorTracker.openEditor(with: PostEditorModel( - post: post, - postTracker: postTracker + post: post )) } From 430a04659a58ac0d4ce715b3a485e41d1bb676ee Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 18 Jan 2024 11:25:42 -0500 Subject: [PATCH 19/69] got filtering by read working --- Mlem.xcodeproj/project.pbxproj | 4 + .../Trackers/Feeds/StandardPostTracker.swift | 129 ++++++++++++++++-- .../NEW PostFeedView+MenuFunctions.swift | 81 +++++++++++ .../Components/NEW PostFeedView.swift | 33 ++++- Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift | 3 +- 5 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index d315b725f..314c2b38c 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -465,6 +465,7 @@ CDC65D8F2A86B6DD007205E5 /* DeleteUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC65D8E2A86B6DD007205E5 /* DeleteUser.swift */; }; CDC65D912A86B830007205E5 /* DeleteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC65D902A86B830007205E5 /* DeleteAccountView.swift */; }; CDC6A8CA2A6F1C8D00CC11AC /* AssociatedIconProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6A8C92A6F1C8D00CC11AC /* AssociatedIconProtocol.swift */; }; + CDCA28D42B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */; }; CDCBD7242A8D62FF00387A2C /* InstanceMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7232A8D62FF00387A2C /* InstanceMetadata.swift */; }; CDCBD7262A8D69A200387A2C /* Instance Picker View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7252A8D69A200387A2C /* Instance Picker View.swift */; }; CDCBD7282A8D6B7700387A2C /* Instance Picker View Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7272A8D6B7700387A2C /* Instance Picker View Logic.swift */; }; @@ -1012,6 +1013,7 @@ CDC65D8E2A86B6DD007205E5 /* DeleteUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteUser.swift; sourceTree = ""; }; CDC65D902A86B830007205E5 /* DeleteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountView.swift; sourceTree = ""; }; CDC6A8C92A6F1C8D00CC11AC /* AssociatedIconProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedIconProtocol.swift; sourceTree = ""; }; + CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; CDCBD7232A8D62FF00387A2C /* InstanceMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceMetadata.swift; sourceTree = ""; }; CDCBD7252A8D69A200387A2C /* Instance Picker View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance Picker View.swift"; sourceTree = ""; }; CDCBD7272A8D6B7700387A2C /* Instance Picker View Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance Picker View Logic.swift"; sourceTree = ""; }; @@ -2498,6 +2500,7 @@ CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */, + CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */, ); path = Components; sourceTree = ""; @@ -3225,6 +3228,7 @@ 6D7782362A48EED8008AC1BF /* APIPrivateMessage.swift in Sources */, CDE3BA892A8C64BD00B972E2 /* Collapsible Text Item.swift in Sources */, 50CC4A742A9CB10B0074C845 /* TimestampedValue.swift in Sources */, + CDCA28D42B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift in Sources */, 505240E72A88D36D00EA4558 /* SectionIndexTitles.swift in Sources */, 5064D0452A71549C00B22EE3 /* NotificationMessage.swift in Sources */, E4F0B56F2ABD00A000BC3E4A /* View+PresentationBackgroundInteraction.swift in Sources */, diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 11462d258..0913e641e 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -9,16 +9,47 @@ import Dependencies import Foundation import Nuke -// TODO: -// - re-enable hidden item counts +/// Enumeration of reasons a post could be filtered +enum NewPostFilterReason: Hashable { + /// Post is filtered because it was read + case read + + /// Post is filtered because it contains a blocked keyword + case keyword + + /// Post is filtered because the user is blocked (associated value is user id) + case blockedUser(Int) + + /// Post is filtered because community is blocked (associated value is community id) + case blockedCommunity(Int) + + func hash(into hasher: inout Hasher) { + switch self { + case .read: + hasher.combine("read") + case .keyword: + hasher.combine("keyword") + case let .blockedUser(userId): + hasher.combine("blockedUser") + hasher.combine(userId) + case let .blockedCommunity(communityId): + hasher.combine("blockedCommunity") + hasher.combine(communityId) + } + } +} /// Post tracker for use with single feeds. Supports all post sorting types, but is not suitable for multi-feed use. class StandardPostTracker: StandardTracker { @Dependency(\.postRepository) var postRepository + @Dependency(\.persistenceRepository) var persistenceRepository + + // TODO: ERIC keyword filters could be more elegant + var filteredKeywords: [String] - var unreadOnly: Bool var feedType: NewFeedType private(set) var postSortType: PostSortType + private var filters: [NewPostFilterReason: Int] // prefetching private let prefetcher = ImagePrefetcher( @@ -27,14 +58,29 @@ class StandardPostTracker: StandardTracker { maxConcurrentRequestCount: 40 ) - init(internetSpeed: InternetSpeed, sortType: PostSortType, unreadOnly: Bool, feedType: NewFeedType) { - self.unreadOnly = unreadOnly + init(internetSpeed: InternetSpeed, sortType: PostSortType, showReadPosts: Bool, feedType: NewFeedType) { + @Dependency(\.persistenceRepository) var persistenceRepository + self.feedType = feedType self.postSortType = sortType + self.filteredKeywords = persistenceRepository.loadFilteredKeywords() + self.filters = [.keyword: 0] + if !showReadPosts { + print("hiding read posts") + filters[.read] = 0 + } + super.init(internetSpeed: internetSpeed) } + override func refresh(clearBeforeRefresh: Bool) async throws { + filteredKeywords = persistenceRepository.loadFilteredKeywords() + try await super.refresh(clearBeforeRefresh: clearBeforeRefresh) + } + + // MARK: StandardTracker Loading Methods + override func fetchPage(page: Int) async throws -> (items: [PostModel], cursor: String?) { // TODO: ERIC migrate repository to use "items" let (items, cursor) = try await postRepository.loadPage( @@ -45,8 +91,10 @@ class StandardPostTracker: StandardTracker { type: feedType.toLegacyFeedType, limit: internetSpeed.pageSize ) - preloadImages(items) - return (items, cursor) + + let filteredItems = filter(items) + preloadImages(filteredItems) + return (filteredItems, cursor) } override func fetchCursor(cursor: String?) async throws -> (items: [PostModel], cursor: String?) { @@ -59,8 +107,71 @@ class StandardPostTracker: StandardTracker { type: feedType.toLegacyFeedType, limit: internetSpeed.pageSize ) - preloadImages(items) - return (items, cursor) + + let filteredItems = filter(items) + preloadImages(filteredItems) + return (filteredItems, cursor) + } + + // MARK: Custom Behavior + + func applyFilter(_ newFilter: NewPostFilterReason) async { + guard !filters.keys.contains(newFilter) else { + assertionFailure("Cannot apply new filter (already present in filters!)") + return + } + + filters[newFilter] = 0 + await setItems(filter(items)) + } + + func removeFilter(_ filterToRemove: NewPostFilterReason) async { + guard filters.keys.contains(filterToRemove) else { + assertionFailure("Cannot remove filter (not present in filters!)") + return + } + + filters.removeValue(forKey: filterToRemove) + do { + try await refresh(clearBeforeRefresh: true) + } catch { + errorHandler.handle(error) + } + } + + /// Filters a given list of posts. Updates the counts of filtered posts in `filters` + /// - Parameter posts: list of posts to filter + /// - Returns: list of posts with filtered posts removed + private func filter(_ posts: [PostModel]) -> [PostModel] { + var ret: [PostModel] = .init() + + for post in posts { + if let filterReason = shouldFilterPost(post) { + filters[filterReason] = filters[filterReason, default: 0] + 1 + } else { + ret.append(post) + } + } + + return ret + } + + /// Given a post, determines whether it should be filtered + /// - Returns: the first reason according to which the post should be filtered, if applicable, or nil if the post should not be filtered + private func shouldFilterPost(_ postModel: PostModel) -> NewPostFilterReason? { + for filter in filters.keys { + switch filter { + case .read: + if postModel.read { return filter } + case .keyword: + if postModel.post.name.lowercased().contains(filteredKeywords) { return filter } + case let .blockedUser(userId): + if postModel.creator.userId == userId { return filter } + case let .blockedCommunity(communityId): + if postModel.community.communityId == communityId { return filter } + } + } + return nil } private func preloadImages(_ newPosts: [PostModel]) { diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift new file mode 100644 index 000000000..81febfc41 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift @@ -0,0 +1,81 @@ +// +// PostFeedView+MenuFunctions.swift +// Mlem +// +// Created by Sjmarf on 31/12/2023. +// + +import Foundation + +extension NewPostFeedView { + func genOuterSortMenuFunctions() -> [MenuFunction] { + PostSortType.availableOuterTypes.map { type in + let isSelected = postSortType == type + let imageName = isSelected ? type.iconNameFill : type.iconName + return MenuFunction.standardMenuFunction( + text: type.label, + imageName: imageName, + destructiveActionPrompt: nil, + enabled: !isSelected + ) { + postSortType = type + } + } + } + + func genTopSortMenuFunctions() -> [MenuFunction] { + PostSortType.availableTopTypes.map { type in + let isSelected = postSortType == type + return MenuFunction.standardMenuFunction( + text: type.label, + imageName: isSelected ? Icons.timeSortFill : Icons.timeSort, + destructiveActionPrompt: nil, + enabled: !isSelected + ) { + postSortType = type + } + } + } + + func genEllipsisMenuFunctions() -> [MenuFunction] { + var ret: [MenuFunction] = .init() + + let blurNsfwText = shouldBlurNsfw ? "Unblur NSFW" : "Blur NSFW" + ret.append(MenuFunction.standardMenuFunction( + text: blurNsfwText, + imageName: Icons.blurNsfw, + destructiveActionPrompt: nil, + enabled: true + ) { + shouldBlurNsfw.toggle() + }) + + let showReadPostsText = showReadPosts ? "Hide Read" : "Show Read" + ret.append(MenuFunction.standardMenuFunction( + text: showReadPostsText, + imageName: "book", + destructiveActionPrompt: nil, + enabled: true + ) { + showReadPosts.toggle() + }) + + return ret + } + + func genPostSizeSwitchingFunctions() -> [MenuFunction] { + PostSize.allCases.map { size in + let (imageName, enabled) = size != postSize + ? (size.iconName, true) + : (size.iconNameFill, false) + + return MenuFunction.standardMenuFunction( + text: size.label, + imageName: imageName, + destructiveActionPrompt: nil, + enabled: enabled, + callback: { postSize = size } + ) + } + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 2e5cb85ee..60793452b 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -11,6 +11,8 @@ import SwiftUI struct NewPostFeedView: View { @AppStorage("shouldShowPostCreator") var shouldShowPostCreator: Bool = true @AppStorage("showReadPosts") var showReadPosts: Bool = true + @AppStorage("shouldBlurNsfw") var shouldBlurNsfw: Bool = true + @AppStorage("postSize") var postSize: PostSize = .large @ObservedObject var postTracker: StandardPostTracker @Binding var postSortType: PostSortType @@ -19,8 +21,37 @@ struct NewPostFeedView: View { @State var errorDetails: ErrorDetails? var body: some View { + content + .onChange(of: showReadPosts) { newValue in + if newValue { + Task { + await postTracker.removeFilter(.read) + } + } else { + Task { + await postTracker.applyFilter(.read) + } + } + } + .toolbar { + ToolbarItemGroup(placement: .secondaryAction) { + ForEach(genEllipsisMenuFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) + } + Menu { + ForEach(genPostSizeSwitchingFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) + } + } label: { + Label("Post Size", systemImage: Icons.postSizeSetting) + } + } + } + } + + var content: some View { LazyVStack(spacing: 0) { - if postTracker.items.isEmpty { // && !(postTracker.loadingState == .loading) { + if postTracker.items.isEmpty { noPostsView() } else { ForEach(postTracker.items, id: \.uid) { feedPost(for: $0) } diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift index e0adb3270..3e17a123b 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift @@ -22,13 +22,14 @@ struct AggregateFeedView: View { // need to grab some stuff from app storage to initialize post tracker with @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false + @AppStorage("showReadPosts") var showReadPosts = true // TODO: ERIC handle sort type self._postTracker = .init(wrappedValue: .init( internetSpeed: internetSpeed, sortType: .hot, - unreadOnly: false, + showReadPosts: showReadPosts, feedType: feedType )) } From 3f30a80646ee4d54b8e01c0d2d72d45d0a6272bd Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 19 Jan 2024 17:29:04 -0500 Subject: [PATCH 20/69] filters mostly operational --- .../Trackers/Feeds/StandardPostTracker.swift | 16 ++++++-- .../Trackers/Generics/StandardTracker.swift | 39 +++++++++++++------ .../Trackers/Inbox/MentionTracker.swift | 4 +- .../Trackers/Inbox/MessageTracker.swift | 4 +- Mlem/Models/Trackers/Inbox/ReplyTracker.swift | 4 +- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 0913e641e..eeaef391a 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -81,7 +81,7 @@ class StandardPostTracker: StandardTracker { // MARK: StandardTracker Loading Methods - override func fetchPage(page: Int) async throws -> (items: [PostModel], cursor: String?) { + override func fetchPage(page: Int) async throws -> FetchResponse { // TODO: ERIC migrate repository to use "items" let (items, cursor) = try await postRepository.loadPage( communityId: nil, @@ -94,10 +94,10 @@ class StandardPostTracker: StandardTracker { let filteredItems = filter(items) preloadImages(filteredItems) - return (filteredItems, cursor) + return .init(items: filteredItems, cursor: cursor, numFiltered: items.count - filteredItems.count) } - override func fetchCursor(cursor: String?) async throws -> (items: [PostModel], cursor: String?) { + override func fetchCursor(cursor: String?) async throws -> FetchResponse { // TODO: ERIC migrate repository to use "items" let (items, cursor) = try await postRepository.loadPage( communityId: nil, @@ -110,7 +110,7 @@ class StandardPostTracker: StandardTracker { let filteredItems = filter(items) preloadImages(filteredItems) - return (filteredItems, cursor) + return .init(items: filteredItems, cursor: cursor, numFiltered: items.count - filteredItems.count) } // MARK: Custom Behavior @@ -123,6 +123,14 @@ class StandardPostTracker: StandardTracker { filters[newFilter] = 0 await setItems(filter(items)) + + if items.isEmpty { + do { + try await refresh(clearBeforeRefresh: false) + } catch { + errorHandler.handle(error) + } + } } func removeFilter(_ filterToRemove: NewPostFilterReason) async { diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index 3846fdadb..cc3145ad1 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -24,6 +24,21 @@ enum LoadAction { case loadCursor(String) } +/// Helper struct bundling the response from a fetchPage or fetchCursor call +struct FetchResponse { + /// Items returned + let items: [Item] + + /// New cursor, if applicable + let cursor: String? + + /// Number of items filtered out + let numFiltered: Int + + /// True if the response has content, false otherwise. It is possible for a filter to remove all fetched items; this avoids that triggering an erroneous end of feed. + var hasContent: Bool { items.count + numFiltered > 0 } +} + class StandardTracker: CoreTracker { @Dependency(\.errorHandler) var errorHandler @@ -114,16 +129,16 @@ class StandardTracker: CoreTracker { /// Fetches the given page of items. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. /// - Parameters: /// - page: page number to fetch - /// - Returns: requested page of items - func fetchPage(page: Int) async throws -> (items: [Item], cursor: String?) { + /// - Returns: tuple of the requested page of items, the cursor returned by the API call (if present), and the number of items that were filtered out. + func fetchPage(page: Int) async throws -> FetchResponse { preconditionFailure("This method must be implemented by the inheriting class") } // Fetches items from the given cursor. This method must be overridden by the instantiating class because different items are loaded differently. Relies on the instantiating class to handle fetch parameters such as unreadOnly and page size. /// - Parameters: /// - cursor: cursor to fetch - /// - Returns: requested list of items - func fetchCursor(cursor: String) async throws -> (items: [Item], cursor: String?) { + /// - Returns: tuple of the requested page of items, the cursor returned by the API call (if present), and the number of items that were filtered out. + func fetchCursor(cursor: String) async throws -> FetchResponse { preconditionFailure("This method must be implemented by the inheriting class") } @@ -192,17 +207,17 @@ class StandardTracker: CoreTracker { var newItems: [Item] = .init() while newItems.count < internetSpeed.pageSize { - let (fetchedItems, newLoadingCursor) = try await fetchPage(page: page + 1) + let fetched = try await fetchPage(page: page + 1) page += 1 - loadingCursor = newLoadingCursor + loadingCursor = fetched.cursor - if fetchedItems.isEmpty { + if !fetched.hasContent { print("[\(Item.self) tracker] fetch returned no items, setting loading state to done") await setLoading(.done) break } - newItems.append(contentsOf: fetchedItems) + newItems.append(contentsOf: fetched.items) } let allowedItems = storeIdsAndDedupe(newItems: newItems) @@ -236,18 +251,18 @@ class StandardTracker: CoreTracker { var newItems: [Item] = .init() while newItems.count < internetSpeed.pageSize { - let (fetchedItems, newLoadingCursor) = try await fetchCursor(cursor: cursor) + let fetched = try await fetchCursor(cursor: cursor) - if fetchedItems.isEmpty || newLoadingCursor == loadingCursor { + if !fetched.hasContent || fetched.cursor == loadingCursor { print("[\(Item.self) tracker] fetch returned no items or EOF cursor, setting loading state to done") await setLoading(.done) break } - loadingCursor = newLoadingCursor + loadingCursor = fetched.cursor page += 1 // not strictly necessary but good for tracking number of loaded pages - newItems.append(contentsOf: fetchedItems) + newItems.append(contentsOf: fetched.items) } let allowedItems = storeIdsAndDedupe(newItems: newItems) diff --git a/Mlem/Models/Trackers/Inbox/MentionTracker.swift b/Mlem/Models/Trackers/Inbox/MentionTracker.swift index f425b16e3..69f2ea102 100644 --- a/Mlem/Models/Trackers/Inbox/MentionTracker.swift +++ b/Mlem/Models/Trackers/Inbox/MentionTracker.swift @@ -18,10 +18,10 @@ class MentionTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> (items: [MentionModel], cursor: String?) { + override func fetchPage(page: Int) async throws -> FetchResponse { // TODO: can this return a cursor? let newItems = try await inboxRepository.loadMentions(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) - return (newItems, nil) + return .init(items: newItems, cursor: nil, numFiltered: 0) } override func toParent(item: MentionModel) -> AnyInboxItem { diff --git a/Mlem/Models/Trackers/Inbox/MessageTracker.swift b/Mlem/Models/Trackers/Inbox/MessageTracker.swift index 08e003abf..f0135396f 100644 --- a/Mlem/Models/Trackers/Inbox/MessageTracker.swift +++ b/Mlem/Models/Trackers/Inbox/MessageTracker.swift @@ -17,10 +17,10 @@ class MessageTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> (items: [MessageModel], cursor: String?) { + override func fetchPage(page: Int) async throws -> FetchResponse { // TODO: can this return a cursor? let newItems = try await inboxRepository.loadMessages(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) - return (newItems, nil) + return .init(items: newItems, cursor: nil, numFiltered: 0) } override func toParent(item: MessageModel) -> AnyInboxItem { diff --git a/Mlem/Models/Trackers/Inbox/ReplyTracker.swift b/Mlem/Models/Trackers/Inbox/ReplyTracker.swift index 101bb1c03..eb5fb11fc 100644 --- a/Mlem/Models/Trackers/Inbox/ReplyTracker.swift +++ b/Mlem/Models/Trackers/Inbox/ReplyTracker.swift @@ -18,10 +18,10 @@ class ReplyTracker: ChildTracker { super.init(internetSpeed: internetSpeed, sortType: sortType) } - override func fetchPage(page: Int) async throws -> (items: [ReplyModel], cursor: String?) { + override func fetchPage(page: Int) async throws -> FetchResponse { // TODO: can this return a cursor? let newItems = try await inboxRepository.loadReplies(page: page, limit: internetSpeed.pageSize, unreadOnly: unreadOnly) - return (newItems, nil) + return .init(items: newItems, cursor: nil, numFiltered: 0) } override func toParent(item: ReplyModel) -> AnyInboxItem { From 452bb1db3960849b78a09ee13bc639a02b4c6f14 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 19 Jan 2024 23:28:22 -0500 Subject: [PATCH 21/69] got the basics of nav working --- Mlem.xcodeproj/project.pbxproj | 12 ++- Mlem/API/APIClient/APIClient+Post.swift | 27 +++++++ Mlem/API/Requests/Post/GetPosts.swift | 50 +++++++++++++ Mlem/Enums/NEW FeedType.swift | 58 ++++++++++++++- .../View+HandleLemmyLinks.swift | 5 ++ .../Navigation Contexts/Post Link.swift | 17 +++++ .../Trackers/Feeds/StandardPostTracker.swift | 44 +++++++---- Mlem/Models/Trackers/Post Tracker.swift | 8 +- Mlem/Navigation/Routes/AppRoutes.swift | 6 +- Mlem/Repositories/PostRepository.swift | 27 +++++++ Mlem/Views/Tabs/Feeds/PostFeedView.swift | 1 - ...FeedView.swift => AggregateFeedView.swift} | 2 +- .../Components/NEW PostFeedView.swift | 17 +++-- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 41 ++++++---- .../Tabs/NEW Feeds/NEW CommunityView.swift | 74 +++++++++++++++++++ 15 files changed, 339 insertions(+), 50 deletions(-) rename Mlem/Views/Tabs/NEW Feeds/{NEW FeedView.swift => AggregateFeedView.swift} (98%) create mode 100644 Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 314c2b38c..76e9bd882 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -397,7 +397,7 @@ CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */; }; CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */; }; - CD4BAD432B507F2B00A1E726 /* NEW FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */; }; + CD4BAD432B507F2B00A1E726 /* AggregateFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; @@ -504,6 +504,7 @@ CDEBC32C2A9A582500518D9D /* Votes Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32B2A9A582500518D9D /* Votes Model.swift */; }; CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32D2A9A583900518D9D /* Post Tracker.swift */; }; CDEBC3392A9ADE6C00518D9D /* APIClient+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */; }; + CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */; }; CDF1EF162A6C3BC2003594B6 /* End Of Feed View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */; }; CDF1EF182A6C40C9003594B6 /* Menu Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF172A6C40C9003594B6 /* Menu Button.swift */; }; CDF8425C2A49E4C000723DA0 /* APIPersonMentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */; }; @@ -945,7 +946,7 @@ CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRowView.swift; sourceTree = ""; }; CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedType.swift"; sourceTree = ""; }; - CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedView.swift"; sourceTree = ""; }; + CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregateFeedView.swift; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; @@ -1052,6 +1053,7 @@ CDEBC32B2A9A582500518D9D /* Votes Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Votes Model.swift"; sourceTree = ""; }; CDEBC32D2A9A583900518D9D /* Post Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Tracker.swift"; sourceTree = ""; }; CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIClient+Post.swift"; sourceTree = ""; }; + CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityView.swift"; sourceTree = ""; }; CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "End Of Feed View.swift"; sourceTree = ""; }; CDF1EF172A6C40C9003594B6 /* Menu Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Button.swift"; sourceTree = ""; }; CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPersonMentionView.swift; sourceTree = ""; }; @@ -2489,7 +2491,8 @@ children = ( CD4BAD392B4C6C2500A1E726 /* Components */, CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, - CD4BAD422B507F2B00A1E726 /* NEW FeedView.swift */, + CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */, + CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */, ); path = "NEW Feeds"; sourceTree = ""; @@ -3178,7 +3181,7 @@ CDDB08782A5DF1330075BFEE /* CommentSettingsView.swift in Sources */, 6386E02C2A03D1EC006B3C1D /* App State.swift in Sources */, 504106CD2A744D7F000AAEF8 /* CommentRepository+Dependency.swift in Sources */, - CD4BAD432B507F2B00A1E726 /* NEW FeedView.swift in Sources */, + CD4BAD432B507F2B00A1E726 /* AggregateFeedView.swift in Sources */, 6372186F2A3A2AAD008C4816 /* SearchRequest.swift in Sources */, 03EC92952AC064AE007BBE7E /* SearchHomeView.swift in Sources */, CD46C1F62B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */, @@ -3449,6 +3452,7 @@ 039C8DB92B35A81C0096BAAF /* AccountIconStack.swift in Sources */, CDCBD7262A8D69A200387A2C /* Instance Picker View.swift in Sources */, 03C905CE2B3C8DC400B9082F /* UserView+Logic.swift in Sources */, + CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */, 6372185B2A3A2AAD008C4816 /* APICommunityView.swift in Sources */, CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */, 030E86442AC6F6D5000283A6 /* SearchBar+NavigationView.swift in Sources */, diff --git a/Mlem/API/APIClient/APIClient+Post.swift b/Mlem/API/APIClient/APIClient+Post.swift index aef5c95c2..c3d2ae7a9 100644 --- a/Mlem/API/APIClient/APIClient+Post.swift +++ b/Mlem/API/APIClient/APIClient+Post.swift @@ -8,6 +8,7 @@ import Foundation extension APIClient { + // TODO: ERIC delete this // swiftlint:disable function_parameter_count func loadPosts( communityId: Int?, @@ -18,6 +19,32 @@ extension APIClient { limit: Int?, savedOnly: Bool?, communityName: String? + ) async throws -> GetPostsResponse { + let request = try OldGetPostsRequest( + session: session, + communityId: communityId, + page: page, + cursor: cursor, + sort: sort, + type: type, + limit: limit, + savedOnly: savedOnly, + communityName: communityName + ) + + return try await perform(request: request) + } + + // swiftlint:disable function_parameter_count + func loadPosts( + communityId: Int?, + page: Int, + cursor: String?, + sort: PostSortType?, + type: NewFeedType, + limit: Int?, + savedOnly: Bool?, + communityName: String? ) async throws -> GetPostsResponse { let request = try GetPostsRequest( session: session, diff --git a/Mlem/API/Requests/Post/GetPosts.swift b/Mlem/API/Requests/Post/GetPosts.swift index a5d980b0e..5c915797c 100644 --- a/Mlem/API/Requests/Post/GetPosts.swift +++ b/Mlem/API/Requests/Post/GetPosts.swift @@ -15,6 +15,56 @@ struct GetPostsRequest: APIGetRequest { let path = "post/list" let queryItems: [URLQueryItem] + init( + session: APISession, + communityId: Int?, + page: Int, + cursor: String?, + sort: PostSortType?, + type: NewFeedType, + limit: Int? = nil, + savedOnly: Bool? = nil, + communityName: String? = nil + // TODO: 0.19 support add liked_only and disliked_only fields + ) throws { + self.instanceURL = try session.instanceUrl + var queryItems: [URLQueryItem] = [ + .init(name: "type_", value: type.typeString), + .init(name: "sort", value: sort.map(\.rawValue)), + .init(name: "community_id", value: communityId.map(String.init)), + .init(name: "community_name", value: communityName), + .init(name: "limit", value: limit.map(String.init)), + .init(name: "saved_only", value: savedOnly.map(String.init)) + ] + + let paginationParameter: URLQueryItem + if let cursor { + paginationParameter = .init(name: "page_cursor", value: cursor) + } else { + paginationParameter = .init(name: "page", value: "\(page)") + } + + queryItems.append(paginationParameter) + + if let token = try? session.token { + queryItems.append( + .init(name: "auth", value: token) + ) + } + + self.queryItems = queryItems + } +} + +// TODO: ERIC delete this +// lemmy_api_common::post::GetPosts +struct OldGetPostsRequest: APIGetRequest { + typealias Response = GetPostsResponse + + let instanceURL: URL + let path = "post/list" + let queryItems: [URLQueryItem] + init( session: APISession, communityId: Int?, diff --git a/Mlem/Enums/NEW FeedType.swift b/Mlem/Enums/NEW FeedType.swift index 7ed88589e..48d343904 100644 --- a/Mlem/Enums/NEW FeedType.swift +++ b/Mlem/Enums/NEW FeedType.swift @@ -8,11 +8,29 @@ import Foundation import SwiftUI -enum NewFeedType: String, CaseIterable { +enum NewFeedType { case all, local, subscribed, saved + case community(CommunityModel) var label: String { - rawValue.capitalized + switch self { + case .all: "All" + case .local: "Local" + case .subscribed: "Subscribed" + case .saved: "Saved" + case let .community(communityModel): communityModel.name + } + } + + /// String to pass into the API call + var typeString: String { + switch self { + case .all: "All" + case .local: "Local" + case .subscribed: "Subscribed" + case .saved: "Saved" // TODO: change this? + case .community: "Subscribed" + } } static func fromShortcut(shortcut: String?) -> NewFeedType? { @@ -39,13 +57,40 @@ enum NewFeedType: String, CaseIterable { case .subscribed: return .subscribed case .saved: + assertionFailure("Incompatible feed type!") + return .all + default: + assertionFailure("Incompatible feed type!") return .all } } + + var communityId: Int? { + switch self { + case let .community(communityModel): communityModel.communityId + default: nil + } + } } -extension NewFeedType: Identifiable { - var id: Self { self } +extension NewFeedType: Hashable, Identifiable { + func hash(into hasher: inout Hasher) { + switch self { + case .all: + hasher.combine("all") + case .local: + hasher.combine("local") + case .subscribed: + hasher.combine("subscribed") + case .saved: + hasher.combine("saved") + case let .community(communityModel): + hasher.combine("community") + hasher.combine(communityModel.communityId) + } + } + + var id: Int { hashValue } } extension NewFeedType: AssociatedIcon { @@ -55,6 +100,7 @@ extension NewFeedType: AssociatedIcon { case .local: Icons.localFeed case .subscribed: Icons.subscribedFeed case .saved: Icons.savedFeed + case .community: Icons.community } } @@ -64,6 +110,7 @@ extension NewFeedType: AssociatedIcon { case .local: Icons.localFeedFill case .subscribed: Icons.subscribedFeedFill case .saved: Icons.savedFeedFill + case .community: Icons.communityFill } } @@ -73,6 +120,7 @@ extension NewFeedType: AssociatedIcon { case .local: Icons.localFeedCircle case .subscribed: Icons.subscribedFeedCircle case .saved: Icons.savedFeedCircle + case .community: Icons.community } } @@ -83,6 +131,7 @@ extension NewFeedType: AssociatedIcon { case .local: "house" case .subscribed: "newspaper" case .saved: Icons.save + case .community: Icons.community } } } @@ -94,6 +143,7 @@ extension NewFeedType: AssociatedColor { case .local: .red case .subscribed: .red case .saved: .green + case .community: .blue } } } diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index 39b2217be..b371402ef 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -69,6 +69,11 @@ struct HandleLemmyLinksDisplay: ViewModifier { .environmentObject(appState) .environmentObject(quickLookState) .environmentObject(layoutWidgetTracker) + case let .newPostLinkWithContext(post): + ExpandedPost(post: post.post, community: post.community, scrollTarget: post.scrollTarget) + .environmentObject(appState) + .environmentObject(quickLookState) + .environmentObject(layoutWidgetTracker) case let .lazyLoadPostLinkWithContext(post): LazyLoadExpandedPost(post: post.post, scrollTarget: post.scrollTarget) .environmentObject(quickLookState) diff --git a/Mlem/Models/Navigation Contexts/Post Link.swift b/Mlem/Models/Navigation Contexts/Post Link.swift index 50494b866..f62287af3 100644 --- a/Mlem/Models/Navigation Contexts/Post Link.swift +++ b/Mlem/Models/Navigation Contexts/Post Link.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI +// TODO: ERIC delete this struct PostLinkWithContext: Equatable, Identifiable, Hashable { static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id @@ -24,3 +25,19 @@ struct PostLinkWithContext: Equatable, Identifiable, Hashable { let postTracker: PostTracker var scrollTarget: Int? } + +struct NewPostLinkWithContext: Equatable, Identifiable, Hashable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + var id: Int { post.postId } + + let post: PostModel + var community: CommunityModel? + var scrollTarget: Int? +} diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index eeaef391a..4f8012ae6 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -42,7 +42,10 @@ enum NewPostFilterReason: Hashable { /// Post tracker for use with single feeds. Supports all post sorting types, but is not suitable for multi-feed use. class StandardPostTracker: StandardTracker { @Dependency(\.postRepository) var postRepository + @Dependency(\.personRepository) var personRepository @Dependency(\.persistenceRepository) var persistenceRepository + @Dependency(\.siteInformation) var siteInformation + @Dependency(\.apiClient) var apiClient // TODO: ERIC keyword filters could be more elegant var filteredKeywords: [String] @@ -67,7 +70,6 @@ class StandardPostTracker: StandardTracker { self.filteredKeywords = persistenceRepository.loadFilteredKeywords() self.filters = [.keyword: 0] if !showReadPosts { - print("hiding read posts") filters[.read] = 0 } @@ -82,15 +84,7 @@ class StandardPostTracker: StandardTracker { // MARK: StandardTracker Loading Methods override func fetchPage(page: Int) async throws -> FetchResponse { - // TODO: ERIC migrate repository to use "items" - let (items, cursor) = try await postRepository.loadPage( - communityId: nil, - page: page, - cursor: nil, - sort: postSortType, - type: feedType.toLegacyFeedType, - limit: internetSpeed.pageSize - ) + let (items, cursor) = try await loadPageHelper(page: page) let filteredItems = filter(items) preloadImages(filteredItems) @@ -98,13 +92,12 @@ class StandardPostTracker: StandardTracker { } override func fetchCursor(cursor: String?) async throws -> FetchResponse { - // TODO: ERIC migrate repository to use "items" let (items, cursor) = try await postRepository.loadPage( - communityId: nil, + communityId: feedType.communityId, page: page, cursor: cursor, sort: postSortType, - type: feedType.toLegacyFeedType, + type: feedType, limit: internetSpeed.pageSize ) @@ -113,6 +106,31 @@ class StandardPostTracker: StandardTracker { return .init(items: filteredItems, cursor: cursor, numFiltered: items.count - filteredItems.count) } + func loadPageHelper(page: Int) async throws -> (items: [PostModel], cursor: String?) { + if feedType == .saved { + guard let userId = siteInformation.myUserInfo?.localUserView.person.id else { + assertionFailure("Called loadPageHelper with no valid user!") + return (items: .init(), cursor: nil) + } + + let savedContentData = try await personRepository.loadUserDetails( + for: userId, + limit: internetSpeed.pageSize, + savedOnly: true + ) + return (items: savedContentData.posts.map { PostModel(from: $0) }, cursor: nil) + } else { + return try await postRepository.loadPage( + communityId: feedType.communityId, + page: page, + cursor: nil, + sort: postSortType, + type: feedType, + limit: internetSpeed.pageSize + ) + } + } + // MARK: Custom Behavior func applyFilter(_ newFilter: NewPostFilterReason) async { diff --git a/Mlem/Models/Trackers/Post Tracker.swift b/Mlem/Models/Trackers/Post Tracker.swift index cc4ec6f91..334c3ab75 100644 --- a/Mlem/Models/Trackers/Post Tracker.swift +++ b/Mlem/Models/Trackers/Post Tracker.swift @@ -88,8 +88,8 @@ class PostTracker: ObservableObject { // TODO: ERIC handle loading state properly func getNextPageFromRepository() async throws -> (posts: [PostModel], cursor: String?) { - switch self.type { - case .feed(let feedType, let postSortType): + switch type { + case let .feed(feedType, postSortType): return try await postRepository.loadPage( communityId: nil, page: page, @@ -98,13 +98,13 @@ class PostTracker: ObservableObject { type: feedType, limit: internetSpeed.pageSize ) - case .community(let community, let postSortType): + case let .community(community, postSortType): return try await postRepository.loadPage( communityId: community.communityId, page: page, cursor: currentCursor, sort: postSortType, - type: .subscribed, + type: FeedType.subscribed, limit: internetSpeed.pageSize ) case nil: diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index b1da264b4..c876f547c 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -27,6 +27,7 @@ enum AppRoute: Routable { case userProfile(UserModel, communityContext: CommunityModel? = nil) case postLinkWithContext(PostLinkWithContext) + case newPostLinkWithContext(NewPostLinkWithContext) case lazyLoadPostLinkWithContext(LazyLoadPostLinkWithContext) // MARK: - Settings @@ -41,9 +42,6 @@ enum AppRoute: Routable { // swiftlint:disable cyclomatic_complexity static func makeRoute(_ value: some Hashable) throws -> AppRoute { switch value { -// case let value as NewFeedType: -// print("Navigating to new feed type value!") -// return .feed(value) case let value as CommunityLinkWithContext: return .communityLinkWithContext(value) case let value as APIPostView: @@ -58,6 +56,8 @@ enum AppRoute: Routable { return .userProfile(value) case let value as PostLinkWithContext: return .postLinkWithContext(value) + case let value as NewPostLinkWithContext: + return .newPostLinkWithContext(value) case let value as LazyLoadPostLinkWithContext: return .lazyLoadPostLinkWithContext(value) case let value as SettingsPage: diff --git a/Mlem/Repositories/PostRepository.swift b/Mlem/Repositories/PostRepository.swift index b0e3165fb..e2a6f809e 100644 --- a/Mlem/Repositories/PostRepository.swift +++ b/Mlem/Repositories/PostRepository.swift @@ -11,6 +11,7 @@ import Foundation class PostRepository { @Dependency(\.apiClient) private var apiClient + // TODO: ERIC delete this // swiftlint:disable function_parameter_count func loadPage( communityId: Int?, @@ -36,6 +37,32 @@ class PostRepository { let posts = response.posts.map { PostModel(from: $0) } return (posts, response.nextPage) } + + // swiftlint:disable function_parameter_count + func loadPage( + communityId: Int?, + page: Int, + cursor: String?, + sort: PostSortType?, + type: NewFeedType, + limit: Int, + savedOnly: Bool? = nil, + communityName: String? = nil + ) async throws -> (items: [PostModel], cursor: String?) { + let response = try await apiClient.loadPosts( + communityId: communityId, + page: page, + cursor: cursor, + sort: sort, + type: type, + limit: limit, + savedOnly: savedOnly, + communityName: communityName + ) + + let items = response.posts.map { PostModel(from: $0) } + return (items, response.nextPage) + } // swiftlint:enable function_parameter_count diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/PostFeedView.swift index c0ff94f84..d0c78f10e 100644 --- a/Mlem/Views/Tabs/Feeds/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/PostFeedView.swift @@ -128,7 +128,6 @@ struct PostFeedView: View { @ViewBuilder private func feedPost(for post: PostModel) -> some View { VStack(spacing: 0) { - // TODO: reenable nav NavigationLink(.postLinkWithContext(.init(post: post, community: community, postTracker: postTracker))) { FeedPost( post: post, diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift rename to Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift index 3e17a123b..23e9c3d4d 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW FeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift @@ -1,5 +1,5 @@ // -// NEW FeedView.swift +// AggregateFeedView.swift // Mlem // // Created by Eric Andrews on 2024-01-11. diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 60793452b..ae0b1bb1f 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -64,15 +64,18 @@ struct NewPostFeedView: View { private func feedPost(for post: PostModel) -> some View { VStack(spacing: 0) { // TODO: reenable nav - FeedPost( - post: post, - community: post.community, - showPostCreator: shouldShowPostCreator, - showCommunity: showCommunity - ) - .onAppear { postTracker.loadIfThreshold(post) } + NavigationLink(.newPostLinkWithContext(.init(post: post, community: nil))) { + FeedPost( + post: post, + community: post.community, + showPostCreator: shouldShowPostCreator, + showCommunity: showCommunity + ) + } + Divider() } + .onAppear { postTracker.loadIfThreshold(post) } .buttonStyle(EmptyButtonStyle()) // Make it so that the link doesn't mess with the styling } diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index c54997e88..0c644e589 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -17,6 +17,9 @@ struct FeedsView: View { @StateObject private var communityListModel: CommunityListModel = .init() + @StateObject private var feedTabNavigation: AnyNavigationPath = .init() + @StateObject private var navigation: Navigation = .init() + var body: some View { content .onAppear { @@ -38,14 +41,16 @@ struct FeedsView: View { List(selection: $selectedFeed) { ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in // These are automagically turned into NavigationLinks - FeedRowView(feedType: feedType) + NavigationLink(value: feedType) { + FeedRowView(feedType: feedType) + } } ForEach(communityListModel.visibleSections) { section in Section(header: communitySectionHeaderView(for: section)) { ForEach(communityListModel.communities(for: section)) { community in // These are not automagically turned into NavigationLinks, so we do it manually - NavigationLink(value: NewFeedType.all) { + NavigationLink(value: NewFeedType.community(.init(from: community, subscribed: true))) { CommunityFeedRowView( community: community, subscribed: communityListModel.isSubscribed(to: community), @@ -58,20 +63,30 @@ struct FeedsView: View { } } } detail: { - switch selectedFeed { - case .all: - AggregateFeedView(feedType: .all) - case .local: - AggregateFeedView(feedType: .local) - case .subscribed: - AggregateFeedView(feedType: .subscribed) - case .saved: - AggregateFeedView(feedType: .saved) - case .none: - Text("Please select a feed") + NavigationStack(path: $feedTabNavigation.path) { + Group { + switch selectedFeed { + case .all: + AggregateFeedView(feedType: .all) + case .local: + AggregateFeedView(feedType: .local) + case .subscribed: + AggregateFeedView(feedType: .subscribed) + case .saved: + AggregateFeedView(feedType: .saved) + case .community(let communityModel): + NewCommunityFeedView(feedType: .community(communityModel)) + case .none: + Text("Please select a feed") + } + } + .handleLemmyViews() } } } +// .handleLemmyLinkResolution( +// navigationPath: .constant(feedTabNavigation) +// ) } private func communitySectionHeaderView(for section: CommunitySection) -> some View { diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift b/Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift new file mode 100644 index 000000000..59f117542 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift @@ -0,0 +1,74 @@ +// +// NEW CommunityView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-19. +// + +import Dependencies +import Foundation +import SwiftUI + +/// View for post feeds aggregating multiple communities (all, local, subscribed, saved) +struct NewCommunityFeedView: View { + @Dependency(\.errorHandler) var errorHandler + + @StateObject var postTracker: StandardPostTracker + + // TODO: sorting + @State var postSortType: PostSortType = .hot + + init(feedType: NewFeedType) { + // need to grab some stuff from app storage to initialize post tracker with + @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast + @AppStorage("upvoteOnSave") var upvoteOnSave = false + @AppStorage("showReadPosts") var showReadPosts = true + + // TODO: ERIC handle sort type + + self._postTracker = .init(wrappedValue: .init( + internetSpeed: internetSpeed, + sortType: .hot, + showReadPosts: showReadPosts, + feedType: feedType + )) + } + + var body: some View { + content + .onAppear { + Task { await postTracker.loadMoreItems() } + } + .refreshable { + await Task { + do { + _ = try await postTracker.refresh(clearBeforeRefresh: false) + } catch { + errorHandler.handle(error) + } + }.value + } + .background { + VStack(spacing: 0) { + Color.systemBackground + Color.secondarySystemBackground + } + } + .fancyTabScrollCompatible() + .toolbar { + ToolbarItem(placement: .principal) { + Text("Community!") + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarColor(visibility: .automatic) + } + + @ViewBuilder + var content: some View { + ScrollView { + NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) + .background(Color.secondarySystemBackground) + } + } +} From fa3d5c6de06d9fed3006588c06b41c98bd44d9a0 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Fri, 19 Jan 2024 23:28:30 -0500 Subject: [PATCH 22/69] got the basics of nav working --- Mlem/Views/Tabs/Feeds/PostFeedView.swift | 14 +++++++------- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/PostFeedView.swift index d0c78f10e..d8dc074ea 100644 --- a/Mlem/Views/Tabs/Feeds/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/PostFeedView.swift @@ -5,8 +5,8 @@ // Created by Sjmarf on 31/12/2023. // -import SwiftUI import Dependencies +import SwiftUI struct PostFeedView: View { @Dependency(\.errorHandler) var errorHandler @@ -76,8 +76,8 @@ struct PostFeedView: View { .onAppear { if postTracker.showLoadingIcon { Task(priority: .userInitiated) { - postTracker.handleError = self.handle - postTracker.filter = self.filter + postTracker.handleError = handle + postTracker.filter = filter await postTracker.initFeed() } } @@ -95,9 +95,9 @@ struct PostFeedView: View { .onChange(of: postSortType) { newValue in Task(priority: .userInitiated) { switch postTracker.type { - case .feed(let feedType, _): + case let .feed(feedType, _): postTracker.type = .feed(feedType, sortedBy: newValue) - case .community(let community, _): + case let .community(community, _): postTracker.type = .community(community, sortedBy: newValue) case nil: break @@ -112,7 +112,7 @@ struct PostFeedView: View { } .onChange(of: showReadPosts) { _ in Task(priority: .userInitiated) { - postTracker.filter = self.filter + postTracker.filter = filter await postTracker.refresh(clearBeforeFetch: true) } } @@ -150,7 +150,7 @@ struct PostFeedView: View { @ViewBuilder private func noPostsView() -> some View { VStack { - if postTracker.showLoadingIcon { // don't show posts until site information loads to avoid jarring redraw + if postTracker.showLoadingIcon { // don't show posts until site information loads to avoid jarring redraw LoadingView(whatIsLoading: .posts) .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index 0c644e589..1a15751e8 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -74,7 +74,7 @@ struct FeedsView: View { AggregateFeedView(feedType: .subscribed) case .saved: AggregateFeedView(feedType: .saved) - case .community(let communityModel): + case let .community(communityModel): NewCommunityFeedView(feedType: .community(communityModel)) case .none: Text("Please select a feed") From 6b2e1ea4d843561fa7528f215109ee1838c6b19b Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 12:03:20 -0500 Subject: [PATCH 23/69] removed post tracker from LargePost --- .../View+HandleLemmyLinks.swift | 4 +-- Mlem/Models/Content/Post Model.swift | 2 +- Mlem/Views/Shared/Posts/Expanded Post.swift | 4 +-- .../Shared/Posts/ExpandedPostLogic.swift | 36 ++++--------------- .../Shared/Posts/Post Sizes/Large Post.swift | 3 +- 5 files changed, 12 insertions(+), 37 deletions(-) diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index b371402ef..4dfd9178e 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -69,8 +69,8 @@ struct HandleLemmyLinksDisplay: ViewModifier { .environmentObject(appState) .environmentObject(quickLookState) .environmentObject(layoutWidgetTracker) - case let .newPostLinkWithContext(post): - ExpandedPost(post: post.post, community: post.community, scrollTarget: post.scrollTarget) + case let .newPostLinkWithContext(postLink): + ExpandedPost(post: postLink.post, community: postLink.community, scrollTarget: postLink.scrollTarget) .environmentObject(appState) .environmentObject(quickLookState) .environmentObject(layoutWidgetTracker) diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index 3b4d8afb6..081de7e6e 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -169,7 +169,7 @@ class PostModel: ContentIdentifiable, ObservableObject { let original: PostModel = .init(from: self) await setSaved(shouldSave) await setRead(true) - if upvoteOnSave, votes.myVote != .upvote { + if shouldSave, upvoteOnSave, votes.myVote != .upvote { await setVotes(votes.applyScoringOperation(operation: .upvote)) } diff --git a/Mlem/Views/Shared/Posts/Expanded Post.swift b/Mlem/Views/Shared/Posts/Expanded Post.swift index a69d752aa..1def3d426 100644 --- a/Mlem/Views/Shared/Posts/Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Expanded Post.swift @@ -55,7 +55,7 @@ struct ExpandedPost: View { @StateObject var commentTracker: CommentTracker = .init() @EnvironmentObject var postTracker: PostTracker - @State var post: PostModel + @StateObject var post: PostModel var community: CommunityModel? @State var commentErrorDetails: ErrorDetails? @@ -82,7 +82,7 @@ struct ExpandedPost: View { ToolbarItemGroup(placement: .navigationBarTrailing) { toolbarMenu } } .task { await loadComments() } - .task { await postTracker.markRead(post: post) } + .task { await post.markRead(true) } .refreshable { await refreshComments() } .onChange(of: commentSortingType) { newSortingType in withAnimation(.easeIn(duration: 0.4)) { diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index c2a11f880..83cbaf9e8 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -11,39 +11,15 @@ extension ExpandedPost { // MARK: Interaction callbacks func upvotePost() async { - // ensure post tracker isn't loading--avoids state faking causing flickering when post tracker doesn't upvote - guard !postTracker.isLoading else { return } - - // fake state - let oldPost = post // save this to pass to postTracker - let operation = post.votes.myVote == .upvote ? ScoringOperation.resetVote : .upvote - post = PostModel(from: post, votes: post.votes.applyScoringOperation(operation: operation)) - - // perform upvote--passing in oldPost so that the state-faked upvote of post doesn't result in the opposite vote being passed in - post = await postTracker.voteOnPost(post: oldPost, inputOp: .upvote) + await post.vote(inputOp: .upvote) } - + func downvotePost() async { - // fake state - let oldPost = post - let operation = post.votes.myVote == .downvote ? ScoringOperation.resetVote : .downvote - post = PostModel(from: post, votes: post.votes.applyScoringOperation(operation: operation)) - - // perform downvote - post = await postTracker.voteOnPost(post: oldPost, inputOp: .downvote) + await post.vote(inputOp: .downvote) } func savePost() async { - // fake state - var stateFakedPost = PostModel(from: post, saved: !post.saved) - if upvoteOnSave, !post.saved, stateFakedPost.votes.myVote != .upvote { - stateFakedPost.votes = stateFakedPost.votes.applyScoringOperation(operation: .upvote) - } - let oldPost = post - post = stateFakedPost - - // perform save - post = await postTracker.toggleSave(post: oldPost) + await post.toggleSave(upvoteOnSave: upvoteOnSave) } func replyToPost() { @@ -212,8 +188,8 @@ extension ExpandedPost { do { // Making this request marks unread comments as read. - post = try await PostModel(from: postRepository.loadPost(postId: post.postId)) - postTracker.update(with: post) + // post = try await PostModel(from: postRepository.loadPost(postId: post.postId)) + // postTracker.update(with: post) let comments = try await commentRepository.comments(for: post.post.id) let sorted = sortComments(comments, by: commentSortingType) diff --git a/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift index f92cf2f63..9f9b824a0 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift @@ -43,7 +43,6 @@ struct LargePost: View { @Dependency(\.errorHandler) var errorHandler // global state - @EnvironmentObject var postTracker: PostTracker @EnvironmentObject var appState: AppState @AppStorage("shouldBlurNsfw") var shouldBlurNsfw: Bool = true @AppStorage("limitImageHeightInFeed") var limitImageHeightInFeed: Bool = true @@ -258,7 +257,7 @@ struct LargePost: View { /// Synchronous void wrapper for apiClient.markPostAsRead to pass into CachedImage as dismiss callback func markPostAsRead() { Task(priority: .userInitiated) { - await postTracker.markRead(post: post) + await post.markRead(true) } } } From e74ac04bc1df7d59f167e3ac781c67899cfcec65 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 12:36:31 -0500 Subject: [PATCH 24/69] implemented deletion --- .../View+HandleLemmyLinks.swift | 5 -- Mlem/Models/Content/Post Model.swift | 26 +++++++-- .../Navigation Contexts/Post Link.swift | 32 +++++------ .../Trackers/Feeds/StandardPostTracker.swift | 5 ++ Mlem/Navigation/Routes/AppRoutes.swift | 6 +- Mlem/Views/Shared/Posts/Feed Post.swift | 56 ++++++++++--------- Mlem/Views/Tabs/Feeds/PostFeedView.swift | 16 +++--- .../Components/NEW PostFeedView.swift | 2 +- Mlem/Views/Tabs/Profile/UserFeedView.swift | 9 +-- Mlem/Views/Tabs/Profile/UserView+Logic.swift | 22 ++++---- Mlem/Views/Tabs/Profile/UserView.swift | 16 ++++-- 11 files changed, 112 insertions(+), 83 deletions(-) diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index 4dfd9178e..39b2217be 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -69,11 +69,6 @@ struct HandleLemmyLinksDisplay: ViewModifier { .environmentObject(appState) .environmentObject(quickLookState) .environmentObject(layoutWidgetTracker) - case let .newPostLinkWithContext(postLink): - ExpandedPost(post: postLink.post, community: postLink.community, scrollTarget: postLink.scrollTarget) - .environmentObject(appState) - .environmentObject(quickLookState) - .environmentObject(layoutWidgetTracker) case let .lazyLoadPostLinkWithContext(post): LazyLoadExpandedPost(post: post.post, scrollTarget: post.scrollTarget) .environmentObject(quickLookState) diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index 081de7e6e..f46c1f7f6 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -24,6 +24,7 @@ class PostModel: ContentIdentifiable, ObservableObject { var unreadCommentCount: Int @Published var saved: Bool @Published var read: Bool + @Published var deleted: Bool var published: Date var updated: Date? var links: [LinkType] @@ -45,6 +46,7 @@ class PostModel: ContentIdentifiable, ObservableObject { self.unreadCommentCount = apiPostView.unreadComments self.saved = apiPostView.saved self.read = apiPostView.read + self.deleted = apiPostView.post.deleted self.published = apiPostView.post.published self.updated = apiPostView.post.updated @@ -74,6 +76,7 @@ class PostModel: ContentIdentifiable, ObservableObject { unreadCommentCount: Int? = nil, saved: Bool? = nil, read: Bool? = nil, + deleted: Bool? = nil, published: Date? = nil, updated: Date? = nil ) { @@ -86,6 +89,7 @@ class PostModel: ContentIdentifiable, ObservableObject { self.unreadCommentCount = unreadCommentCount ?? other.unreadCommentCount self.saved = saved ?? other.saved self.read = read ?? other.read + self.deleted = deleted ?? other.deleted self.published = published ?? other.published self.updated = updated ?? other.updated @@ -124,6 +128,11 @@ class PostModel: ContentIdentifiable, ObservableObject { saved = newSaved } + @MainActor + func setDeleted(_ newDeleted: Bool) { + deleted = newDeleted + } + // MARK: Interaction Methods func vote(inputOp: ScoringOperation) async { @@ -207,10 +216,19 @@ class PostModel: ContentIdentifiable, ObservableObject { } } - // TODO: implement - func delete(updateTrackers: (() async -> Void)?) async { - if let updateTrackers { - await updateTrackers() + func delete() async { + // state fake + let original: PostModel = .init(from: self) + await setDeleted(true) + + // API call + do { + let deletedResponse = try await postRepository.deletePost(postId: postId, shouldDelete: true) + await reinit(from: deletedResponse) + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + await reinit(from: original) } } diff --git a/Mlem/Models/Navigation Contexts/Post Link.swift b/Mlem/Models/Navigation Contexts/Post Link.swift index f62287af3..1eba56d8d 100644 --- a/Mlem/Models/Navigation Contexts/Post Link.swift +++ b/Mlem/Models/Navigation Contexts/Post Link.swift @@ -22,22 +22,22 @@ struct PostLinkWithContext: Equatable, Identifiable, Hashable { let post: PostModel var community: CommunityModel? - let postTracker: PostTracker + let postTracker: StandardPostTracker var scrollTarget: Int? } -struct NewPostLinkWithContext: Equatable, Identifiable, Hashable { - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - var id: Int { post.postId } - - let post: PostModel - var community: CommunityModel? - var scrollTarget: Int? -} +// struct NewPostLinkWithContext: Equatable, Identifiable, Hashable { +// static func == (lhs: Self, rhs: Self) -> Bool { +// lhs.id == rhs.id +// } +// +// func hash(into hasher: inout Hasher) { +// hasher.combine(id) +// } +// +// var id: Int { post.postId } +// +// let post: PostModel +// var community: CommunityModel? +// var scrollTarget: Int? +// } diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 4f8012ae6..f291da952 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -133,6 +133,11 @@ class StandardPostTracker: StandardTracker { // MARK: Custom Behavior + @available(*, deprecated, message: "Compatibility function for UserView. Should be removed and UserView refactored to use new multi-trackers.") + func reset(with newPosts: [PostModel]) async { + await setItems(newPosts) + } + func applyFilter(_ newFilter: NewPostFilterReason) async { guard !filters.keys.contains(newFilter) else { assertionFailure("Cannot apply new filter (already present in filters!)") diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index c876f547c..28d469b08 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -27,7 +27,7 @@ enum AppRoute: Routable { case userProfile(UserModel, communityContext: CommunityModel? = nil) case postLinkWithContext(PostLinkWithContext) - case newPostLinkWithContext(NewPostLinkWithContext) + // case newPostLinkWithContext(NewPostLinkWithContext) case lazyLoadPostLinkWithContext(LazyLoadPostLinkWithContext) // MARK: - Settings @@ -56,8 +56,8 @@ enum AppRoute: Routable { return .userProfile(value) case let value as PostLinkWithContext: return .postLinkWithContext(value) - case let value as NewPostLinkWithContext: - return .newPostLinkWithContext(value) +// case let value as NewPostLinkWithContext: +// return .newPostLinkWithContext(value) case let value as LazyLoadPostLinkWithContext: return .lazyLoadPostLinkWithContext(value) case let value as SettingsPage: diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index df0e43742..dd7c1683c 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -99,32 +99,36 @@ struct FeedPost: View { var showCheck: Bool { post.read && diffWithoutColor && readMarkStyle == .check } var body: some View { - VStack(spacing: 0) { - postItem - .border(width: barThickness, edges: [.leading], color: .secondary) - .background(Color.systemBackground) -// .background(horizontalSizeClass == .regular ? Color.secondarySystemBackground : Color.systemBackground) -// .clipShape(RoundedRectangle(cornerRadius: horizontalSizeClass == .regular ? 16 : 0)) -// .padding(.all, horizontalSizeClass == .regular ? nil : 0) - .destructiveConfirmation( - isPresentingConfirmDestructive: $isPresentingConfirmDestructive, - confirmationMenuFunction: confirmationMenuFunction - ) - .addSwipeyActions( - leading: [ - enableSwipeActions ? upvoteSwipeAction : nil, - enableSwipeActions ? downvoteSwipeAction : nil - ], - trailing: [ - enableSwipeActions ? saveSwipeAction : nil, - enableSwipeActions ? replySwipeAction : nil - ] - ) - .contextMenu { - ForEach(genMenuFunctions()) { item in - MenuButton(menuFunction: item, confirmDestructive: confirmDestructive) + if post.post.deleted { + EmptyView() + } else { + VStack(spacing: 0) { + postItem + .border(width: barThickness, edges: [.leading], color: .secondary) + .background(Color.systemBackground) + // .background(horizontalSizeClass == .regular ? Color.secondarySystemBackground : Color.systemBackground) + // .clipShape(RoundedRectangle(cornerRadius: horizontalSizeClass == .regular ? 16 : 0)) + // .padding(.all, horizontalSizeClass == .regular ? nil : 0) + .destructiveConfirmation( + isPresentingConfirmDestructive: $isPresentingConfirmDestructive, + confirmationMenuFunction: confirmationMenuFunction + ) + .addSwipeyActions( + leading: [ + enableSwipeActions ? upvoteSwipeAction : nil, + enableSwipeActions ? downvoteSwipeAction : nil + ], + trailing: [ + enableSwipeActions ? saveSwipeAction : nil, + enableSwipeActions ? replySwipeAction : nil + ] + ) + .contextMenu { + ForEach(genMenuFunctions()) { item in + MenuButton(menuFunction: item, confirmDestructive: confirmDestructive) + } } - } + } } } @@ -229,7 +233,7 @@ struct FeedPost: View { } func deletePost() async { - await postTracker.delete(post: post) + await post.delete() } func blockUser() async { diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/PostFeedView.swift index d8dc074ea..d8219dc19 100644 --- a/Mlem/Views/Tabs/Feeds/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/PostFeedView.swift @@ -128,14 +128,14 @@ struct PostFeedView: View { @ViewBuilder private func feedPost(for post: PostModel) -> some View { VStack(spacing: 0) { - NavigationLink(.postLinkWithContext(.init(post: post, community: community, postTracker: postTracker))) { - FeedPost( - post: post, - community: community, - showPostCreator: shouldShowPostCreator, - showCommunity: community == nil - ) - } + // NavigationLink(.postLinkWithContext(.init(post: post, community: community, postTracker: postTracker))) { + FeedPost( + post: post, + community: community, + showPostCreator: shouldShowPostCreator, + showCommunity: community == nil + ) + // } Divider() } .buttonStyle(EmptyButtonStyle()) // Make it so that the link doesn't mess with the styling diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index ae0b1bb1f..7be252c32 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -64,7 +64,7 @@ struct NewPostFeedView: View { private func feedPost(for post: PostModel) -> some View { VStack(spacing: 0) { // TODO: reenable nav - NavigationLink(.newPostLinkWithContext(.init(post: post, community: nil))) { + NavigationLink(.postLinkWithContext(.init(post: post, community: nil, postTracker: postTracker))) { FeedPost( post: post, community: post.community, diff --git a/Mlem/Views/Tabs/Profile/UserFeedView.swift b/Mlem/Views/Tabs/Profile/UserFeedView.swift index 1bf7303c3..794c26cb7 100644 --- a/Mlem/Views/Tabs/Profile/UserFeedView.swift +++ b/Mlem/Views/Tabs/Profile/UserFeedView.swift @@ -5,15 +5,17 @@ // Created by Sjmarf on 25/08/2023. // -import SwiftUI import Dependencies +import SwiftUI struct UserFeedView: View { @Dependency(\.siteInformation) var siteInformation @EnvironmentObject var editorTracker: EditorTracker var user: UserModel - @ObservedObject var privatePostTracker: PostTracker + + // TODO: this private post tracker feels super ugly + @ObservedObject var privatePostTracker: StandardPostTracker @ObservedObject var privateCommentTracker: CommentTracker @ObservedObject var communityTracker: ContentTracker @@ -33,11 +35,10 @@ struct UserFeedView: View { } var isOwnProfile: Bool { - return siteInformation.myUserInfo?.localUserView.person.id == user.userId + siteInformation.myUserInfo?.localUserView.person.id == user.userId } var body: some View { - LazyVStack(spacing: 0) { switch selectedTab { case .communities: diff --git a/Mlem/Views/Tabs/Profile/UserView+Logic.swift b/Mlem/Views/Tabs/Profile/UserView+Logic.swift index 21b6adc09..b9ceaa571 100644 --- a/Mlem/Views/Tabs/Profile/UserView+Logic.swift +++ b/Mlem/Views/Tabs/Profile/UserView+Logic.swift @@ -42,8 +42,8 @@ extension UserView { func tryReloadUser() async { do { let authoredContent = try await personRepository.loadUserDetails(for: user.userId, limit: internetSpeed.pageSize) - self.user = UserModel(from: authoredContent) - self.communityTracker.replaceAll(with: user.moderatedCommunities ?? []) + user = UserModel(from: authoredContent) + communityTracker.replaceAll(with: user.moderatedCommunities ?? []) var savedContentData: GetPersonDetailsResponse? if isOwnProfile { @@ -72,18 +72,18 @@ extension UserView { } privateCommentTracker.comments = newComments - privatePostTracker.reset(with: newPosts) + await privatePostTracker.reset(with: newPosts) - self.isLoadingContent = false + isLoadingContent = false } catch { - errorHandler.handle( - .init( - title: "Couldn't load user info", - message: "There was an error while loading user information.\nTry again later.", - underlyingError: error - ) + errorHandler.handle( + .init( + title: "Couldn't load user info", + message: "There was an error while loading user information.\nTry again later.", + underlyingError: error ) - } + ) + } } } diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index 889364f42..dc89a5816 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -28,7 +28,7 @@ struct UserView: View { @State var isPresentingAccountSwitcher: Bool = false - @StateObject var privatePostTracker: PostTracker + @StateObject var privatePostTracker: StandardPostTracker @StateObject var privateCommentTracker: CommentTracker = .init() @StateObject var communityTracker: ContentTracker = .init() @@ -49,12 +49,18 @@ struct UserView: View { @AppStorage("upvoteOnSave") var upvoteOnSave = false self.internetSpeed = internetSpeed - - self._privatePostTracker = StateObject(wrappedValue: .init( - shouldPerformMergeSorting: false, + + self._privatePostTracker = .init(wrappedValue: .init( internetSpeed: internetSpeed, - upvoteOnSave: upvoteOnSave + sortType: .new, + showReadPosts: true, + feedType: .all )) +// self._privatePostTracker = StateObject(wrappedValue: .init( +// shouldPerformMergeSorting: false, +// internetSpeed: internetSpeed, +// upvoteOnSave: upvoteOnSave +// )) self._user = State(wrappedValue: user) self.communityContext = communityContext From ec4e331b1457a25360ae23d5d752e685a39aa000 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 20:49:12 -0500 Subject: [PATCH 25/69] tweaked filtering behavior --- .../View+HandleLemmyLinks.swift | 6 +- .../Trackers/Feeds/StandardPostTracker.swift | 18 +++- Mlem/Views/Shared/Posts/Feed Post.swift | 83 ++++++++++--------- .../Tabs/NEW Feeds/AggregateFeedView.swift | 1 + .../Components/NEW PostFeedView.swift | 2 +- 5 files changed, 61 insertions(+), 49 deletions(-) diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index 39b2217be..bb4f8b324 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -63,9 +63,9 @@ struct HandleLemmyLinksDisplay: ViewModifier { UserView(user: user, communityContext: communityContext) .environmentObject(appState) .environmentObject(quickLookState) - case let .postLinkWithContext(post): - ExpandedPost(post: post.post, community: post.community, scrollTarget: post.scrollTarget) - .environmentObject(post.postTracker) + case let .postLinkWithContext(postLink): + ExpandedPost(post: postLink.post, community: postLink.community, scrollTarget: postLink.scrollTarget) + .environmentObject(postLink.postTracker) .environmentObject(appState) .environmentObject(quickLookState) .environmentObject(layoutWidgetTracker) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index f291da952..423225225 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -138,7 +138,17 @@ class StandardPostTracker: StandardTracker { await setItems(newPosts) } - func applyFilter(_ newFilter: NewPostFilterReason) async { + /// Applies a filter to all items currently in the tracker, but does **NOT** add the filter to the tracker! + /// Use in situations where filtering is handled server-side but should be retroactively applied to the current set of posts (e.g., filtering posts from a blocked user or community) + /// - Parameter filter: filter to apply + func applyFilter(_ filter: NewPostFilterReason) async { + await setItems(items.filter { shouldFilterPost($0, filters: [filter]) == nil }) + } + + /// Adds a filter to the tracker, removing all current posts that do not pass the filter and filtering out all future posts that do not pass the filter. + /// Use in situations where filtering is handled client-side (e.g., filtering read posts or keywords) + /// - Parameter newFilter: NewPostFilterReason describing the filter to apply + func addFilter(_ newFilter: NewPostFilterReason) async { guard !filters.keys.contains(newFilter) else { assertionFailure("Cannot apply new filter (already present in filters!)") return @@ -177,7 +187,7 @@ class StandardPostTracker: StandardTracker { var ret: [PostModel] = .init() for post in posts { - if let filterReason = shouldFilterPost(post) { + if let filterReason = shouldFilterPost(post, filters: Array(filters.keys)) { filters[filterReason] = filters[filterReason, default: 0] + 1 } else { ret.append(post) @@ -189,8 +199,8 @@ class StandardPostTracker: StandardTracker { /// Given a post, determines whether it should be filtered /// - Returns: the first reason according to which the post should be filtered, if applicable, or nil if the post should not be filtered - private func shouldFilterPost(_ postModel: PostModel) -> NewPostFilterReason? { - for filter in filters.keys { + private func shouldFilterPost(_ postModel: PostModel, filters: [NewPostFilterReason]) -> NewPostFilterReason? { + for filter in filters { switch filter { case .read: if postModel.read { return filter } diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index dd7c1683c..f20f642db 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -44,7 +44,7 @@ struct FeedPost: View { @AppStorage("upvoteOnSave") var upvoteOnSave: Bool = false - @EnvironmentObject var postTracker: PostTracker + @EnvironmentObject var postTracker: StandardPostTracker @EnvironmentObject var editorTracker: EditorTracker @EnvironmentObject var appState: AppState @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @@ -57,7 +57,7 @@ struct FeedPost: View { // MARK: Parameters - @ObservedObject var post: PostModel + @ObservedObject var postModel: PostModel let community: CommunityModel? let showPostCreator: Bool let showCommunity: Bool @@ -70,7 +70,7 @@ struct FeedPost: View { showCommunity: Bool = true, enableSwipeActions: Bool = true ) { - self.post = post + self.postModel = post self.community = community self.showPostCreator = showPostCreator self.showCommunity = showCommunity @@ -95,11 +95,12 @@ struct FeedPost: View { // MARK: Computed - var barThickness: CGFloat { !post.read && diffWithoutColor && readMarkStyle == .bar ? CGFloat(readBarThickness) : .zero } - var showCheck: Bool { post.read && diffWithoutColor && readMarkStyle == .check } + var barThickness: CGFloat { !postModel.read && diffWithoutColor && readMarkStyle == .bar ? CGFloat(readBarThickness) : .zero } + var showCheck: Bool { postModel.read && diffWithoutColor && readMarkStyle == .check } var body: some View { - if post.post.deleted { + // this allows post deletion to not require tracker updates + if postModel.post.deleted { EmptyView() } else { VStack(spacing: 0) { @@ -152,7 +153,7 @@ struct FeedPost: View { var postItem: some View { if postSize == .compact { CompactPost( - post: post, + post: postModel, showCommunity: showCommunity, menuFunctions: genMenuFunctions() ) @@ -166,7 +167,7 @@ struct FeedPost: View { // } HStack { CommunityLinkView( - community: post.community, + community: postModel.community, serverInstanceLocation: communityServerInstanceLocation ) @@ -180,10 +181,10 @@ struct FeedPost: View { } if postSize == .headline { - HeadlinePost(post: post) + HeadlinePost(post: postModel) } else { LargePost( - post: post, + post: postModel, layoutMode: .constant(.preferredSize) ) } @@ -191,7 +192,7 @@ struct FeedPost: View { // posting user if showPostCreator { UserLinkView( - user: post.creator, + user: postModel.creator, serverInstanceLocation: userServerInstanceLocation, communityContext: community ) @@ -201,19 +202,19 @@ struct FeedPost: View { .padding(.horizontal, AppConstants.postAndCommentSpacing) InteractionBarView( - votes: post.votes, - published: post.published, - updated: post.updated, - commentCount: post.commentCount, - unreadCommentCount: post.unreadCommentCount, - saved: post.saved, + votes: postModel.votes, + published: postModel.published, + updated: postModel.updated, + commentCount: postModel.commentCount, + unreadCommentCount: postModel.unreadCommentCount, + saved: postModel.saved, accessibilityContext: "post", widgets: layoutWidgetTracker.groups.post, upvote: upvotePost, downvote: downvotePost, save: savePost, reply: replyToPost, - shareURL: URL(string: post.post.apId), + shareURL: URL(string: postModel.post.apId), shouldShowScore: shouldShowScoreInPostBar, showDownvotesSeparately: showPostDownvotesSeparately, shouldShowTime: shouldShowTimeInPostBar, @@ -233,22 +234,22 @@ struct FeedPost: View { } func deletePost() async { - await post.delete() + await postModel.delete() } func blockUser() async { // TODO: migrate to personRepository do { - let response = try await apiClient.blockPerson(id: post.creator.userId, shouldBlock: true) + let response = try await apiClient.blockPerson(id: postModel.creator.userId, shouldBlock: true) if response.blocked { - postTracker.removeUserPosts(from: post.creator.userId) + await postTracker.applyFilter(.blockedUser(postModel.creator.userId)) hapticManager.play(haptic: .violentSuccess, priority: .high) - await notifier.add(.success("Blocked \(post.creator.name)")) + await notifier.add(.success("Blocked \(postModel.creator.name)")) } } catch { errorHandler.handle( .init( - message: "Unable to block \(post.creator.name)", + message: "Unable to block \(postModel.creator.name)", style: .toast, underlyingError: error ) @@ -259,15 +260,15 @@ struct FeedPost: View { func blockCommunity() async { // TODO: migrate to communityRepository do { - let response = try await apiClient.blockCommunity(id: post.community.communityId, shouldBlock: true) + let response = try await apiClient.blockCommunity(id: postModel.community.communityId, shouldBlock: true) if response.blocked { - postTracker.removeCommunityPosts(from: post.community.communityId) - await notifier.add(.success("Blocked \(post.community.name)")) + await postTracker.applyFilter(.blockedCommunity(postModel.community.communityId)) + await notifier.add(.success("Blocked \(postModel.community.name)")) } } catch { errorHandler.handle( .init( - message: "Unable to block \(post.community.name)", + message: "Unable to block \(postModel.community.name)", style: .toast, underlyingError: error ) @@ -277,29 +278,29 @@ struct FeedPost: View { func replyToPost() { editorTracker.openEditor(with: ConcreteEditorModel( - post: post, + post: postModel, operation: PostOperation.replyToPost )) } func editPost() { editorTracker.openEditor(with: PostEditorModel( - post: post + post: postModel )) } /// Votes on a post /// - Parameter inputOp: The vote operation to perform func voteOnPost(inputOp: ScoringOperation) async { - await post.vote(inputOp: inputOp) + await postModel.vote(inputOp: inputOp) } func savePost() async { - await post.toggleSave(upvoteOnSave: upvoteOnSave) + await postModel.toggleSave(upvoteOnSave: upvoteOnSave) } func reportPost() { - editorTracker.openEditor(with: ConcreteEditorModel(post: post, operation: PostOperation.reportPost)) + editorTracker.openEditor(with: ConcreteEditorModel(post: postModel, operation: PostOperation.reportPost)) } // swiftlint:disable function_body_length @@ -307,7 +308,7 @@ struct FeedPost: View { var ret: [MenuFunction] = .init() // upvote - let (upvoteText, upvoteImg) = post.votes.myVote == .upvote ? + let (upvoteText, upvoteImg) = postModel.votes.myVote == .upvote ? ("Undo Upvote", Icons.upvoteSquareFill) : ("Upvote", Icons.upvoteSquare) ret.append(MenuFunction.standardMenuFunction( @@ -322,7 +323,7 @@ struct FeedPost: View { }) // downvote - let (downvoteText, downvoteImg) = post.votes.myVote == .downvote ? + let (downvoteText, downvoteImg) = postModel.votes.myVote == .downvote ? ("Undo Downvote", Icons.downvoteSquareFill) : ("Downvote", Icons.downvoteSquare) ret.append(MenuFunction.standardMenuFunction( @@ -337,7 +338,7 @@ struct FeedPost: View { }) // save - let (saveText, saveImg) = post.saved ? ("Unsave", "bookmark.slash") : ("Save", "bookmark") + let (saveText, saveImg) = postModel.saved ? ("Unsave", "bookmark.slash") : ("Save", "bookmark") ret.append(MenuFunction.standardMenuFunction( text: saveText, imageName: saveImg, @@ -359,7 +360,7 @@ struct FeedPost: View { replyToPost() }) - if appState.isCurrentAccountId(post.creator.userId) { + if appState.isCurrentAccountId(postModel.creator.userId) { // edit ret.append(MenuFunction.standardMenuFunction( text: "Edit", @@ -375,7 +376,7 @@ struct FeedPost: View { text: "Delete", imageName: Icons.delete, destructiveActionPrompt: "Are you sure you want to delete this post? This cannot be undone.", - enabled: !post.post.deleted + enabled: !postModel.post.deleted ) { Task(priority: .userInitiated) { await deletePost() @@ -384,7 +385,7 @@ struct FeedPost: View { } // share - if let url = URL(string: post.post.apId) { + if let url = URL(string: postModel.post.apId) { ret.append(MenuFunction.shareMenuFunction(url: url)) } @@ -435,7 +436,7 @@ extension FeedPost { // this may need to wait until we complete https://github.com/mormaer/Mlem/issues/117 var upvoteSwipeAction: SwipeAction { - let (emptySymbolName, fullSymbolName) = post.votes.myVote == .upvote ? + let (emptySymbolName, fullSymbolName) = postModel.votes.myVote == .upvote ? (Icons.resetVoteSquare, Icons.resetVoteSquareFill) : (Icons.upvoteSquare, Icons.upvoteSquareFill) return SwipeAction( @@ -452,7 +453,7 @@ extension FeedPost { var downvoteSwipeAction: SwipeAction? { guard siteInformation.enableDownvotes else { return nil } - let (emptySymbolName, fullSymbolName) = post.votes.myVote == .downvote ? + let (emptySymbolName, fullSymbolName) = postModel.votes.myVote == .downvote ? (Icons.resetVoteSquare, Icons.resetVoteSquareFill) : (Icons.downvoteSquare, Icons.downvoteSquareFill) return SwipeAction( @@ -467,7 +468,7 @@ extension FeedPost { } var saveSwipeAction: SwipeAction { - let (emptySymbolName, fullSymbolName) = post.saved + let (emptySymbolName, fullSymbolName) = postModel.saved ? (Icons.unsave, Icons.unsaveFill) : (Icons.save, Icons.saveFill) return SwipeAction( diff --git a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift index 23e9c3d4d..dff06263d 100644 --- a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift @@ -36,6 +36,7 @@ struct AggregateFeedView: View { var body: some View { content + .environmentObject(postTracker) .onAppear { Task { await postTracker.loadMoreItems() } } diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 7be252c32..0105a468a 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -29,7 +29,7 @@ struct NewPostFeedView: View { } } else { Task { - await postTracker.applyFilter(.read) + await postTracker.addFilter(.read) } } } From d255c6653d5764be5dfd843f069ccc34f7861bee Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 21:09:15 -0500 Subject: [PATCH 26/69] added no posts view back in --- .../Trackers/Feeds/StandardPostTracker.swift | 4 +++ .../Components/NEW NoPostsView.swift | 33 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 423225225..c7870c6c4 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -180,6 +180,10 @@ class StandardPostTracker: StandardTracker { } } + func getFilteredCount(for filter: NewPostFilterReason) -> Int { + filters[filter, default: 0] + } + /// Filters a given list of posts. Updates the counts of filtered posts in `filters` /// - Parameter posts: list of posts to filter /// - Returns: list of posts with filtered posts removed diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift index ecd3b37f1..6e19a9128 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift @@ -8,7 +8,7 @@ import SwiftUI struct NewNoPostsView: View { - @EnvironmentObject var postTracker: PostTracker + @EnvironmentObject var postTracker: StandardPostTracker let loadingState: LoadingState @Binding var postSortType: PostSortType @@ -18,28 +18,27 @@ struct NewNoPostsView: View { VStack { if loadingState != .loading { VStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { -// let unreadItems = postTracker.hiddenItems[.read, default: 0] + let unreadItems = postTracker.getFilteredCount(for: .read) Image(systemName: Icons.noPosts) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35) .padding(.bottom, 12) - // .frame(width: unreadItems == 0 ? 35 : 50) - // .padding(.bottom, unreadItems == 0 ? 8: 12) + .frame(width: unreadItems == 0 ? 35 : 50) + .padding(.bottom, unreadItems == 0 ? 8 : 12) Text(title) -// -// if unreadItems != 0 { -// Text( -// "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." -// ) -// .foregroundStyle(.tertiary) -// .multilineTextAlignment(.center) -// .fixedSize(horizontal: false, vertical: true) -// .padding(.horizontal, 20) -// -// } -// buttons + + if unreadItems != 0 { + Text( + "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." + ) + .foregroundStyle(.tertiary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, 20) + } + buttons } .foregroundStyle(.secondary) } @@ -63,7 +62,7 @@ struct NewNoPostsView: View { Label("Switch to Hot", systemImage: Icons.hotSort) } } - if postTracker.hiddenItems[.read, default: 0] > 0 { + if postTracker.getFilteredCount(for: .read) > 0 { Button { if !showReadPosts { showReadPosts = true From 9bdd880258893a80daf7627d27d9aea41bcae323 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 21:33:57 -0500 Subject: [PATCH 27/69] added sorting --- .../Trackers/Feeds/StandardPostTracker.swift | 11 ++++++ .../Components/NEW PostFeedView.swift | 36 +++++++++++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index c7870c6c4..bbb38352c 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -106,6 +106,7 @@ class StandardPostTracker: StandardTracker { return .init(items: filteredItems, cursor: cursor, numFiltered: items.count - filteredItems.count) } + /// Helper function to make loading saved items and feed items look the same to `fetchPage` func loadPageHelper(page: Int) async throws -> (items: [PostModel], cursor: String?) { if feedType == .saved { guard let userId = siteInformation.myUserInfo?.localUserView.person.id else { @@ -133,6 +134,16 @@ class StandardPostTracker: StandardTracker { // MARK: Custom Behavior + /// Changes the post sort type to the specified value and reloads the feed + func changeSortType(to newSortType: PostSortType) async { + postSortType = newSortType + do { + try await refresh(clearBeforeRefresh: true) + } catch { + errorHandler.handle(error) + } + } + @available(*, deprecated, message: "Compatibility function for UserView. Should be removed and UserView refactored to use new multi-trackers.") func reset(with newPosts: [PostModel]) async { await setItems(newPosts) diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 0105a468a..61d477cc8 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -24,16 +24,18 @@ struct NewPostFeedView: View { content .onChange(of: showReadPosts) { newValue in if newValue { - Task { - await postTracker.removeFilter(.read) - } + Task { await postTracker.removeFilter(.read) } } else { - Task { - await postTracker.addFilter(.read) - } + Task { await postTracker.addFilter(.read) } } } + .onChange(of: postSortType) { newValue in + Task { await postTracker.changeSortType(to: newValue) } + } .toolbar { + if postTracker.feedType != .saved { + ToolbarItem(placement: .primaryAction) { sortMenu } + } ToolbarItemGroup(placement: .secondaryAction) { ForEach(genEllipsisMenuFunctions()) { menuFunction in MenuButton(menuFunction: menuFunction, confirmDestructive: nil) @@ -97,4 +99,26 @@ struct NewPostFeedView: View { } .animation(.easeOut(duration: 0.1), value: postTracker.loadingState) } + + @ViewBuilder + private var sortMenu: some View { + Menu { + ForEach(genOuterSortMenuFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) // no destructive sorts + } + + Menu { + ForEach(genTopSortMenuFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) // no destructive sorts + } + } label: { + Label("Top...", systemImage: Icons.topSort) + } + } label: { + Label( + "Selected sorting by \(postSortType.description)", + systemImage: postSortType.iconName + ) + } + } } From ad86dd8bbd31645ce7704eef46b27e80632634f6 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 20 Jan 2024 22:12:43 -0500 Subject: [PATCH 28/69] reinstated feed headers --- Mlem.xcodeproj/project.pbxproj | 4 + Mlem/Enums/NEW FeedType.swift | 2 + .../Trackers/Feeds/StandardPostTracker.swift | 10 ++ .../NEW Feeds/AggregateFeedView+Logic.swift | 31 ++++ .../Tabs/NEW Feeds/AggregateFeedView.swift | 155 +++++++++++++++++- 5 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 76e9bd882..ab15909de 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -505,6 +505,7 @@ CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32D2A9A583900518D9D /* Post Tracker.swift */; }; CDEBC3392A9ADE6C00518D9D /* APIClient+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */; }; CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */; }; + CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */; }; CDF1EF162A6C3BC2003594B6 /* End Of Feed View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */; }; CDF1EF182A6C40C9003594B6 /* Menu Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF172A6C40C9003594B6 /* Menu Button.swift */; }; CDF8425C2A49E4C000723DA0 /* APIPersonMentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */; }; @@ -1054,6 +1055,7 @@ CDEBC32D2A9A583900518D9D /* Post Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Tracker.swift"; sourceTree = ""; }; CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIClient+Post.swift"; sourceTree = ""; }; CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityView.swift"; sourceTree = ""; }; + CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AggregateFeedView+Logic.swift"; sourceTree = ""; }; CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "End Of Feed View.swift"; sourceTree = ""; }; CDF1EF172A6C40C9003594B6 /* Menu Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Button.swift"; sourceTree = ""; }; CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPersonMentionView.swift; sourceTree = ""; }; @@ -2493,6 +2495,7 @@ CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */, CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */, + CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */, ); path = "NEW Feeds"; sourceTree = ""; @@ -3489,6 +3492,7 @@ 038A16E12A75AA880087987E /* LayoutWidgetModel.swift in Sources */, 63344C4F2A07BD2A001BC616 /* Filters Tracker.swift in Sources */, 03B7AAEF2ABCB9DC00068B23 /* ContentTracker.swift in Sources */, + CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */, 50811B2C2A920443006BA3F2 /* Date+Mock.swift in Sources */, CD391FA02A545F8600E213B5 /* Compact Post.swift in Sources */, 0315E9F72B41CD0C00E3BA88 /* FeedView.swift in Sources */, diff --git a/Mlem/Enums/NEW FeedType.swift b/Mlem/Enums/NEW FeedType.swift index 48d343904..43a3807d3 100644 --- a/Mlem/Enums/NEW FeedType.swift +++ b/Mlem/Enums/NEW FeedType.swift @@ -12,6 +12,8 @@ enum NewFeedType { case all, local, subscribed, saved case community(CommunityModel) + static var allAggregateFeedCases: [NewFeedType] = [.all, .local, .subscribed, .saved] + var label: String { switch self { case .all: "All" diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index bbb38352c..70e54e33d 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -144,6 +144,16 @@ class StandardPostTracker: StandardTracker { } } + @MainActor + func changeFeedType(to newFeedType: NewFeedType) async { + feedType = newFeedType + do { + try await refresh(clearBeforeRefresh: true) + } catch { + errorHandler.handle(error) + } + } + @available(*, deprecated, message: "Compatibility function for UserView. Should be removed and UserView refactored to use new multi-trackers.") func reset(with newPosts: [PostModel]) async { await setItems(newPosts) diff --git a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift new file mode 100644 index 000000000..2468ac501 --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift @@ -0,0 +1,31 @@ +// +// AggregateFeedView+Logic.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-20. +// + +import Foundation + +extension AggregateFeedView { + func genFeedSwitchingFunctions() -> [MenuFunction] { + var ret: [MenuFunction] = .init() + NewFeedType.allAggregateFeedCases.forEach { type in + let (imageName, enabled) = type != postTracker.feedType + ? (type.iconName, true) + : (type.iconNameFill, false) + ret.append(MenuFunction.standardMenuFunction( + text: type.label, + imageName: imageName, + destructiveActionPrompt: nil, + enabled: enabled, + callback: { + Task { + await postTracker.changeFeedType(to: type) + } + } + )) + } + return ret + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift index dff06263d..24b52fd5e 100644 --- a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift @@ -13,27 +13,50 @@ import SwiftUI struct AggregateFeedView: View { @Dependency(\.errorHandler) var errorHandler + @EnvironmentObject var appState: AppState + @StateObject var postTracker: StandardPostTracker - // TODO: sorting - @State var postSortType: PostSortType = .hot + @State var postSortType: PostSortType + + @Namespace var scrollToTop + @State private var scrollToTopAppeared = false + private var scrollToTopId: Int? { + postTracker.items.first?.id + } init(feedType: NewFeedType) { - // need to grab some stuff from app storage to initialize post tracker with + // need to grab some stuff from app storage to initialize with @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false @AppStorage("showReadPosts") var showReadPosts = true + @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - // TODO: ERIC handle sort type - + self._postSortType = .init(wrappedValue: defaultPostSorting) self._postTracker = .init(wrappedValue: .init( internetSpeed: internetSpeed, - sortType: .hot, + sortType: defaultPostSorting, showReadPosts: showReadPosts, feedType: feedType )) } + var subtitle: String { + switch postTracker.feedType { + case .all: + return "Posts from all federated instances" + case .local: + return "Posts from \(appState.currentActiveAccount?.instanceLink.host() ?? "your instance's") communities" + case .subscribed: + return "Posts from all subscribed communities" + case .saved: + return "Your saved posts" + default: + assertionFailure("We shouldn't be here...") + return "" + } + } + var body: some View { content .environmentObject(postTracker) @@ -58,7 +81,9 @@ struct AggregateFeedView: View { .fancyTabScrollCompatible() .toolbar { ToolbarItem(placement: .principal) { - Text("Feed!") + navBarTitle + .opacity(scrollToTopAppeared ? 0 : 1) + .animation(.easeOut(duration: 0.2), value: scrollToTopAppeared) } } .navigationBarTitleDisplayMode(.inline) @@ -68,8 +93,122 @@ struct AggregateFeedView: View { @ViewBuilder var content: some View { ScrollView { - NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) + VStack(spacing: 0) { + VStack(spacing: 0) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + headerView + .padding(.top, 5) + } + + NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) + .background(Color.secondarySystemBackground) + } + } + } + + @ViewBuilder + var headerView: some View { + VStack(spacing: 5) { + Menu { + ForEach(genFeedSwitchingFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) + } + } label: { + HStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { + Image(systemName: postTracker.feedType.iconNameCircle) + .resizable() + .frame(width: 44, height: 44) + .foregroundStyle(postTracker.feedType.color ?? .primary) + .padding(.leading, AppConstants.postAndCommentSpacing) + + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 5) { + Text(postTracker.feedType.label) + .lineLimit(1) + .minimumScaleFactor(0.01) + .fontWeight(.semibold) + Image(systemName: Icons.dropdown) + .foregroundStyle(.secondary) + } + .font(.title2) + + Text(subtitle) + .font(.footnote) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.bottom, 3) + } + .buttonStyle(.plain) + + Divider() + .padding(.bottom, 15) + .frame(maxWidth: .infinity) .background(Color.secondarySystemBackground) } + +// Group { +// VStack(spacing: 5) { +// HStack(alignment: .center, spacing: 10) { +// Image(systemName: postTracker.feedType.iconNameCircle) +// .resizable() +// .frame(width: 44, height: 44) +// .foregroundStyle(postTracker.feedType.color ?? .primary) +// VStack(alignment: .leading, spacing: 0) { +// Menu { +// ForEach(genFeedSwitchingFunctions()) { menuFunction in +// MenuButton(menuFunction: menuFunction, confirmDestructive: nil) +// } +// } label: { +// HStack(spacing: 5) { +// Text(postTracker.feedType.label) +// .lineLimit(1) +// .minimumScaleFactor(0.01) +// .fontWeight(.semibold) +// Image(systemName: Icons.dropdown) +// .foregroundStyle(.secondary) +// } +// .font(.title2) +// } +// .buttonStyle(.plain) +// Text(subtitle) +// .font(.footnote) +// .foregroundStyle(.secondary) +// } +// .frame(height: 44) +// Spacer() +// } +// .padding(.horizontal, AppConstants.postAndCommentSpacing) +// .padding(.bottom, 3) +// } +// Divider() +// .padding(.bottom, 15) +// .frame(maxWidth: .infinity) +// .background(Color.secondarySystemBackground) +// } + } + + @ViewBuilder + var navBarTitle: some View { + Menu { + ForEach(genFeedSwitchingFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) + } + } label: { + HStack(alignment: .center, spacing: 0) { + Text(postTracker.feedType.label) + .font(.headline) + Image(systemName: Icons.dropdown) + .scaleEffect(0.7) + .fontWeight(.semibold) + } + .foregroundColor(.primary) + .accessibilityElement(children: .combine) + .accessibilityHint("Activate to change feeds.") + // this disables the implicit animation on the header view... + .transaction { $0.animation = nil } + } } } From f12fddb46c28d9af301a8429c80b63c104dae861 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sun, 21 Jan 2024 18:38:46 -0500 Subject: [PATCH 29/69] one approach to background color problems --- Mlem.xcodeproj/project.pbxproj | 22 +++- Mlem/Enums/NEW FeedType.swift | 2 +- Mlem/Enums/User/UserViewTab.swift | 2 +- .../Trackers/Feeds/StandardPostTracker.swift | 17 ++- .../Generics/README - Generic Trackers.md | 10 +- .../NEW Feeds/Components/FeedRowView.swift | 6 +- .../Components/NEW PostFeedView+Logic.swift | 28 +++++ .../Components/NEW PostFeedView.swift | 49 ++++++--- .../AggregateFeedView+Logic.swift | 0 .../{ => Feed Types}/AggregateFeedView.swift | 102 ++++++------------ .../{ => Feed Types}/NEW CommunityView.swift | 9 +- .../NEW Feeds/Feed Types/SavedFeedView.swift | 17 +++ Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 5 +- Mlem/Views/Tabs/Profile/UserFeedView.swift | 29 ++--- Mlem/Views/Tabs/Profile/UserView+Logic.swift | 5 +- Mlem/Views/Tabs/Profile/UserView.swift | 5 - 16 files changed, 175 insertions(+), 133 deletions(-) create mode 100644 Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+Logic.swift rename Mlem/Views/Tabs/NEW Feeds/{ => Feed Types}/AggregateFeedView+Logic.swift (100%) rename Mlem/Views/Tabs/NEW Feeds/{ => Feed Types}/AggregateFeedView.swift (63%) rename Mlem/Views/Tabs/NEW Feeds/{ => Feed Types}/NEW CommunityView.swift (86%) create mode 100644 Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index ab15909de..62c0ee75c 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -506,6 +506,8 @@ CDEBC3392A9ADE6C00518D9D /* APIClient+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */; }; CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */; }; CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */; }; + CDEC95162B5D8C05004BA288 /* SavedFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */; }; + CDEC95192B5D950D004BA288 /* NEW PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */; }; CDF1EF162A6C3BC2003594B6 /* End Of Feed View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */; }; CDF1EF182A6C40C9003594B6 /* Menu Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF172A6C40C9003594B6 /* Menu Button.swift */; }; CDF8425C2A49E4C000723DA0 /* APIPersonMentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */; }; @@ -1056,6 +1058,8 @@ CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIClient+Post.swift"; sourceTree = ""; }; CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityView.swift"; sourceTree = ""; }; CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AggregateFeedView+Logic.swift"; sourceTree = ""; }; + CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedFeedView.swift; sourceTree = ""; }; + CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView+Logic.swift"; sourceTree = ""; }; CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "End Of Feed View.swift"; sourceTree = ""; }; CDF1EF172A6C40C9003594B6 /* Menu Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Button.swift"; sourceTree = ""; }; CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPersonMentionView.swift; sourceTree = ""; }; @@ -2491,11 +2495,9 @@ CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */ = { isa = PBXGroup; children = ( + CDEC95172B5D8D06004BA288 /* Feed Types */, CD4BAD392B4C6C2500A1E726 /* Components */, CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, - CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */, - CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */, - CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */, ); path = "NEW Feeds"; sourceTree = ""; @@ -2507,6 +2509,7 @@ CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */, CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */, + CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */, ); path = Components; sourceTree = ""; @@ -2758,6 +2761,17 @@ path = Content; sourceTree = ""; }; + CDEC95172B5D8D06004BA288 /* Feed Types */ = { + isa = PBXGroup; + children = ( + CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */, + CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */, + CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */, + CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */, + ); + path = "Feed Types"; + sourceTree = ""; + }; CDF842582A49D23800723DA0 /* Messages */ = { isa = PBXGroup; children = ( @@ -3204,6 +3218,7 @@ 637218672A3A2AAD008C4816 /* GetPersonDetails.swift in Sources */, B1A26FE12A44AAB200B91A32 /* EnvironmentValues+NavigationPathWithRoutes.swift in Sources */, CDD55D222B2674BD002020C7 /* String+ParseLinks.swift in Sources */, + CDEC95162B5D8C05004BA288 /* SavedFeedView.swift in Sources */, 6332FDC027EFB05F0009A98A /* Settings Item.swift in Sources */, 031A93D62AC847DA0077030C /* UploadConfirmationView.swift in Sources */, CD8C55342A95515C0060B75B /* Onboarding Text.swift in Sources */, @@ -3277,6 +3292,7 @@ CD863FBC2A6B026400A31ED9 /* DocumentView.swift in Sources */, CD8461662A96F9EB0026A627 /* Website Indicator View.swift in Sources */, 038A16E92A7A9C640087987E /* LayoutWidget.swift in Sources */, + CDEC95192B5D950D004BA288 /* NEW PostFeedView+Logic.swift in Sources */, CD9A03C82B389F7000C16276 /* EnvironmentValues+FeedType.swift in Sources */, 50811B2E2A92046D006BA3F2 /* URL+Mock.swift in Sources */, 03C905CC2B3C88F700B9082F /* SearchTab.swift in Sources */, diff --git a/Mlem/Enums/NEW FeedType.swift b/Mlem/Enums/NEW FeedType.swift index 43a3807d3..884d72782 100644 --- a/Mlem/Enums/NEW FeedType.swift +++ b/Mlem/Enums/NEW FeedType.swift @@ -142,7 +142,7 @@ extension NewFeedType: AssociatedColor { var color: Color? { switch self { case .all: .blue - case .local: .red + case .local: .purple case .subscribed: .red case .saved: .green case .community: .blue diff --git a/Mlem/Enums/User/UserViewTab.swift b/Mlem/Enums/User/UserViewTab.swift index b17fb9979..932a77c77 100644 --- a/Mlem/Enums/User/UserViewTab.swift +++ b/Mlem/Enums/User/UserViewTab.swift @@ -8,7 +8,7 @@ import Foundation enum UserViewTab: String, CaseIterable, Identifiable { - case overview, comments, posts, communities, saved + case overview, comments, posts, communities var id: Self { self } diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 70e54e33d..669952f05 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -114,6 +114,10 @@ class StandardPostTracker: StandardTracker { return (items: .init(), cursor: nil) } + if page > 1 { + return (items: .init(), cursor: nil) + } + let savedContentData = try await personRepository.loadUserDetails( for: userId, limit: internetSpeed.pageSize, @@ -135,7 +139,13 @@ class StandardPostTracker: StandardTracker { // MARK: Custom Behavior /// Changes the post sort type to the specified value and reloads the feed - func changeSortType(to newSortType: PostSortType) async { + func changeSortType(to newSortType: PostSortType, forceRefresh: Bool = false) async { + // don't do anything if sort type not changed + guard postSortType != newSortType || forceRefresh else { + print("DEBUG sort type unchanged and forceRefresh false, will not reload feed") + return + } + postSortType = newSortType do { try await refresh(clearBeforeRefresh: true) @@ -146,6 +156,11 @@ class StandardPostTracker: StandardTracker { @MainActor func changeFeedType(to newFeedType: NewFeedType) async { + // don't do anything if feed type not changed + guard feedType != newFeedType else { + return + } + feedType = newFeedType do { try await refresh(clearBeforeRefresh: true) diff --git a/Mlem/Models/Trackers/Generics/README - Generic Trackers.md b/Mlem/Models/Trackers/Generics/README - Generic Trackers.md index b60e61ee8..60f7de79c 100644 --- a/Mlem/Models/Trackers/Generics/README - Generic Trackers.md +++ b/Mlem/Models/Trackers/Generics/README - Generic Trackers.md @@ -6,10 +6,18 @@ This group contains a set of generic classes intended to back feed views. This d The heart of a tracker is very simple: an array of items and a method for loading more. +Note that tracker models must be classes, not structs, as much of the logic is built on the assumption that items will be passed by reference. + ## Tracker Types There are three types of trackers: `StandardTracker`, `ChildTracker`, and `ParentTracker`. `CoreTracker` holds shared logic between these trackers, and should **not** be used! -`StandardTracker` should be used for feeds with a single item type (e.g., the main posts feed). +`StandardTracker` should be used for feeds with a single item type (e.g., the main posts feed). To use it, simply create an inheriting class `ChildTracker` and `ParentTracker` should always be used in conjunction! They handle feeds with mixed item types (e.g., the inbox feed). `ChildTracker` is a modified version of `StandardTracker`, and can safely be used to drive its own feed in addition to the mixed feed (as is done in the inbox). `ParentTracker` offers a similar interface, but functions radically differently: it relies on its `ChildTracker`s to load items! + +To create a multi-tracker, first create a protocol `MyTrackerItem` conforming to `TrackerItem` and an enum `AnyMyTrackerItem` conforming to `MyTrackerItem`. For each child type, create an extension conforming it to `MyTrackerItem` and add a case to `AnyMyTrackerItem` for that type with the associated value of the content type. With that done, create one child tracker for each child type (`class FooTracker: ChildTracker`) and a single parent tracker inheriting from `ParentTracker` (`class MyTracker: ParentTracker some View { VStack { - if postTracker.loadingState == .loading { // don't show posts until site information loads to avoid jarring redraw + // don't show posts until site information loads to avoid jarring redraw + if postTracker.loadingState == .loading || !siteVersionResolved { LoadingView(whatIsLoading: .posts) .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) diff --git a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView+Logic.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/AggregateFeedView+Logic.swift rename to Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView+Logic.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift similarity index 63% rename from Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift rename to Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift index 24b52fd5e..f51b8057e 100644 --- a/Mlem/Views/Tabs/NEW Feeds/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift @@ -60,9 +60,6 @@ struct AggregateFeedView: View { var body: some View { content .environmentObject(postTracker) - .onAppear { - Task { await postTracker.loadMoreItems() } - } .refreshable { await Task { do { @@ -73,10 +70,7 @@ struct AggregateFeedView: View { }.value } .background { - VStack(spacing: 0) { - Color.systemBackground - Color.secondarySystemBackground - } + Color.secondarySystemBackground } .fancyTabScrollCompatible() .toolbar { @@ -92,36 +86,43 @@ struct AggregateFeedView: View { @ViewBuilder var content: some View { - ScrollView { - VStack(spacing: 0) { + VStack(spacing: 0) { + Divider() + .padding(.bottom, -1) + + ScrollView { VStack(spacing: 0) { - ScrollToView(appeared: $scrollToTopAppeared) - .id(scrollToTop) - headerView - .padding(.top, 5) + VStack(spacing: 0) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + headerView + .padding(.top, -1) + } + + NewPostFeedView(postSortType: $postSortType, showCommunity: true) + .environmentObject(postTracker) } - - NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) - .background(Color.secondarySystemBackground) } } } @ViewBuilder var headerView: some View { - VStack(spacing: 5) { - Menu { - ForEach(genFeedSwitchingFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) - } - } label: { + Menu { + ForEach(genFeedSwitchingFunctions()) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: nil) + } + } label: { + VStack(spacing: 0) { + Divider() + HStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { Image(systemName: postTracker.feedType.iconNameCircle) .resizable() .frame(width: 44, height: 44) .foregroundStyle(postTracker.feedType.color ?? .primary) .padding(.leading, AppConstants.postAndCommentSpacing) - + VStack(alignment: .leading, spacing: 0) { HStack(spacing: 5) { Text(postTracker.feedType.label) @@ -132,62 +133,21 @@ struct AggregateFeedView: View { .foregroundStyle(.secondary) } .font(.title2) - + Text(subtitle) .font(.footnote) .foregroundStyle(.secondary) } + .frame(height: 44) .frame(maxWidth: .infinity, alignment: .leading) } - .padding(.bottom, 3) + .padding(.vertical, 5) + + Divider() } - .buttonStyle(.plain) - - Divider() - .padding(.bottom, 15) - .frame(maxWidth: .infinity) - .background(Color.secondarySystemBackground) + .background(Color.systemBackground) } - -// Group { -// VStack(spacing: 5) { -// HStack(alignment: .center, spacing: 10) { -// Image(systemName: postTracker.feedType.iconNameCircle) -// .resizable() -// .frame(width: 44, height: 44) -// .foregroundStyle(postTracker.feedType.color ?? .primary) -// VStack(alignment: .leading, spacing: 0) { -// Menu { -// ForEach(genFeedSwitchingFunctions()) { menuFunction in -// MenuButton(menuFunction: menuFunction, confirmDestructive: nil) -// } -// } label: { -// HStack(spacing: 5) { -// Text(postTracker.feedType.label) -// .lineLimit(1) -// .minimumScaleFactor(0.01) -// .fontWeight(.semibold) -// Image(systemName: Icons.dropdown) -// .foregroundStyle(.secondary) -// } -// .font(.title2) -// } -// .buttonStyle(.plain) -// Text(subtitle) -// .font(.footnote) -// .foregroundStyle(.secondary) -// } -// .frame(height: 44) -// Spacer() -// } -// .padding(.horizontal, AppConstants.postAndCommentSpacing) -// .padding(.bottom, 3) -// } -// Divider() -// .padding(.bottom, 15) -// .frame(maxWidth: .infinity) -// .background(Color.secondarySystemBackground) -// } + .buttonStyle(.plain) } @ViewBuilder diff --git a/Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift similarity index 86% rename from Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift rename to Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift index 59f117542..f13d26fba 100644 --- a/Mlem/Views/Tabs/NEW Feeds/NEW CommunityView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift @@ -23,12 +23,12 @@ struct NewCommunityFeedView: View { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false @AppStorage("showReadPosts") var showReadPosts = true + @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - // TODO: ERIC handle sort type - + self._postSortType = .init(wrappedValue: defaultPostSorting) self._postTracker = .init(wrappedValue: .init( internetSpeed: internetSpeed, - sortType: .hot, + sortType: defaultPostSorting, showReadPosts: showReadPosts, feedType: feedType )) @@ -67,7 +67,8 @@ struct NewCommunityFeedView: View { @ViewBuilder var content: some View { ScrollView { - NewPostFeedView(postTracker: postTracker, postSortType: $postSortType, showCommunity: true) + NewPostFeedView(postSortType: $postSortType, showCommunity: true) + .environmentObject(postTracker) .background(Color.secondarySystemBackground) } } diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift new file mode 100644 index 000000000..de765c9bb --- /dev/null +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift @@ -0,0 +1,17 @@ +// +// SavedFeedView.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-21. +// + +import Foundation +import SwiftUI + +struct SavedFeedView: View { + // TODO: ERIC this should be a multi-feed with saved comments as well + + var body: some View { + AggregateFeedView(feedType: .saved) + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index 1a15751e8..0eaada587 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -73,7 +73,7 @@ struct FeedsView: View { case .subscribed: AggregateFeedView(feedType: .subscribed) case .saved: - AggregateFeedView(feedType: .saved) + SavedFeedView() case let .community(communityModel): NewCommunityFeedView(feedType: .community(communityModel)) case .none: @@ -84,9 +84,6 @@ struct FeedsView: View { } } } -// .handleLemmyLinkResolution( -// navigationPath: .constant(feedTabNavigation) -// ) } private func communitySectionHeaderView(for section: CommunitySection) -> some View { diff --git a/Mlem/Views/Tabs/Profile/UserFeedView.swift b/Mlem/Views/Tabs/Profile/UserFeedView.swift index 794c26cb7..e4df9cb95 100644 --- a/Mlem/Views/Tabs/Profile/UserFeedView.swift +++ b/Mlem/Views/Tabs/Profile/UserFeedView.swift @@ -80,9 +80,7 @@ struct UserFeedView: View { let feed: [FeedItem] switch selectedTab { case .overview: - feed = generateMixedFeed(savedItems: false) - case .saved: - feed = generateMixedFeed(savedItems: true) + feed = generateOverviewFeed() case .comments: feed = generateCommentFeed() case .posts: @@ -146,13 +144,7 @@ struct UserFeedView: View { privateCommentTracker.comments // Matched saved state .filter { - if savedItems { - return $0.commentView.saved - } else { - // If we unfavorited something while - // here we don't want it showing up in our feed - return $0.commentView.creator.id == user.userId - } + $0.commentView.creator.id == user.userId } // Create Feed Items @@ -167,17 +159,10 @@ struct UserFeedView: View { } } - private func generatePostFeed(savedItems: Bool = false) -> [FeedItem] { + private func generatePostFeed() -> [FeedItem] { privatePostTracker.items - // Matched saved state .filter { - if savedItems { - return $0.saved - } else { - // If we unfavorited something while - // here we don't want it showing up in our feed - return $0.creator.userId == user.userId - } + $0.creator.userId == user.userId } // Create Feed Items @@ -192,11 +177,11 @@ struct UserFeedView: View { } } - private func generateMixedFeed(savedItems: Bool) -> [FeedItem] { + private func generateOverviewFeed() -> [FeedItem] { var result: [FeedItem] = [] - result.append(contentsOf: generatePostFeed(savedItems: savedItems)) - result.append(contentsOf: generateCommentFeed(savedItems: savedItems)) + result.append(contentsOf: generatePostFeed()) + result.append(contentsOf: generateCommentFeed()) return result } diff --git a/Mlem/Views/Tabs/Profile/UserView+Logic.swift b/Mlem/Views/Tabs/Profile/UserView+Logic.swift index b9ceaa571..7437bee1f 100644 --- a/Mlem/Views/Tabs/Profile/UserView+Logic.swift +++ b/Mlem/Views/Tabs/Profile/UserView+Logic.swift @@ -12,12 +12,11 @@ extension UserView { var tabs: [UserViewTab] { var tabs: [UserViewTab] = [.overview, .posts, .comments] - if isOwnProfile { - tabs.append(.saved) - } + if !(user.moderatedCommunities?.isEmpty ?? true) { tabs.append(.communities) } + return tabs } diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index dc89a5816..356845a1d 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -56,11 +56,6 @@ struct UserView: View { showReadPosts: true, feedType: .all )) -// self._privatePostTracker = StateObject(wrappedValue: .init( -// shouldPerformMergeSorting: false, -// internetSpeed: internetSpeed, -// upvoteOnSave: upvoteOnSave -// )) self._user = State(wrappedValue: user) self.communityContext = communityContext From 204e44af32b1b140eeb7954fe522aeb409cb537b Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sun, 21 Jan 2024 18:44:17 -0500 Subject: [PATCH 30/69] set feed background to systemBackground --- .../Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift index f51b8057e..d22a8c31e 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift @@ -70,7 +70,7 @@ struct AggregateFeedView: View { }.value } .background { - Color.secondarySystemBackground + Color.systemBackground } .fancyTabScrollCompatible() .toolbar { @@ -87,9 +87,6 @@ struct AggregateFeedView: View { @ViewBuilder var content: some View { VStack(spacing: 0) { - Divider() - .padding(.bottom, -1) - ScrollView { VStack(spacing: 0) { VStack(spacing: 0) { @@ -114,8 +111,6 @@ struct AggregateFeedView: View { } } label: { VStack(spacing: 0) { - Divider() - HStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { Image(systemName: postTracker.feedType.iconNameCircle) .resizable() @@ -145,7 +140,6 @@ struct AggregateFeedView: View { Divider() } - .background(Color.systemBackground) } .buttonStyle(.plain) } From 6e39f0da896017b58913073f0abdfcf8ab6b7afe Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 10:58:59 -0500 Subject: [PATCH 31/69] initial work towards post and comment tracker --- Mlem/Models/Content/CommentModel.swift | 8 ++++++++ Mlem/Models/Trackers/Feeds/UserContentTracker.swift | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 Mlem/Models/Content/CommentModel.swift create mode 100644 Mlem/Models/Trackers/Feeds/UserContentTracker.swift diff --git a/Mlem/Models/Content/CommentModel.swift b/Mlem/Models/Content/CommentModel.swift new file mode 100644 index 000000000..9891810a0 --- /dev/null +++ b/Mlem/Models/Content/CommentModel.swift @@ -0,0 +1,8 @@ +// +// CommentModel.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-22. +// + +import Foundation diff --git a/Mlem/Models/Trackers/Feeds/UserContentTracker.swift b/Mlem/Models/Trackers/Feeds/UserContentTracker.swift new file mode 100644 index 000000000..8b7a4dbcf --- /dev/null +++ b/Mlem/Models/Trackers/Feeds/UserContentTracker.swift @@ -0,0 +1,8 @@ +// +// UserContentTracker.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-22. +// + +import Foundation From c54416d35d5181505b48615b4d0ee39d4c2086f1 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 10:59:16 -0500 Subject: [PATCH 32/69] initial work towards post and comment tracker --- Mlem.xcodeproj/project.pbxproj | 8 + Mlem/Models/Content/CommentModel.swift | 17 ++ .../CommunityModel+MenuFunctions.swift | 8 +- .../Trackers/Feeds/UserContentTracker.swift | 19 ++ .../Feed Types/NEW CommunityView.swift | 268 +++++++++++++++++- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 2 +- 6 files changed, 303 insertions(+), 19 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 62c0ee75c..c76b97242 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -401,6 +401,8 @@ CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; + CD62D12A2B5EC4E900395BD9 /* UserContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */; }; + CD62D12C2B5EC65200395BD9 /* CommentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD62D12B2B5EC65200395BD9 /* CommentModel.swift */; }; CD6483302A38D31C00EE6CA3 /* UpvoteCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD64832F2A38D31C00EE6CA3 /* UpvoteCounterView.swift */; }; CD6483322A38D3A600EE6CA3 /* ScoreCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6483312A38D3A600EE6CA3 /* ScoreCounterView.swift */; }; CD6483362A39F20800EE6CA3 /* Post Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6483352A39F20800EE6CA3 /* Post Type.swift */; }; @@ -953,6 +955,8 @@ CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; + CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContentTracker.swift; sourceTree = ""; }; + CD62D12B2B5EC65200395BD9 /* CommentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentModel.swift; sourceTree = ""; }; CD64832F2A38D31C00EE6CA3 /* UpvoteCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpvoteCounterView.swift; sourceTree = ""; }; CD6483312A38D3A600EE6CA3 /* ScoreCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreCounterView.swift; sourceTree = ""; }; CD6483352A39F20800EE6CA3 /* Post Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Type.swift"; sourceTree = ""; }; @@ -2197,6 +2201,7 @@ isa = PBXGroup; children = ( CD1262792B4759BC007549F9 /* StandardPostTracker.swift */, + CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */, ); path = Feeds; sourceTree = ""; @@ -2753,6 +2758,7 @@ CD4368C22AE2409D00BD8BD1 /* Inbox */, CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */, CDEBC3292A9A580B00518D9D /* Post Model.swift */, + CD62D12B2B5EC65200395BD9 /* CommentModel.swift */, 03FD64FD2AE538C600957AA9 /* Community */, 030D00862AD1BB0D00953B1D /* User */, 03B7AAF02ABE404300068B23 /* ContentModel.swift */, @@ -3312,6 +3318,7 @@ B1CB6E752A4C729D00DA9675 /* Bundle+IconFileName.swift in Sources */, 030D4AE82AA1278400A3393D /* ErrorDetails+Mock.swift in Sources */, CD4368C62AE240BF00BD8BD1 /* MessageModel.swift in Sources */, + CD62D12C2B5EC65200395BD9 /* CommentModel.swift in Sources */, 6363D60427EE20A200E34822 /* Expanded Post.swift in Sources */, 6DE1183C2A4A217400810C7E /* Profile View.swift in Sources */, CD04D5DF2A361585008EF95B /* Empty Button Style.swift in Sources */, @@ -3374,6 +3381,7 @@ CD9A49D72B059303001E18A0 /* ImageSaver.swift in Sources */, 03E90FB12B3703ED00E5A802 /* AccountSortMode.swift in Sources */, CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */, + CD62D12A2B5EC4E900395BD9 /* UserContentTracker.swift in Sources */, 50EC39B22A346DDC00E014C2 /* URLHandler.swift in Sources */, 63F0C7BF2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift in Sources */, 632E8EE827EE63DB007E8D75 /* DownvoteButtonView.swift in Sources */, diff --git a/Mlem/Models/Content/CommentModel.swift b/Mlem/Models/Content/CommentModel.swift index 9891810a0..509c72cdd 100644 --- a/Mlem/Models/Content/CommentModel.swift +++ b/Mlem/Models/Content/CommentModel.swift @@ -6,3 +6,20 @@ // import Foundation + +class CommentModel: ContentIdentifiable, ObservableObject { + let comment: APIComment + let creator: APIPerson + let post: APIPost + let community: APICommunity + let counts: APICommentAggregates + let creatorBannedFromCommunity: Bool + let creatorIsModerator: Bool? // TODO: 0.18 deprecation make this field non-optional + let creatorIsAdmin: Bool? // TODO: 0.18 deprecation make this field non-optional + let subscribed: APISubscribedStatus + let saved: Bool + let creatorBlocked: Bool + var myVote: ScoringOperation? + + init(from apiCommentView: APICommentView) {} +} diff --git a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift index 5d6855d26..80585f106 100644 --- a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift +++ b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift @@ -17,10 +17,10 @@ extension CommunityModel { enabled: true ) { assertionFailure("ERIC RE-IMPLEMENT THIS") -// editorTracker.openEditor(with: PostEditorModel( -// community: self, -// postTracker: postTracker -// )) +// editorTracker.openEditor(with: PostEditorModel( +// community: self, +// postTracker: postTracker +// )) } } diff --git a/Mlem/Models/Trackers/Feeds/UserContentTracker.swift b/Mlem/Models/Trackers/Feeds/UserContentTracker.swift index 8b7a4dbcf..2bbe3916a 100644 --- a/Mlem/Models/Trackers/Feeds/UserContentTracker.swift +++ b/Mlem/Models/Trackers/Feeds/UserContentTracker.swift @@ -6,3 +6,22 @@ // import Foundation + +enum UserContentItem: TrackerItem { + case post(PostModel) + case comment(CommentModel) + + var uid: ContentModelIdentifier { + switch self { + case let .post(postModel): postModel.uid + case let .comment(commentModel): commentModel.uid + } + } + + func sortVal(sortType: TrackerSortType) -> TrackerSortVal { + switch self { + case let .post(postModel): postModel.sortVal(sortType: sortType) + case let .comment(commentModel): commentModel.sortVal(sortType: sortType) + } + } +} diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift index f13d26fba..3ea77d099 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift @@ -9,28 +9,67 @@ import Dependencies import Foundation import SwiftUI -/// View for post feeds aggregating multiple communities (all, local, subscribed, saved) +/// View for a single community struct NewCommunityFeedView: View { + enum Tab: String, Identifiable, CaseIterable { + var id: Self { self } + case posts, about, moderators, statistics + } + + @AppStorage("shouldShowCommunityHeaders") var shouldShowCommunityHeaders: Bool = true + @AppStorage("shouldShowCommunityIcons") var shouldShowCommunityIcons: Bool = true + @Dependency(\.errorHandler) var errorHandler + @Dependency(\.hapticManager) var hapticManager + @Dependency(\.communityRepository) var communityRepository + + @Environment(\.colorScheme) var colorScheme @StateObject var postTracker: StandardPostTracker - // TODO: sorting - @State var postSortType: PostSortType = .hot + @State var postSortType: PostSortType + @State var selectedTab: Tab = .posts + + @State var communityModel: CommunityModel + + // destructive confirmation + @State private var isPresentingConfirmDestructive: Bool = false + @State private var confirmationMenuFunction: StandardMenuFunction? + + func confirmDestructive(destructiveFunction: StandardMenuFunction) { + confirmationMenuFunction = destructiveFunction + isPresentingConfirmDestructive = true + } + + // scroll to top + @Namespace var scrollToTop + @State private var scrollToTopAppeared = false + private var scrollToTopId: Int? { + postTracker.items.first?.id + } - init(feedType: NewFeedType) { + var availableTabs: [Tab] { + var output: [Tab] = [.posts, .moderators, .statistics] + if communityModel.description != nil { + output.insert(.about, at: 1) + } + return output + } + + init(communityModel: CommunityModel) { // need to grab some stuff from app storage to initialize post tracker with @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false @AppStorage("showReadPosts") var showReadPosts = true @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot + self._communityModel = .init(wrappedValue: communityModel) self._postSortType = .init(wrappedValue: defaultPostSorting) self._postTracker = .init(wrappedValue: .init( internetSpeed: internetSpeed, sortType: defaultPostSorting, showReadPosts: showReadPosts, - feedType: feedType + feedType: .community(communityModel) )) } @@ -38,6 +77,16 @@ struct NewCommunityFeedView: View { content .onAppear { Task { await postTracker.loadMoreItems() } + + if communityModel.moderators == nil { + Task(priority: .userInitiated) { + do { + communityModel = try await communityRepository.loadDetails(for: communityModel.communityId) + } catch { + errorHandler.handle(error) + } + } + } } .refreshable { await Task { @@ -48,16 +97,17 @@ struct NewCommunityFeedView: View { } }.value } - .background { - VStack(spacing: 0) { - Color.systemBackground - Color.secondarySystemBackground - } - } + .destructiveConfirmation( + isPresentingConfirmDestructive: $isPresentingConfirmDestructive, + confirmationMenuFunction: confirmationMenuFunction + ) .fancyTabScrollCompatible() .toolbar { ToolbarItem(placement: .principal) { - Text("Community!") + Text(communityModel.name) + .font(.headline) + .opacity(scrollToTopAppeared ? 0 : 1) + .animation(.easeOut(duration: 0.2), value: scrollToTopAppeared) } } .navigationBarTitleDisplayMode(.inline) @@ -67,9 +117,199 @@ struct NewCommunityFeedView: View { @ViewBuilder var content: some View { ScrollView { - NewPostFeedView(postSortType: $postSortType, showCommunity: true) - .environmentObject(postTracker) + VStack(spacing: 0) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + headerView + .padding(.top, 5) + .background(Color.systemBackground) + + switch selectedTab { + case .posts: posts() + case .about: about() + case .moderators: moderators() + case .statistics: statistics() + } + } + } + .background { + VStack(spacing: 0) { + // this awful little hack prevents a line from appearing in gray background tabs + Color.systemBackground + .frame(height: 1) + + if selectedTab == .statistics || selectedTab == .moderators { + Color(uiColor: .systemGroupedBackground) + } + } + } + } + + func posts() -> some View { + NewPostFeedView(postSortType: $postSortType, showCommunity: false) + .environmentObject(postTracker) + } + + func about() -> some View { + VStack(spacing: AppConstants.postAndCommentSpacing) { + if shouldShowCommunityHeaders, let banner = communityModel.banner { + CachedImage(url: banner, cornerRadius: AppConstants.largeItemCornerRadius) + } + MarkdownView(text: communityModel.description ?? "", isNsfw: false) + } + .padding(AppConstants.postAndCommentSpacing) + } + + @ViewBuilder + func moderators() -> some View { + if let moderators = communityModel.moderators { + Divider() + .padding(.top, 15) .background(Color.secondarySystemBackground) + ForEach(moderators, id: \.id) { user in + UserResultView(user, communityContext: communityModel) + Divider() + } + Color.secondarySystemBackground + .frame(height: 100) + } + } + + func statistics() -> some View { + VStack(spacing: 0) { + CommunityStatsView(community: communityModel) + .padding(.top, AppConstants.postAndCommentSpacing) + .background(Color(uiColor: .systemGroupedBackground)) + + Color(uiColor: .systemGroupedBackground) + .frame(maxHeight: .infinity) + } + .frame(maxHeight: .infinity) + } + + // MARK: Header + + @ViewBuilder + var headerView: some View { + Group { + VStack(spacing: 5) { + HStack(alignment: .center, spacing: 10) { + if shouldShowCommunityIcons { + AvatarView(community: communityModel, avatarSize: 44, iconResolution: .unrestricted) + } + Button(action: communityModel.copyFullyQualifiedName) { + VStack(alignment: .leading, spacing: 0) { + Text(communityModel.displayName) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.01) + if let fullyQualifiedName = communityModel.fullyQualifiedName { + Text(fullyQualifiedName) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + .frame(height: 44) + } + .buttonStyle(.plain) + Spacer() + subscribeButton + } + .padding(.horizontal, AppConstants.postAndCommentSpacing) + .padding(.bottom, 3) + Divider() + BubblePicker(availableTabs, selected: $selectedTab) { + Text($0.rawValue.capitalized) + } + } + Divider() + } + } + + var subscribeButtonForegroundColor: Color { + if communityModel.favorited { + return .blue + } else if communityModel.subscribed ?? false { + return .green + } + return .secondary + } + + var subscribeButtonBackgroundColor: Color { + if communityModel.favorited { + return .blue.opacity(0.1) + } else if communityModel.subscribed ?? false { + return .green.opacity(0.1) + } + return .clear + } + + var subscribeButtonIcon: String { + if communityModel.favorited { + return Icons.favoriteFill + } else if communityModel.subscribed ?? false { + return Icons.successCircle + } + return Icons.personFill + } + + @ViewBuilder + var subscribeButton: some View { + let foregroundColor = subscribeButtonForegroundColor + if let subscribed = communityModel.subscribed { + HStack(spacing: 4) { + if let subscriberCount = communityModel.subscriberCount { + Text(abbreviateNumber(subscriberCount)) + } + Image(systemName: subscribeButtonIcon) + .aspectRatio(contentMode: .fit) + } + .foregroundStyle(foregroundColor) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background( + Capsule() + .strokeBorder(foregroundColor, style: .init(lineWidth: 1)) + .background(Capsule().fill(subscribeButtonBackgroundColor)) + ) + .gesture(TapGesture().onEnded { _ in + hapticManager.play(haptic: .lightSuccess, priority: .low) + print("tapped subscribe") + Task { + var community = communityModel + do { + if communityModel.favorited { + print("favorited") + confirmDestructive(destructiveFunction: communityModel.favoriteMenuFunction { communityModel = $0 }) + } else if subscribed { + print("subscribed") + try confirmDestructive(destructiveFunction: communityModel.subscribeMenuFunction { communityModel = $0 }) + } else { + print("not subscribed") + try await community.toggleSubscribe { item in + DispatchQueue.main.async { communityModel = item } + } + } + } catch { + errorHandler.handle(error) + } + } + }) + .simultaneousGesture(LongPressGesture().onEnded { _ in + hapticManager.play(haptic: .lightSuccess, priority: .low) + Task { + var community = communityModel + do { + try await communityModel.toggleFavorite { item in + DispatchQueue.main.async { communityModel = item } + } + } catch { + errorHandler.handle(error) + } + } + }) } } } diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index 0eaada587..dcc924681 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -75,7 +75,7 @@ struct FeedsView: View { case .saved: SavedFeedView() case let .community(communityModel): - NewCommunityFeedView(feedType: .community(communityModel)) + NewCommunityFeedView(communityModel: communityModel) case .none: Text("Please select a feed") } From 61d16c00c25edc019e85d427ba1418655d093810 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 11:41:58 -0500 Subject: [PATCH 33/69] killed some old views --- Mlem.xcodeproj/project.pbxproj | 20 - .../View+HandleLemmyLinks.swift | 7 +- Mlem/Models/Content/CommentModel.swift | 25 -- .../CommunityModel+MenuFunctions.swift | 13 +- .../Navigation Contexts/Post Link.swift | 17 - .../Trackers/Feeds/UserContentTracker.swift | 27 -- .../Trackers/Generics/CoreTracker.swift | 6 - .../Trackers/Generics/StandardTracker.swift | 2 - Mlem/Navigation/Routes/AppRoutes.swift | 6 +- .../Composer/PostComposerView+Logic.swift | 4 +- .../Components/CommunityListRowViews.swift | 5 +- Mlem/Views/Tabs/Feeds/CommunityView.swift | 349 ------------------ Mlem/Views/Tabs/Feeds/Feed Root.swift | 98 ----- Mlem/Views/Tabs/Feeds/FeedParentView.swift | 44 --- .../Components/NEW PostFeedView.swift | 2 +- .../Feed Types/NEW CommunityView.swift | 26 +- 16 files changed, 39 insertions(+), 612 deletions(-) delete mode 100644 Mlem/Models/Content/CommentModel.swift delete mode 100644 Mlem/Models/Trackers/Feeds/UserContentTracker.swift delete mode 100644 Mlem/Views/Tabs/Feeds/CommunityView.swift delete mode 100644 Mlem/Views/Tabs/Feeds/Feed Root.swift delete mode 100644 Mlem/Views/Tabs/Feeds/FeedParentView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index c76b97242..868660b9f 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -27,9 +27,7 @@ 030E864C2AC7037F000283A6 /* SearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */; }; 0315E9F12B41BD2800E3BA88 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */; }; 0315E9F32B41C1F900E3BA88 /* PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */; }; - 0315E9F52B41C3EB00E3BA88 /* CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F42B41C3EB00E3BA88 /* CommunityView.swift */; }; 0315E9F72B41CD0C00E3BA88 /* FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */; }; - 0315E9F92B41D6DC00E3BA88 /* FeedParentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F82B41D6DC00E3BA88 /* FeedParentView.swift */; }; 0315E9FB2B41E09A00E3BA88 /* FeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */; }; 0315E9FD2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */; }; 031A617C2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */; }; @@ -305,7 +303,6 @@ B104A6DC2A59BF3C00B3E725 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6DB2A59BF3C00B3E725 /* NukeUI */; }; B104A6DE2A59BF3C00B3E725 /* NukeVideo in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6DD2A59BF3C00B3E725 /* NukeVideo */; }; B104A6E02A59C19400B3E725 /* OperationQueue+ConvenienceInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B104A6DF2A59C19400B3E725 /* OperationQueue+ConvenienceInit.swift */; }; - B11A1A782A4EFF2B00520DB4 /* Feed Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11A1A772A4EFF2B00520DB4 /* Feed Root.swift */; }; B11D72832A49FAA7009DC22F /* Cached Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11D72822A49FAA7009DC22F /* Cached Image.swift */; }; B14E93C02A45CA3400D6DA93 /* Post Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E93BF2A45CA3400D6DA93 /* Post Link.swift */; }; B14E93C22A45D3B300D6DA93 /* Community Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14E93C12A45D3B300D6DA93 /* Community Link.swift */; }; @@ -401,8 +398,6 @@ CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; CD59E8A52A72C943005757F4 /* MarkAllAsReadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */; }; - CD62D12A2B5EC4E900395BD9 /* UserContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */; }; - CD62D12C2B5EC65200395BD9 /* CommentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD62D12B2B5EC65200395BD9 /* CommentModel.swift */; }; CD6483302A38D31C00EE6CA3 /* UpvoteCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD64832F2A38D31C00EE6CA3 /* UpvoteCounterView.swift */; }; CD6483322A38D3A600EE6CA3 /* ScoreCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6483312A38D3A600EE6CA3 /* ScoreCounterView.swift */; }; CD6483362A39F20800EE6CA3 /* Post Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6483352A39F20800EE6CA3 /* Post Type.swift */; }; @@ -583,9 +578,7 @@ 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarExtensions.swift; sourceTree = ""; }; 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = ""; }; 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+Logic.swift"; sourceTree = ""; }; - 0315E9F42B41C3EB00E3BA88 /* CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityView.swift; sourceTree = ""; }; 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedView.swift; sourceTree = ""; }; - 0315E9F82B41D6DC00E3BA88 /* FeedParentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedParentView.swift; sourceTree = ""; }; 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedView+Logic.swift"; sourceTree = ""; }; 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedAccountSettingsView.swift; sourceTree = ""; }; @@ -859,7 +852,6 @@ ADDC9E392A5CEAA100383D58 /* BlockPerson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockPerson.swift; sourceTree = ""; }; B104A6DF2A59C19400B3E725 /* OperationQueue+ConvenienceInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperationQueue+ConvenienceInit.swift"; sourceTree = ""; }; B104A6E12A5AFC9F00B3E725 /* Mlem.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mlem.entitlements; sourceTree = ""; }; - B11A1A772A4EFF2B00520DB4 /* Feed Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feed Root.swift"; sourceTree = ""; }; B11D72822A49FAA7009DC22F /* Cached Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cached Image.swift"; sourceTree = ""; }; B14E93BF2A45CA3400D6DA93 /* Post Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Link.swift"; sourceTree = ""; }; B14E93C12A45D3B300D6DA93 /* Community Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Community Link.swift"; sourceTree = ""; }; @@ -955,8 +947,6 @@ CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; CD59E8A42A72C943005757F4 /* MarkAllAsReadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadRequest.swift; sourceTree = ""; }; - CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserContentTracker.swift; sourceTree = ""; }; - CD62D12B2B5EC65200395BD9 /* CommentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentModel.swift; sourceTree = ""; }; CD64832F2A38D31C00EE6CA3 /* UpvoteCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpvoteCounterView.swift; sourceTree = ""; }; CD6483312A38D3A600EE6CA3 /* ScoreCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreCounterView.swift; sourceTree = ""; }; CD6483352A39F20800EE6CA3 /* Post Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Type.swift"; sourceTree = ""; }; @@ -2201,7 +2191,6 @@ isa = PBXGroup; children = ( CD1262792B4759BC007549F9 /* StandardPostTracker.swift */, - CD62D1292B5EC4E900395BD9 /* UserContentTracker.swift */, ); path = Feeds; sourceTree = ""; @@ -2389,15 +2378,12 @@ CD2E14782A6B283D004198DE /* Feeds */ = { isa = PBXGroup; children = ( - 0315E9F42B41C3EB00E3BA88 /* CommunityView.swift */, 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */, 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */, - 0315E9F82B41D6DC00E3BA88 /* FeedParentView.swift */, 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */, 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */, 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */, - B11A1A772A4EFF2B00520DB4 /* Feed Root.swift */, 6332FDD427F080FA0009A98A /* Community List */, 63344C6F2A098054001BC616 /* Components */, ); @@ -2758,7 +2744,6 @@ CD4368C22AE2409D00BD8BD1 /* Inbox */, CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */, CDEBC3292A9A580B00518D9D /* Post Model.swift */, - CD62D12B2B5EC65200395BD9 /* CommentModel.swift */, 03FD64FD2AE538C600957AA9 /* Community */, 030D00862AD1BB0D00953B1D /* User */, 03B7AAF02ABE404300068B23 /* ContentModel.swift */, @@ -3156,7 +3141,6 @@ CDB0117F2A6F70A000D043EB /* Editor Tracker.swift in Sources */, 030E86482AC6FD1D000283A6 /* _assignIfNotEqual.swift in Sources */, 6354F30A2A2E20040074C08D /* View+Alert.swift in Sources */, - 0315E9F52B41C3EB00E3BA88 /* CommunityView.swift in Sources */, 03EC92992AC0BF8A007BBE7E /* APIClient+Pictrs.swift in Sources */, 03C905C82B3C70E200B9082F /* UserView.swift in Sources */, 6372186C2A3A2AAD008C4816 /* SaveComment.swift in Sources */, @@ -3318,7 +3302,6 @@ B1CB6E752A4C729D00DA9675 /* Bundle+IconFileName.swift in Sources */, 030D4AE82AA1278400A3393D /* ErrorDetails+Mock.swift in Sources */, CD4368C62AE240BF00BD8BD1 /* MessageModel.swift in Sources */, - CD62D12C2B5EC65200395BD9 /* CommentModel.swift in Sources */, 6363D60427EE20A200E34822 /* Expanded Post.swift in Sources */, 6DE1183C2A4A217400810C7E /* Profile View.swift in Sources */, CD04D5DF2A361585008EF95B /* Empty Button Style.swift in Sources */, @@ -3374,14 +3357,12 @@ 6D7782342A48EE8C008AC1BF /* APIPrivateMessageView.swift in Sources */, 030E86392AC6B44B000283A6 /* DeletePictrsFile.swift in Sources */, 50811B342A9204EB006BA3F2 /* APICommunityAggregates+Mock.swift in Sources */, - B11A1A782A4EFF2B00520DB4 /* Feed Root.swift in Sources */, CDC1C93F2A7AB8C700072E3D /* AccessibilitySettingsView.swift in Sources */, 50C99B622A629C06005D57DD /* ErrorHandler+Dependency.swift in Sources */, CD309C462A93FBD300988F95 /* Logo View.swift in Sources */, CD9A49D72B059303001E18A0 /* ImageSaver.swift in Sources */, 03E90FB12B3703ED00E5A802 /* AccountSortMode.swift in Sources */, CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */, - CD62D12A2B5EC4E900395BD9 /* UserContentTracker.swift in Sources */, 50EC39B22A346DDC00E014C2 /* URLHandler.swift in Sources */, 63F0C7BF2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift in Sources */, 632E8EE827EE63DB007E8D75 /* DownvoteButtonView.swift in Sources */, @@ -3501,7 +3482,6 @@ CD69F56F2A41EDF50028D4F7 /* View+SwipeyActions.swift in Sources */, 637218632A3A2AAD008C4816 /* CreatePost.swift in Sources */, CDE6A80B2A43E9F00062D161 /* CommentSortType.swift in Sources */, - 0315E9F92B41D6DC00E3BA88 /* FeedParentView.swift in Sources */, 503BA26F2A2C94540052516C /* URL+Identifiable.swift in Sources */, CD6483322A38D3A600EE6CA3 /* ScoreCounterView.swift in Sources */, 50CC4A7A2A9CC45D0074C845 /* InstanceMetadata+Mock.swift in Sources */, diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index bb4f8b324..98508620d 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -29,12 +29,7 @@ struct HandleLemmyLinksDisplay: ViewModifier { .navigationDestination(for: AppRoute.self) { route in switch route { case let .community(community): - CommunityView(community: community) - .environmentObject(appState) - .environmentObject(filtersTracker) - .environmentObject(quickLookState) - case let .communityLinkWithContext(context): - FeedParentView(community: context.community, feedType: context.feedType) + NewCommunityFeedView(communityModel: community) .environmentObject(appState) .environmentObject(filtersTracker) .environmentObject(quickLookState) diff --git a/Mlem/Models/Content/CommentModel.swift b/Mlem/Models/Content/CommentModel.swift deleted file mode 100644 index 509c72cdd..000000000 --- a/Mlem/Models/Content/CommentModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CommentModel.swift -// Mlem -// -// Created by Eric Andrews on 2024-01-22. -// - -import Foundation - -class CommentModel: ContentIdentifiable, ObservableObject { - let comment: APIComment - let creator: APIPerson - let post: APIPost - let community: APICommunity - let counts: APICommentAggregates - let creatorBannedFromCommunity: Bool - let creatorIsModerator: Bool? // TODO: 0.18 deprecation make this field non-optional - let creatorIsAdmin: Bool? // TODO: 0.18 deprecation make this field non-optional - let subscribed: APISubscribedStatus - let saved: Bool - let creatorBlocked: Bool - var myVote: ScoringOperation? - - init(from apiCommentView: APICommentView) {} -} diff --git a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift index 80585f106..08a09b94b 100644 --- a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift +++ b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift @@ -9,18 +9,17 @@ import Foundation import SwiftUI extension CommunityModel { - func newPostMenuFunction(editorTracker: EditorTracker, postTracker: PostTracker? = nil) -> MenuFunction { + func newPostMenuFunction(editorTracker: EditorTracker, postTracker: StandardPostTracker? = nil) -> MenuFunction { .standardMenuFunction( text: "New Post", imageName: Icons.sendFill, destructiveActionPrompt: nil, enabled: true ) { - assertionFailure("ERIC RE-IMPLEMENT THIS") -// editorTracker.openEditor(with: PostEditorModel( -// community: self, -// postTracker: postTracker -// )) + editorTracker.openEditor(with: PostEditorModel( + community: self, + postTracker: postTracker + )) } } @@ -89,7 +88,7 @@ extension CommunityModel { func menuFunctions( _ callback: @escaping (_ item: Self) -> Void = { _ in }, editorTracker: EditorTracker? = nil, - postTracker: PostTracker? = nil + postTracker: StandardPostTracker? = nil ) -> [MenuFunction] { var functions: [MenuFunction] = .init() if let editorTracker { diff --git a/Mlem/Models/Navigation Contexts/Post Link.swift b/Mlem/Models/Navigation Contexts/Post Link.swift index 1eba56d8d..88a24f5bb 100644 --- a/Mlem/Models/Navigation Contexts/Post Link.swift +++ b/Mlem/Models/Navigation Contexts/Post Link.swift @@ -8,7 +8,6 @@ import Foundation import SwiftUI -// TODO: ERIC delete this struct PostLinkWithContext: Equatable, Identifiable, Hashable { static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id @@ -25,19 +24,3 @@ struct PostLinkWithContext: Equatable, Identifiable, Hashable { let postTracker: StandardPostTracker var scrollTarget: Int? } - -// struct NewPostLinkWithContext: Equatable, Identifiable, Hashable { -// static func == (lhs: Self, rhs: Self) -> Bool { -// lhs.id == rhs.id -// } -// -// func hash(into hasher: inout Hasher) { -// hasher.combine(id) -// } -// -// var id: Int { post.postId } -// -// let post: PostModel -// var community: CommunityModel? -// var scrollTarget: Int? -// } diff --git a/Mlem/Models/Trackers/Feeds/UserContentTracker.swift b/Mlem/Models/Trackers/Feeds/UserContentTracker.swift deleted file mode 100644 index 2bbe3916a..000000000 --- a/Mlem/Models/Trackers/Feeds/UserContentTracker.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// UserContentTracker.swift -// Mlem -// -// Created by Eric Andrews on 2024-01-22. -// - -import Foundation - -enum UserContentItem: TrackerItem { - case post(PostModel) - case comment(CommentModel) - - var uid: ContentModelIdentifier { - switch self { - case let .post(postModel): postModel.uid - case let .comment(commentModel): commentModel.uid - } - } - - func sortVal(sortType: TrackerSortType) -> TrackerSortVal { - switch self { - case let .post(postModel): postModel.sortVal(sortType: sortType) - case let .comment(commentModel): commentModel.sortVal(sortType: sortType) - } - } -} diff --git a/Mlem/Models/Trackers/Generics/CoreTracker.swift b/Mlem/Models/Trackers/Generics/CoreTracker.swift index f82a15a82..4464432f0 100644 --- a/Mlem/Models/Trackers/Generics/CoreTracker.swift +++ b/Mlem/Models/Trackers/Generics/CoreTracker.swift @@ -58,12 +58,6 @@ class CoreTracker: ObservableObject { updateThresholds() } - func synchronousPrependItem(_ newItem: Item) { - Task { - await prependItem(newItem) - } - } - @MainActor func prependItem(_ newItem: Item) async { items.prepend(newItem) diff --git a/Mlem/Models/Trackers/Generics/StandardTracker.swift b/Mlem/Models/Trackers/Generics/StandardTracker.swift index cc3145ad1..2cf0d5d81 100644 --- a/Mlem/Models/Trackers/Generics/StandardTracker.swift +++ b/Mlem/Models/Trackers/Generics/StandardTracker.swift @@ -96,8 +96,6 @@ class StandardTracker: CoreTracker { await clearHelper() // this is not a thread-safe use of clear, but I'm using it here because we should never get here } } - - func prepend(_ item: Item) async {} // MARK: - Internal methods diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index 28d469b08..21c090b1d 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -15,7 +15,7 @@ enum AppRoute: Routable { // case feed(NewFeedType) // TODO: ERIC remove this - case communityLinkWithContext(CommunityLinkWithContext) + // case communityLinkWithContext(CommunityLinkWithContext) case apiPostView(APIPostView) case apiPost(APIPost) @@ -42,8 +42,8 @@ enum AppRoute: Routable { // swiftlint:disable cyclomatic_complexity static func makeRoute(_ value: some Hashable) throws -> AppRoute { switch value { - case let value as CommunityLinkWithContext: - return .communityLinkWithContext(value) +// case let value as CommunityLinkWithContext: +// return .communityLinkWithContext(value) case let value as APIPostView: return .apiPostView(value) case let value as APIPost: diff --git a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift index aebe0e7c4..b09f078b7 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift @@ -67,8 +67,8 @@ extension PostComposerView { // TODO: ERIC test this if let postTracker = editModel.postTracker { - withAnimation { - postTracker.synchronousPrependItem(_:)(PostModel(from: response.postView)) + Task { + await postTracker.prependItem(PostModel(from: response.postView)) } } } diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift b/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift index 1055fead3..6449cd87d 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift @@ -71,7 +71,8 @@ struct CommuntiyFeedRowView: View { return CommunityLinkWithContext(community: CommunityModel(from: community), feedType: .subscribed) } else { // Do not use enum route path in sidebar: It doesn't work, and I have no idea why =/ [2023.09] - return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) + // return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) + return AppRoute.community(CommunityModel(from: community)) } } @@ -153,6 +154,6 @@ struct HomepageFeedRowView: View { } private var pathValue: AnyHashable { - return CommunityLinkWithContext(community: nil, feedType: feedType) + CommunityLinkWithContext(community: nil, feedType: feedType) } } diff --git a/Mlem/Views/Tabs/Feeds/CommunityView.swift b/Mlem/Views/Tabs/Feeds/CommunityView.swift deleted file mode 100644 index 5d2a1a436..000000000 --- a/Mlem/Views/Tabs/Feeds/CommunityView.swift +++ /dev/null @@ -1,349 +0,0 @@ -// -// CommunityView.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import SwiftUI -import Dependencies - -// swiftlint:disable type_body_length -struct CommunityView: View { - @Dependency(\.hapticManager) var hapticManager - @Dependency(\.errorHandler) var errorHandler - @Dependency(\.communityRepository) var communityRepository - - @AppStorage("shouldShowCommunityHeaders") var shouldShowCommunityHeaders: Bool = true - @AppStorage("shouldShowCommunityIcons") var shouldShowCommunityIcons: Bool = true - - enum Tab: String, Identifiable, CaseIterable { - var id: Self { self } - case posts, about, moderators, statistics - } - - @Environment(\.navigationPathWithRoutes) private var navigationPath - @Environment(\.scrollViewProxy) private var scrollViewProxy - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(\.colorScheme) var colorScheme - @EnvironmentObject var editorTracker: EditorTracker - - @State var community: CommunityModel - @State var selectedTab: Tab = .posts - - @Binding var rootDetails: CommunityLinkWithContext? - @Binding var splitViewColumnVisibility: NavigationSplitViewVisibility - - // MARK: Feed - - @StateObject var postTracker: PostTracker - @State var postSortType: PostSortType - - // MARK: Scroll to top - - @Namespace var scrollToTop - @State private var scrollToTopAppeared = false - private var scrollToTopId: Int? { - postTracker.items.first?.id - } - - // MARK: Destructive confirmation - - @State private var isPresentingConfirmDestructive: Bool = false - @State private var confirmationMenuFunction: StandardMenuFunction? - - func confirmDestructive(destructiveFunction: StandardMenuFunction) { - confirmationMenuFunction = destructiveFunction - isPresentingConfirmDestructive = true - } - - init( - community: CommunityModel, - splitViewColumnVisibility: Binding? = nil, - rootDetails: Binding? = nil - ) { - // need to grab some stuff from app storage to initialize post tracker with - @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast - @AppStorage("upvoteOnSave") var upvoteOnSave = false - - self._community = State(initialValue: community) - - self._rootDetails = rootDetails ?? .constant(nil) - self._splitViewColumnVisibility = splitViewColumnVisibility ?? .constant(.automatic) - - @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - self._postSortType = .init(wrappedValue: defaultPostSorting) - - self._postTracker = StateObject(wrappedValue: .init( - shouldPerformMergeSorting: false, - internetSpeed: internetSpeed, - upvoteOnSave: upvoteOnSave, - type: .community(community, sortedBy: defaultPostSorting) - )) - } - - var body: some View { - ScrollView { - VStack(spacing: 0) { - VStack(spacing: 0) { - ScrollToView(appeared: $scrollToTopAppeared) - .id(scrollToTop) - headerView - .padding(.top, 5) - } - .background(Color.systemBackground) - switch selectedTab { - case .posts: - if !postTracker.items.isEmpty { - Divider() - .padding(.top, 15) - .background(Color.secondarySystemBackground) - } - PostFeedView(community: community, postTracker: postTracker, postSortType: $postSortType) - .background(Color.secondarySystemBackground) - case .about: - Divider() - .padding(.top, 15) - .background(Color.secondarySystemBackground) - VStack(spacing: AppConstants.postAndCommentSpacing) { - if shouldShowCommunityHeaders, let banner = community.banner { - CachedImage(url: banner, cornerRadius: AppConstants.largeItemCornerRadius) - } - MarkdownView(text: community.description ?? "", isNsfw: false) - } - .padding(AppConstants.postAndCommentSpacing) - case .moderators: - if let moderators = community.moderators { - Divider() - .padding(.top, 15) - .background(Color.secondarySystemBackground) - ForEach(moderators, id: \.id) { user in - UserResultView(user, communityContext: community) - Divider() - } - Color.secondarySystemBackground - .frame(height: 100) - } - case .statistics: - CommunityStatsView(community: community) - .padding(.top, 16) - .background(Color(uiColor: .systemGroupedBackground)) - } - } - } - .refreshable { - await postTracker.refresh() - } - .background { - VStack(spacing: 0) { - Color.systemBackground - .frame(height: 200) - if selectedTab != .about && (selectedTab != .statistics || colorScheme == .light) { - Color.secondarySystemBackground - } else { - Color.systemBackground - } - } - } - .navigationBarTitleDisplayMode(.inline) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .fancyTabScrollCompatible() - .navigationBarColor(visibility: .automatic) - .hoistNavigation { - if navigationPath.isEmpty { - // Need to check `scrollToTopAppeared` because we want to scroll to top before popping back to sidebar. [2023.09] - if scrollToTopAppeared { - if horizontalSizeClass == .regular { - print("show/hide sidebar in regular size class") - splitViewColumnVisibility = splitViewColumnVisibility == .all ? .detailOnly : .all - return true - } else { - print("show/hide sidebar in compact size class") - // This seems a lot more reliable than dismiss action for some reason. [2023.09] - rootDetails = nil - return true - } - } else { - print("scroll to top") - withAnimation { - scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) - } - return true - } - } else { - if scrollToTopAppeared { - print("exhausted auxiliary actions, perform dismiss action instead...") - return false - } else { - withAnimation { - scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) - } - return true - } - } - } - .toolbar { - ToolbarItem(placement: .principal) { - Text(community.name) - .font(.headline) - .opacity(scrollToTopAppeared ? 0 : 1) - .animation(.easeOut(duration: 0.2), value: scrollToTopAppeared) - } - ToolbarItemGroup(placement: .secondaryAction) { - ForEach( - community.menuFunctions({ community = $0 }, - editorTracker: editorTracker, - postTracker: postTracker - ) - ) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: confirmDestructive) - } - .destructiveConfirmation( - isPresentingConfirmDestructive: $isPresentingConfirmDestructive, - confirmationMenuFunction: confirmationMenuFunction - ) - } - } - .onAppear { - if community.moderators == nil { - Task(priority: .userInitiated) { - do { - self.community = try await communityRepository.loadDetails(for: community.communityId) - } catch { - errorHandler.handle(error) - } - } - } - } - } - - var availableTabs: [Tab] { - var output: [Tab] = [.posts, .moderators, .statistics] - if community.description != nil { - output.insert(.about, at: 1) - } - return output - } - - @ViewBuilder - var headerView: some View { - Group { - VStack(spacing: 5) { - HStack(alignment: .center, spacing: 10) { - if shouldShowCommunityIcons { - AvatarView(community: community, avatarSize: 44, iconResolution: .unrestricted) - } - Button(action: community.copyFullyQualifiedName) { - VStack(alignment: .leading, spacing: 0) { - Text(community.displayName) - .font(.title2) - .fontWeight(.semibold) - .lineLimit(1) - .minimumScaleFactor(0.01) - if let fullyQualifiedName = community.fullyQualifiedName { - Text(fullyQualifiedName) - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) - } - } - .frame(height: 44) - } - .buttonStyle(.plain) - Spacer() - subscribeButton - } - .padding(.horizontal, AppConstants.postAndCommentSpacing) - .padding(.bottom, 3) - Divider() - BubblePicker(availableTabs, selected: $selectedTab) { - Text($0.rawValue.capitalized) - } - } - Divider() - } - } - - var subscribeButtonForegroundColor: Color { - if community.favorited { - return .blue - } else if community.subscribed ?? false { - return .green - } - return .secondary - } - - var subscribeButtonBackgroundColor: Color { - if community.favorited { - return .blue.opacity(0.1) - } else if community.subscribed ?? false { - return .green.opacity(0.1) - } - return .clear - } - - var subscribeButtonIcon: String { - if community.favorited { - return Icons.favoriteFill - } else if community.subscribed ?? false { - return Icons.successCircle - } - return Icons.personFill - } - - @ViewBuilder - var subscribeButton: some View { - let foregroundColor = subscribeButtonForegroundColor - if let subscribed = community.subscribed { - HStack(spacing: 4) { - if let subscriberCount = community.subscriberCount { - Text(abbreviateNumber(subscriberCount)) - } - Image(systemName: subscribeButtonIcon) - .aspectRatio(contentMode: .fit) - } - .foregroundStyle(foregroundColor) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background( - Capsule() - .strokeBorder(foregroundColor, style: .init(lineWidth: 1)) - .background(Capsule().fill(subscribeButtonBackgroundColor)) - ) - .gesture(TapGesture().onEnded { _ in - hapticManager.play(haptic: .lightSuccess, priority: .low) - Task { - var community = community - do { - if community.favorited { - confirmDestructive(destructiveFunction: community.favoriteMenuFunction { self.community = $0 }) - } else if subscribed { - confirmDestructive(destructiveFunction: try community.subscribeMenuFunction { self.community = $0 }) - } else { - try await community.toggleSubscribe { item in - DispatchQueue.main.async { self.community = item } - } - } - } catch { - errorHandler.handle(error) - } - } - }) - .simultaneousGesture(LongPressGesture().onEnded { _ in - hapticManager.play(haptic: .lightSuccess, priority: .low) - Task { - var community = community - do { - try await community.toggleFavorite { item in - DispatchQueue.main.async { self.community = item } - } - } catch { - errorHandler.handle(error) - } - } - }) - } - } -} - -// swiftlint:enable type_body_length diff --git a/Mlem/Views/Tabs/Feeds/Feed Root.swift b/Mlem/Views/Tabs/Feeds/Feed Root.swift deleted file mode 100644 index 0261ccae4..000000000 --- a/Mlem/Views/Tabs/Feeds/Feed Root.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// Feed Root.swift -// Mlem -// -// Created by tht7 on 30/06/2023. -// - -import Dependencies -import SwiftUI - -struct FeedRoot: View { - @EnvironmentObject var appState: AppState - @Environment(\.scenePhase) var phase - @Environment(\.tabSelectionHashValue) private var selectedTagHashValue - - @AppStorage("defaultFeed") var defaultFeed: FeedType = .subscribed - - @StateObject private var feedTabNavigation: AnyNavigationPath = .init() - @StateObject private var navigation: Navigation = .init() - - @State var rootDetails: CommunityLinkWithContext? - @State private var columnVisibility: NavigationSplitViewVisibility = .automatic - - var body: some View { - /* - Implementation Note: - - The conditional content in `detail` column must be inside the `NavigationStack`. To be clear, the root view for `detail` column must be `NavigationStack`, otherwise navigation may break in odd ways. [2023.09] - - For tab bar navigation (scroll to top) to work, ScrollViewReader must wrap the entire `NavigationSplitView`. Furthermore, the proxy must be passed into the environment on the split view. Attempting to do so on a column view doesn't work. [2023.09] - */ - ScrollViewReader { scrollProxy in - NavigationSplitView(columnVisibility: $columnVisibility) { - CommunityListView(selectedCommunity: $rootDetails) - } detail: { - NavigationStack(path: $feedTabNavigation.path) { - if let rootDetails { - FeedParentView( - community: rootDetails.community, - feedType: rootDetails.feedType - ) - .environmentObject(appState) - .environmentObject(feedTabNavigation) - .tabBarNavigationEnabled(.feeds, navigation) - .handleLemmyViews() - } else { - Text("Please select a community") - } - } - .id(rootDetails?.id ?? 0) - } - .environment(\.scrollViewProxy, scrollProxy) - } - .handleLemmyLinkResolution( - navigationPath: .constant(feedTabNavigation) - ) - .environment(\.navigationPathWithRoutes, $feedTabNavigation.path) - .environment(\.navigation, navigation) - .environmentObject(feedTabNavigation) - .environmentObject(appState) - .onAppear { - if rootDetails == nil || shortcutItemToProcess != nil { - let feedType = FeedType(rawValue: - shortcutItemToProcess?.type ?? - "nothing to see here" - ) ?? defaultFeed - rootDetails = CommunityLinkWithContext(community: nil, feedType: feedType) - shortcutItemToProcess = nil - } - } - .onOpenURL { url in - DispatchQueue.main.asyncAfter(deadline: .now()) { - if rootDetails == nil { - rootDetails = CommunityLinkWithContext(community: nil, feedType: defaultFeed) - } - - _ = HandleLemmyLinkResolution(navigationPath: .constant(feedTabNavigation)) - .didReceiveURL(url) - } - } - .onChange(of: phase) { newPhase in - if newPhase == .active { - if let shortcutItem = FeedType(rawValue: - shortcutItemToProcess?.type ?? - "nothing to see here" - ) { - rootDetails = CommunityLinkWithContext(community: nil, feedType: shortcutItem) - - shortcutItemToProcess = nil - } - } - } - } -} - -struct FeedRootPreview: PreviewProvider { - static var previews: some View { - FeedRoot() - } -} diff --git a/Mlem/Views/Tabs/Feeds/FeedParentView.swift b/Mlem/Views/Tabs/Feeds/FeedParentView.swift deleted file mode 100644 index 86389cf43..000000000 --- a/Mlem/Views/Tabs/Feeds/FeedParentView.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// FeedDecider.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import SwiftUI - -// This is messy I know, but I couldn't work out another way of doing it, thanks to NavigationSplitView's weirdness with NavigationLinks across columns. Sjmarf [2023.12] -struct FeedParentView: View { - - let community: CommunityModel? - let feedType: FeedType? - - @Binding var rootDetails: CommunityLinkWithContext? - @Binding var splitViewColumnVisibility: NavigationSplitViewVisibility - - init( - community: CommunityModel?, - feedType: FeedType?, - splitViewColumnVisibility: Binding? = nil, - rootDetails: Binding? = nil - ) { - self.community = community - self.feedType = feedType - self._splitViewColumnVisibility = splitViewColumnVisibility ?? .constant(.automatic) - self._rootDetails = rootDetails ?? .constant(nil) - } - - var body: some View { - Group { - if let community { - CommunityView( - community: community, - splitViewColumnVisibility: $splitViewColumnVisibility, - rootDetails: $rootDetails - ) - } else if let feedType { - FeedView(feedType: feedType) - } - } - } -} diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift index 392ba3230..727fa9a91 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift @@ -40,7 +40,7 @@ struct NewPostFeedView: View { } .task(id: siteInformation.version) { // when site version changes, check if it's resolved; if so, update sort type and siteVersionResolved - if let siteVersion = siteInformation.version { + if let siteVersion = siteInformation.version, !siteVersionResolved { let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting // manually change the tracker sort type here so that view is not redrawn by `onChange(of: postSortType)` diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift index 3ea77d099..cc05ee585 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift @@ -23,6 +23,8 @@ struct NewCommunityFeedView: View { @Dependency(\.hapticManager) var hapticManager @Dependency(\.communityRepository) var communityRepository + @EnvironmentObject var editorTracker: EditorTracker + @Environment(\.colorScheme) var colorScheme @StateObject var postTracker: StandardPostTracker @@ -76,7 +78,9 @@ struct NewCommunityFeedView: View { var body: some View { content .onAppear { - Task { await postTracker.loadMoreItems() } + if postTracker.items.isEmpty { + Task { await postTracker.loadMoreItems() } + } if communityModel.moderators == nil { Task(priority: .userInitiated) { @@ -109,6 +113,22 @@ struct NewCommunityFeedView: View { .opacity(scrollToTopAppeared ? 0 : 1) .animation(.easeOut(duration: 0.2), value: scrollToTopAppeared) } + + ToolbarItemGroup(placement: .secondaryAction) { + ForEach( + communityModel.menuFunctions( + { communityModel = $0 }, + editorTracker: editorTracker, + postTracker: postTracker + ) + ) { menuFunction in + MenuButton(menuFunction: menuFunction, confirmDestructive: confirmDestructive) + } + .destructiveConfirmation( + isPresentingConfirmDestructive: $isPresentingConfirmDestructive, + confirmationMenuFunction: confirmationMenuFunction + ) + } } .navigationBarTitleDisplayMode(.inline) .navigationBarColor(visibility: .automatic) @@ -134,9 +154,8 @@ struct NewCommunityFeedView: View { } .background { VStack(spacing: 0) { - // this awful little hack prevents a line from appearing in gray background tabs Color.systemBackground - .frame(height: 1) + .frame(height: 200) if selectedTab == .statistics || selectedTab == .moderators { Color(uiColor: .systemGroupedBackground) @@ -302,6 +321,7 @@ struct NewCommunityFeedView: View { Task { var community = communityModel do { + // TODO: this doesn't update view state when favoriting, but it does when unfavoriting try await communityModel.toggleFavorite { item in DispatchQueue.main.async { communityModel = item } } From 9ce3e11a35d93e8d6b7185b49023e0075becfcc4 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 11:43:00 -0500 Subject: [PATCH 34/69] killed FeedView --- Mlem.xcodeproj/project.pbxproj | 8 - Mlem/Views/Tabs/Feeds/FeedView+Logic.swift | 27 --- Mlem/Views/Tabs/Feeds/FeedView.swift | 227 --------------------- 3 files changed, 262 deletions(-) delete mode 100644 Mlem/Views/Tabs/Feeds/FeedView+Logic.swift delete mode 100644 Mlem/Views/Tabs/Feeds/FeedView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 868660b9f..47200d365 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -27,8 +27,6 @@ 030E864C2AC7037F000283A6 /* SearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */; }; 0315E9F12B41BD2800E3BA88 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */; }; 0315E9F32B41C1F900E3BA88 /* PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */; }; - 0315E9F72B41CD0C00E3BA88 /* FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */; }; - 0315E9FB2B41E09A00E3BA88 /* FeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */; }; 0315E9FD2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */; }; 031A617C2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */; }; 031A617E2B1CE90F00ABF23B /* ChangePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617D2B1CE90F00ABF23B /* ChangePasswordView.swift */; }; @@ -578,8 +576,6 @@ 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarExtensions.swift; sourceTree = ""; }; 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = ""; }; 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+Logic.swift"; sourceTree = ""; }; - 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedView.swift; sourceTree = ""; }; - 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedView+Logic.swift"; sourceTree = ""; }; 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedAccountSettingsView.swift; sourceTree = ""; }; 031A617D2B1CE90F00ABF23B /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = ""; }; @@ -2379,8 +2375,6 @@ isa = PBXGroup; children = ( 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, - 0315E9F62B41CD0C00E3BA88 /* FeedView.swift */, - 0315E9FA2B41E09A00E3BA88 /* FeedView+Logic.swift */, 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */, 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */, 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */, @@ -3149,7 +3143,6 @@ 50811B382A920545006BA3F2 /* APICommunityModeratorView+Mock.swift in Sources */, 50F2851C2A5C5C1500CF8865 /* TokenRefreshView.swift in Sources */, 03F4DCA32B1A8B0400556C67 /* AccountGeneralSettingsView.swift in Sources */, - 0315E9FB2B41E09A00E3BA88 /* FeedView+Logic.swift in Sources */, 507573962A5AD5CF00AA7ABD /* ContextualError.swift in Sources */, 50C99B592A61D889005D57DD /* APIClient+Dependency.swift in Sources */, 031A617C2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift in Sources */, @@ -3499,7 +3492,6 @@ CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */, 50811B2C2A920443006BA3F2 /* Date+Mock.swift in Sources */, CD391FA02A545F8600E213B5 /* Compact Post.swift in Sources */, - 0315E9F72B41CD0C00E3BA88 /* FeedView.swift in Sources */, B1A5A8152A4C882F00F203DB /* AlternativeIcon.swift in Sources */, 637218512A3A2AAD008C4816 /* APILocalSiteRateLimit.swift in Sources */, 50BC1ABB2A8D6A5A00E3C48B /* ScoringOperation.swift in Sources */, diff --git a/Mlem/Views/Tabs/Feeds/FeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/FeedView+Logic.swift deleted file mode 100644 index 023402c32..000000000 --- a/Mlem/Views/Tabs/Feeds/FeedView+Logic.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// FeedView+Logic.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import SwiftUI - -extension FeedView { - func genFeedSwitchingFunctions() -> [MenuFunction] { - var ret: [MenuFunction] = .init() - FeedType.allCases.forEach { type in - let (imageName, enabled) = type != feedType - ? (type.iconName, true) - : (type.iconNameFill, false) - ret.append(MenuFunction.standardMenuFunction( - text: type.label, - imageName: imageName, - destructiveActionPrompt: nil, - enabled: enabled, - callback: { feedType = type } - )) - } - return ret - } -} diff --git a/Mlem/Views/Tabs/Feeds/FeedView.swift b/Mlem/Views/Tabs/Feeds/FeedView.swift deleted file mode 100644 index 58d83b03d..000000000 --- a/Mlem/Views/Tabs/Feeds/FeedView.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// FeedView.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import SwiftUI -import Dependencies - -struct FeedView: View { - @Dependency(\.hapticManager) var hapticManager - @Dependency(\.errorHandler) var errorHandler - @Dependency(\.siteInformation) var siteInformation - - @Environment(\.navigationPathWithRoutes) private var navigationPath - @Environment(\.scrollViewProxy) private var scrollViewProxy - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @EnvironmentObject var appState: AppState - - @State var feedType: FeedType - - // MARK: Feed - - @StateObject var postTracker: PostTracker - @State var postSortType: PostSortType - - @Binding var rootDetails: CommunityLinkWithContext? - @Binding var splitViewColumnVisibility: NavigationSplitViewVisibility - - // MARK: Scroll to top - - @Namespace var scrollToTop - @State private var scrollToTopAppeared = false - private var scrollToTopId: Int? { - postTracker.items.first?.id - } - - // MARK: Destructive confirmation - - @State private var isPresentingConfirmDestructive: Bool = false - @State private var confirmationMenuFunction: StandardMenuFunction? - - func confirmDestructive(destructiveFunction: StandardMenuFunction) { - confirmationMenuFunction = destructiveFunction - isPresentingConfirmDestructive = true - } - - init( - feedType: FeedType, - splitViewColumnVisibility: Binding? = nil, - rootDetails: Binding? = nil - ) { - // need to grab some stuff from app storage to initialize post tracker with - @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast - @AppStorage("upvoteOnSave") var upvoteOnSave = false - - self._feedType = State(initialValue: feedType) - - self._splitViewColumnVisibility = splitViewColumnVisibility ?? .constant(.automatic) - self._rootDetails = rootDetails ?? .constant(nil) - - @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - self._postSortType = .init(wrappedValue: defaultPostSorting) - - self._postTracker = StateObject(wrappedValue: .init( - shouldPerformMergeSorting: false, - internetSpeed: internetSpeed, - upvoteOnSave: upvoteOnSave, - type: .feed(feedType, sortedBy: defaultPostSorting) - )) - } - - var body: some View { - ScrollView { - VStack(spacing: 0) { - VStack(spacing: 0) { - ScrollToView(appeared: $scrollToTopAppeared) - .id(scrollToTop) - headerView - .padding(.top, 5) - } - .background(Color.systemBackground) - if !postTracker.items.isEmpty { - Divider() - } - PostFeedView(postTracker: postTracker, postSortType: $postSortType) - .background(Color.secondarySystemBackground) - } - } - .refreshable { - await Task { - _ = await postTracker.refresh(clearBeforeFetch: false) - }.value - } - .background { - VStack(spacing: 0) { - Color.systemBackground - Color.secondarySystemBackground - } - } - .frame(maxWidth: .infinity) - .navigationBarTitleDisplayMode(.inline) - .navigationBarColor(visibility: .automatic) - .hoistNavigation { - if navigationPath.isEmpty { - // Need to check `scrollToTopAppeared` because we want to scroll to top before popping back to sidebar. [2023.09] - if scrollToTopAppeared { - if horizontalSizeClass == .regular { - print("show/hide sidebar in regular size class") - splitViewColumnVisibility = splitViewColumnVisibility == .all ? .detailOnly : .all - return true - } else { - print("show/hide sidebar in compact size class") - // This seems a lot more reliable than dismiss action for some reason. [2023.09] - rootDetails = nil - return true - } - } else { - print("scroll to top") - withAnimation { - scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) - } - return true - } - } else { - if scrollToTopAppeared { - print("exhausted auxiliary actions, perform dismiss action instead...") - return false - } else { - withAnimation { - scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) - } - return true - } - } - } - .onChange(of: feedType) { newValue in - postTracker.type = .feed(newValue, sortedBy: postSortType) - scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) - } - .fancyTabScrollCompatible() - .toolbar { - ToolbarItem(placement: .principal) { - navBarTitle - .opacity(scrollToTopAppeared ? 0 : 1) - .animation(.easeOut(duration: 0.2), value: scrollToTopAppeared) - } - } - } - - var subtitle: String { - switch feedType { - case .all: - return "Posts from all federated instances" - case .local: - return "Posts from \(appState.currentActiveAccount?.instanceLink.host() ?? "your instance's") communities" - case .subscribed: - return "Posts from all subscribed communities" - } - } - - @ViewBuilder - var headerView: some View { - Group { - VStack(spacing: 5) { - HStack(alignment: .center, spacing: 10) { - Image(systemName: feedType.iconNameCircle) - .resizable() - .frame(width: 44, height: 44) - .foregroundStyle(feedType.color ?? .primary) - VStack(alignment: .leading, spacing: 0) { - Menu { - ForEach(genFeedSwitchingFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) - } - } label: { - HStack(spacing: 5) { - Text(feedType.label) - .lineLimit(1) - .minimumScaleFactor(0.01) - .fontWeight(.semibold) - Image(systemName: Icons.dropdown) - .foregroundStyle(.secondary) - } - .font(.title2) - } - .buttonStyle(.plain) - Text(subtitle) - .font(.footnote) - .foregroundStyle(.secondary) - } - .frame(height: 44) - Spacer() - } - .padding(.horizontal, AppConstants.postAndCommentSpacing) - .padding(.bottom, 3) - } - Divider() - .padding(.bottom, 15) - .frame(maxWidth: .infinity) - .background(Color.secondarySystemBackground) - } - } - - @ViewBuilder - var navBarTitle: some View { - Menu { - ForEach(genFeedSwitchingFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) - } - } label: { - HStack(alignment: .center, spacing: 0) { - Text(feedType.label) - .font(.headline) - Image(systemName: Icons.dropdown) - .scaleEffect(0.7) - .fontWeight(.semibold) - } - .foregroundColor(.primary) - .accessibilityElement(children: .combine) - .accessibilityHint("Activate to change feeds.") - // this disables the implicit animation on the header view... - .transaction { $0.animation = nil } - } - } -} From 880ab44b2d4011e142f73b737b69e9287e51f41b Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 11:43:37 -0500 Subject: [PATCH 35/69] killed PostFeedView --- Mlem.xcodeproj/project.pbxproj | 12 -- .../Views/Tabs/Feeds/PostFeedView+Logic.swift | 54 ----- .../Feeds/PostFeedView+MenuFunctions.swift | 81 -------- Mlem/Views/Tabs/Feeds/PostFeedView.swift | 190 ------------------ 4 files changed, 337 deletions(-) delete mode 100644 Mlem/Views/Tabs/Feeds/PostFeedView+Logic.swift delete mode 100644 Mlem/Views/Tabs/Feeds/PostFeedView+MenuFunctions.swift delete mode 100644 Mlem/Views/Tabs/Feeds/PostFeedView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 47200d365..3235e43b1 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -25,9 +25,6 @@ 030E86462AC6FC1B000283A6 /* DefaultTextInputType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E86452AC6FC1B000283A6 /* DefaultTextInputType.swift */; }; 030E86482AC6FD1D000283A6 /* _assignIfNotEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E86472AC6FD1D000283A6 /* _assignIfNotEqual.swift */; }; 030E864C2AC7037F000283A6 /* SearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */; }; - 0315E9F12B41BD2800E3BA88 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */; }; - 0315E9F32B41C1F900E3BA88 /* PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */; }; - 0315E9FD2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */; }; 031A617C2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */; }; 031A617E2B1CE90F00ABF23B /* ChangePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617D2B1CE90F00ABF23B /* ChangePasswordView.swift */; }; 031A61802B1CEA7300ABF23B /* ChangePassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A617F2B1CEA7300ABF23B /* ChangePassword.swift */; }; @@ -574,9 +571,6 @@ 030E86452AC6FC1B000283A6 /* DefaultTextInputType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextInputType.swift; sourceTree = ""; }; 030E86472AC6FD1D000283A6 /* _assignIfNotEqual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _assignIfNotEqual.swift; sourceTree = ""; }; 030E864B2AC7037F000283A6 /* SearchBarExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarExtensions.swift; sourceTree = ""; }; - 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = ""; }; - 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+Logic.swift"; sourceTree = ""; }; - 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; 031A617B2B1BDFD100ABF23B /* AdvancedAccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedAccountSettingsView.swift; sourceTree = ""; }; 031A617D2B1CE90F00ABF23B /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = ""; }; 031A617F2B1CEA7300ABF23B /* ChangePassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePassword.swift; sourceTree = ""; }; @@ -2375,9 +2369,6 @@ isa = PBXGroup; children = ( 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, - 0315E9F02B41BD2800E3BA88 /* PostFeedView.swift */, - 0315E9F22B41C1F900E3BA88 /* PostFeedView+Logic.swift */, - 0315E9FC2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift */, 6332FDD427F080FA0009A98A /* Community List */, 63344C6F2A098054001BC616 /* Components */, ); @@ -3039,7 +3030,6 @@ CD6F29AA2A78003A00F20B6B /* PostRepository.swift in Sources */, 63F0C7A82A0522FC00A18C5D /* Saved Account Tracker.swift in Sources */, E449C5972B35239500E3BCF4 /* InboxReplyView.swift in Sources */, - 0315E9FD2B41E36300E3BA88 /* PostFeedView+MenuFunctions.swift in Sources */, 6372186A2A3A2AAD008C4816 /* GetComment.swift in Sources */, 03C905CA2B3C834C00B9082F /* AvatarBannerView.swift in Sources */, 03F76FA42B2F5F3500E2B54A /* UploadProgressView.swift in Sources */, @@ -3155,7 +3145,6 @@ CDA145ED2A510AC100DDAFC9 /* MarkCommentReplyAsReadRequest.swift in Sources */, CD391F982A537E8E00E213B5 /* ReplyToComment.swift in Sources */, 5064D03D2A6DE0AA00B22EE3 /* Notifier.swift in Sources */, - 0315E9F32B41C1F900E3BA88 /* PostFeedView+Logic.swift in Sources */, CD9A49D52B0587F1001E18A0 /* ImageDetailView.swift in Sources */, CDC65D912A86B830007205E5 /* DeleteAccountView.swift in Sources */, 039C8DBD2B361C160096BAAF /* AccountButtonView.swift in Sources */, @@ -3497,7 +3486,6 @@ 50BC1ABB2A8D6A5A00E3C48B /* ScoringOperation.swift in Sources */, CD4368BA2AE23F6400BD8BD1 /* TrackerItem.swift in Sources */, 6386E0362A042C59006B3C1D /* Contributor.swift in Sources */, - 0315E9F12B41BD2800E3BA88 /* PostFeedView.swift in Sources */, E40E018C2AABF85500410B2C /* AppRoutes.swift in Sources */, CD18DC6F2A5209C3002C56BC /* MarkPrivateMessageAsReadRequest.swift in Sources */, CD82A2552A716C7C00111034 /* APIPersonUnreadCounts.swift in Sources */, diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/PostFeedView+Logic.swift deleted file mode 100644 index de46f5025..000000000 --- a/Mlem/Views/Tabs/Feeds/PostFeedView+Logic.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// PostFeedView+Logic.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import SwiftUI -import Dependencies - -extension PostFeedView { - - func setDefaultSortMode() { - @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - @AppStorage("fallbackDefaultPostSorting") var fallbackDefaultPostSorting: PostSortType = .hot - @Dependency(\.siteInformation) var siteInformation - if let siteVersion = siteInformation.version, siteVersion < defaultPostSorting.minimumVersion { - postSortType = fallbackDefaultPostSorting - } else { - postSortType = defaultPostSorting - } - } - - func filter(postView: PostModel) -> PostFilterReason? { - guard !postView.post.name.lowercased().contains(filtersTracker.filteredKeywords) else { return .keyword } - guard showReadPosts || !postView.read else { return .read } - return nil - } - - func handle(_ error: Error) { - switch error { - case APIClientError.networking: - guard postTracker.items.isEmpty else { - return - } - errorDetails = .init(title: "Unable to connect to Lemmy", error: error, refresh: { return await postTracker.refresh() }) - return - case APIClientError.decoding(let data, _): - // Checks if it's an "unknown sort type" error - if let str = String(data: data, encoding: .utf8), str.starts(with: "Query deserialize error: unknown variant") { - Task { - print("Unknown sort type: reloading feed") - @AppStorage("fallbackDefaultPostSorting") var fallbackDefaultPostSorting: PostSortType = .hot - postSortType = fallbackDefaultPostSorting - await postTracker.loadNextPage() - } - return - } - default: - break - } - errorDetails = .init(error: error, refresh: { return await postTracker.refresh() }) - } -} diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView+MenuFunctions.swift b/Mlem/Views/Tabs/Feeds/PostFeedView+MenuFunctions.swift deleted file mode 100644 index 9f40cc5b9..000000000 --- a/Mlem/Views/Tabs/Feeds/PostFeedView+MenuFunctions.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// PostFeedView+MenuFunctions.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import Foundation - -extension PostFeedView { - func genOuterSortMenuFunctions() -> [MenuFunction] { - PostSortType.availableOuterTypes.map { type in - let isSelected = postSortType == type - let imageName = isSelected ? type.iconNameFill : type.iconName - return MenuFunction.standardMenuFunction( - text: type.label, - imageName: imageName, - destructiveActionPrompt: nil, - enabled: !isSelected - ) { - postSortType = type - } - } - } - - func genTopSortMenuFunctions() -> [MenuFunction] { - PostSortType.availableTopTypes.map { type in - let isSelected = postSortType == type - return MenuFunction.standardMenuFunction( - text: type.label, - imageName: isSelected ? Icons.timeSortFill : Icons.timeSort, - destructiveActionPrompt: nil, - enabled: !isSelected - ) { - postSortType = type - } - } - } - - func genEllipsisMenuFunctions() -> [MenuFunction] { - var ret: [MenuFunction] = .init() - - let blurNsfwText = shouldBlurNsfw ? "Unblur NSFW" : "Blur NSFW" - ret.append(MenuFunction.standardMenuFunction( - text: blurNsfwText, - imageName: Icons.blurNsfw, - destructiveActionPrompt: nil, - enabled: true - ) { - shouldBlurNsfw.toggle() - }) - - let showReadPostsText = showReadPosts ? "Hide Read" : "Show Read" - ret.append(MenuFunction.standardMenuFunction( - text: showReadPostsText, - imageName: "book", - destructiveActionPrompt: nil, - enabled: true - ) { - showReadPosts.toggle() - }) - - return ret - } - - func genPostSizeSwitchingFunctions() -> [MenuFunction] { - PostSize.allCases.map { size in - let (imageName, enabled) = size != postSize - ? (size.iconName, true) - : (size.iconNameFill, false) - - return MenuFunction.standardMenuFunction( - text: size.label, - imageName: imageName, - destructiveActionPrompt: nil, - enabled: enabled, - callback: { postSize = size } - ) - } - } -} diff --git a/Mlem/Views/Tabs/Feeds/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/PostFeedView.swift deleted file mode 100644 index d8219dc19..000000000 --- a/Mlem/Views/Tabs/Feeds/PostFeedView.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// PostFeedView.swift -// Mlem -// -// Created by Sjmarf on 31/12/2023. -// - -import Dependencies -import SwiftUI - -struct PostFeedView: View { - @Dependency(\.errorHandler) var errorHandler - @Dependency(\.siteInformation) var siteInformation - - @AppStorage("shouldShowPostCreator") var shouldShowPostCreator: Bool = true - @AppStorage("showReadPosts") var showReadPosts: Bool = true - @AppStorage("shouldBlurNsfw") var shouldBlurNsfw: Bool = true - @AppStorage("postSize") var postSize: PostSize = .large - - @EnvironmentObject var filtersTracker: FiltersTracker - @EnvironmentObject var appState: AppState - @ObservedObject var postTracker: PostTracker - - var community: CommunityModel? - - @Binding var postSortType: PostSortType - - @State var shouldLoad: Bool = false - @State var errorDetails: ErrorDetails? - - init( - community: CommunityModel? = nil, - postTracker: PostTracker, - postSortType: Binding - ) { - self.community = community - self._postTracker = .init(wrappedValue: postTracker) - self._postSortType = postSortType - } - - var body: some View { - LazyVStack(spacing: 0) { - if postTracker.items.isEmpty { - noPostsView() - .padding(.top) - .frame(maxWidth: .infinity) - .frame(height: 400) - } else { - Group { - ForEach(postTracker.items, id: \.uid) { post in - feedPost(for: post) - } - // TODO: update to use proper LoadingState - EndOfFeedView(loadingState: postTracker.showLoadingIcon && postTracker.page > 1 ? .loading : .done, viewType: .hobbit) - } - .transition(.opacity) - } - } - .environmentObject(postTracker) - .animation(.easeOut(duration: 0.2), value: postTracker.items.isEmpty) - .toolbar { - ToolbarItem(placement: .primaryAction) { sortMenu } - ToolbarItemGroup(placement: .secondaryAction) { - ForEach(genEllipsisMenuFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) - } - Menu { - ForEach(genPostSizeSwitchingFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) - } - } label: { - Label("Post Size", systemImage: Icons.postSizeSetting) - } - } - } - .onAppear { - if postTracker.showLoadingIcon { - Task(priority: .userInitiated) { - postTracker.handleError = handle - postTracker.filter = filter - await postTracker.initFeed() - } - } - } - .onChange(of: postTracker.items) { newValue in - if !newValue.isEmpty { - errorDetails = nil - } - } - .onChange(of: postTracker.type) { _ in - Task(priority: .userInitiated) { - await postTracker.refresh(clearBeforeFetch: true) - } - } - .onChange(of: postSortType) { newValue in - Task(priority: .userInitiated) { - switch postTracker.type { - case let .feed(feedType, _): - postTracker.type = .feed(feedType, sortedBy: newValue) - case let .community(community, _): - postTracker.type = .community(community, sortedBy: newValue) - case nil: - break - } - } - } - .onChange(of: appState.currentActiveAccount) { _ in - Task(priority: .userInitiated) { - setDefaultSortMode() - await postTracker.refresh(clearBeforeFetch: true) - } - } - .onChange(of: showReadPosts) { _ in - Task(priority: .userInitiated) { - postTracker.filter = filter - await postTracker.refresh(clearBeforeFetch: true) - } - } - .onChange(of: shouldLoad) { value in - if value { - print("should load more posts...") - Task(priority: .medium) { await postTracker.loadNextPage() } - shouldLoad = false - } - } - } - - @ViewBuilder - private func feedPost(for post: PostModel) -> some View { - VStack(spacing: 0) { - // NavigationLink(.postLinkWithContext(.init(post: post, community: community, postTracker: postTracker))) { - FeedPost( - post: post, - community: community, - showPostCreator: shouldShowPostCreator, - showCommunity: community == nil - ) - // } - Divider() - } - .buttonStyle(EmptyButtonStyle()) // Make it so that the link doesn't mess with the styling - .onAppear { - // on appear, flag whether new content should be loaded. Actual loading is attached to the feed view itself so that it doesn't get cancelled by view derenders - if postTracker.shouldLoadContentAfter(after: post) { - shouldLoad = true - } - } - } - - @ViewBuilder - private func noPostsView() -> some View { - VStack { - if postTracker.showLoadingIcon { // don't show posts until site information loads to avoid jarring redraw - LoadingView(whatIsLoading: .posts) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .transition(.opacity) - } else if let errorDetails { - ErrorView(errorDetails) - .frame(maxWidth: .infinity) - } else { - NoPostsView(isLoading: $postTracker.showLoadingIcon, postSortType: $postSortType, showReadPosts: $showReadPosts) - .transition(.scale(scale: 0.9).combined(with: .opacity)) - .padding(.top, 25) - } - } - .animation(.easeOut(duration: 0.1), value: postTracker.showLoadingIcon) - } - - @ViewBuilder - private var sortMenu: some View { - Menu { - ForEach(genOuterSortMenuFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) // no destructive sorts - } - - Menu { - ForEach(genTopSortMenuFunctions()) { menuFunction in - MenuButton(menuFunction: menuFunction, confirmDestructive: nil) // no destructive sorts - } - } label: { - Label("Top...", systemImage: Icons.topSort) - } - } label: { - Label( - "Selected sorting by \(postSortType.description)", - systemImage: postSortType.iconName - ) - } - } -} From 41dc2a961d1b16fc6716bf49ffb912878f4041be Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 11:45:21 -0500 Subject: [PATCH 36/69] killed old feed components --- Mlem.xcodeproj/project.pbxproj | 10 +-- .../Tabs/Feeds/Components/NoPostsView.swift | 82 ------------------- .../Tabs/Feeds/Components/PostSortMenu.swift | 69 ---------------- .../Components}/CommunityStatsView.swift | 3 - 4 files changed, 1 insertion(+), 163 deletions(-) delete mode 100644 Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift delete mode 100644 Mlem/Views/Tabs/Feeds/Components/PostSortMenu.swift rename Mlem/Views/Tabs/{Feeds => NEW Feeds/Components}/CommunityStatsView.swift (98%) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 3235e43b1..6e0ace25a 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -60,7 +60,6 @@ 03A276792AFD903600C0D66B /* CommunityModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */; }; 03A2767B2AFE560000C0D66B /* CommunityModel+SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */; }; 03A2767D2AFE656700C0D66B /* UserModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */; }; - 03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */; }; 03B643572A6864CD00F65700 /* TabBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */; }; 03B7AAEF2ABCB9DC00068B23 /* ContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */; }; 03B7AAF12ABE404300068B23 /* ContentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAF02ABE404300068B23 /* ContentModel.swift */; }; @@ -168,7 +167,6 @@ 6322A5CB27F77A4D00135D4F /* Loading View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6322A5CA27F77A4D00135D4F /* Loading View.swift */; }; 6322A5D027F8629700135D4F /* UserLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6322A5CF27F8629700135D4F /* UserLinkView.swift */; }; 6322A5D227F88CFD00135D4F /* Time Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6322A5D127F88CFD00135D4F /* Time Parser.swift */; }; - 632578182A29F83C00446A66 /* PostSortMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632578172A29F83C00446A66 /* PostSortMenu.swift */; }; 632E8EE627EE63D3007E8D75 /* UpvoteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632E8EE527EE63D3007E8D75 /* UpvoteButtonView.swift */; }; 632E8EE827EE63DB007E8D75 /* DownvoteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632E8EE727EE63DB007E8D75 /* DownvoteButtonView.swift */; }; 6332FDBD27EFAF7C0009A98A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6332FDBC27EFAF7B0009A98A /* Settings.bundle */; }; @@ -606,7 +604,6 @@ 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+MenuFunctions.swift"; sourceTree = ""; }; 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+SwipeActions.swift"; sourceTree = ""; }; 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserModel+MenuFunctions.swift"; sourceTree = ""; }; - 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = ""; }; 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSettingsView.swift; sourceTree = ""; }; 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTracker.swift; sourceTree = ""; }; 03B7AAF02ABE404300068B23 /* ContentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentModel.swift; sourceTree = ""; }; @@ -714,7 +711,6 @@ 6322A5CA27F77A4D00135D4F /* Loading View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Loading View.swift"; sourceTree = ""; }; 6322A5CF27F8629700135D4F /* UserLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLinkView.swift; sourceTree = ""; }; 6322A5D127F88CFD00135D4F /* Time Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Time Parser.swift"; sourceTree = ""; }; - 632578172A29F83C00446A66 /* PostSortMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSortMenu.swift; sourceTree = ""; }; 632E8EE527EE63D3007E8D75 /* UpvoteButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpvoteButtonView.swift; sourceTree = ""; }; 632E8EE727EE63DB007E8D75 /* DownvoteButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownvoteButtonView.swift; sourceTree = ""; }; 6332FDBC27EFAF7B0009A98A /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -1677,8 +1673,6 @@ 63344C6F2A098054001BC616 /* Components */ = { isa = PBXGroup; children = ( - 632578172A29F83C00446A66 /* PostSortMenu.swift */, - 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */, ); path = Components; sourceTree = ""; @@ -2368,7 +2362,6 @@ CD2E14782A6B283D004198DE /* Feeds */ = { isa = PBXGroup; children = ( - 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, 6332FDD427F080FA0009A98A /* Community List */, 63344C6F2A098054001BC616 /* Components */, ); @@ -2481,6 +2474,7 @@ CD4BAD392B4C6C2500A1E726 /* Components */ = { isa = PBXGroup; children = ( + 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */, @@ -3181,7 +3175,6 @@ CDC6A8CA2A6F1C8D00CC11AC /* AssociatedIconProtocol.swift in Sources */, 030E86412AC6F692000283A6 /* SearchBar.swift in Sources */, 50785F762A9A684300117245 /* SavedAccountTracker+Dependency.swift in Sources */, - 632578182A29F83C00446A66 /* PostSortMenu.swift in Sources */, 504ECBAE2AB45B2A006C0B96 /* LemmyURL.swift in Sources */, CDA217EA2A63093E00BDA173 /* ReportComment.swift in Sources */, CDA217E82A63029B00BDA173 /* ReportMention.swift in Sources */, @@ -3455,7 +3448,6 @@ 88B165B82A8643F4007C9115 /* View+NavigationBarColor.swift in Sources */, 030AC0522A64666C00037155 /* UserSettingsView.swift in Sources */, CDA2C5262A705D6000649D5A /* PostEditor.swift in Sources */, - 03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */, E449C5912B2AA8A300E3BCF4 /* AccountDiscussionLanguagesView.swift in Sources */, 6372184A2A3A2AAD008C4816 /* APIPost.swift in Sources */, 6D693A3E2A5113DF009E2D76 /* CreatePostReport.swift in Sources */, diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift deleted file mode 100644 index 9344a17ba..000000000 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// NoPostsView.swift -// Mlem -// -// Created by Sjmarf on 10/10/2023. -// - -import SwiftUI - -struct NoPostsView: View { - @EnvironmentObject var postTracker: PostTracker - - @Binding var isLoading: Bool - @Binding var postSortType: PostSortType - @Binding var showReadPosts: Bool - - var body: some View { - VStack { - if !isLoading { - VStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { - - let unreadItems = postTracker.hiddenItems[.read, default: 0] - - Image(systemName: Icons.noPosts) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: unreadItems == 0 ? 35 : 50) - .padding(.bottom, unreadItems == 0 ? 8: 12) - Text(title) - - if unreadItems != 0 { - Text( - "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." - ) - .foregroundStyle(.tertiary) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, 20) - - } - buttons - } - .foregroundStyle(.secondary) - } - } - } - - var title: String { - if PostSortType.topTypes.contains(postSortType) && postSortType != .topAll { - return "No posts found from the last \(postSortType.label.lowercased())." - } - return "No posts found." - } - - @ViewBuilder - var buttons: some View { - VStack { - if postSortType != .hot { - Button { - isLoading = true - postSortType = .hot - } label: { - Label("Switch to Hot", systemImage: Icons.hotSort) - } - } - if postTracker.hiddenItems[.read, default: 0] > 0 { - Button { - if !showReadPosts { - isLoading = true - showReadPosts = true - } - } label: { - Text("Show read posts") - } - } - } - .foregroundStyle(.secondary) - .buttonStyle(.bordered) - .padding(.top) - .padding(.horizontal, 20) - } -} diff --git a/Mlem/Views/Tabs/Feeds/Components/PostSortMenu.swift b/Mlem/Views/Tabs/Feeds/Components/PostSortMenu.swift deleted file mode 100644 index 2addb68f8..000000000 --- a/Mlem/Views/Tabs/Feeds/Components/PostSortMenu.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Sorting Menu.swift -// Mlem -// -// Created by David Bureš on 02.06.2023. -// - -import SwiftUI - -struct PostSortMenu: View { - @Binding var selectedSortingOption: PostSortType - var shortLabel: Bool = false - - var body: some View { - Menu { - ForEach(PostSortType.outerTypes, id: \.self) { type in - OptionButton( - title: type.label, - imageName: type.iconName, - option: type, - selectedOption: $selectedSortingOption - ) - } - - Menu { - ForEach(PostSortType.topTypes, id: \.self) { type in - OptionButton( - title: type.label, - imageName: type.iconName, - option: type, - selectedOption: $selectedSortingOption - ) - } - - } label: { - Label("Top…", systemImage: Icons.topSortMenu) - } - } label: { - if shortLabel { - HStack { - Spacer() - Image(systemName: selectedSortingOption.iconName) - .tint(.pink) - Text(selectedSortingOption.label) - .tint(.pink) - } - .frame(maxWidth: .infinity) - } else { - Label("Selected sorting by \"\(selectedSortingOption.description)\"", systemImage: selectedSortingOption.iconName) - } - } - } -} - -private struct OptionButton: View { - let title: String - let imageName: String - let option: Option - @Binding var selectedOption: Option - - var body: some View { - Button { - selectedOption = option - } label: { - Label(title, systemImage: imageName) - } - .disabled(option == selectedOption) - } -} diff --git a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/CommunityStatsView.swift similarity index 98% rename from Mlem/Views/Tabs/Feeds/CommunityStatsView.swift rename to Mlem/Views/Tabs/NEW Feeds/Components/CommunityStatsView.swift index d52def8b2..199d7ae5a 100644 --- a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/CommunityStatsView.swift @@ -18,14 +18,12 @@ struct CommunityStatsView: View { Text("\(community.subscriberCount ?? 0)") .fontWeight(.semibold) .font(.title) - } .padding(.vertical) .frame(maxWidth: .infinity) .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(AppConstants.largeItemCornerRadius) HStack(spacing: 16) { - VStack(spacing: 5) { HStack { Text("Posts") @@ -92,7 +90,6 @@ struct CommunityStatsView: View { .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) - } } From 592168016fb6ba77bb89eacd8d8d2b7adaa06ae5 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 11:52:30 -0500 Subject: [PATCH 37/69] refactored community section --- Mlem.xcodeproj/project.pbxproj | 18 ++- .../Community List/CommunityListModel.swift | 36 ++--- .../Community List/CommunityListSection.swift | 17 +++ .../Community List/Community List View.swift | 134 ------------------ .../Components/SectionIndexTitles.swift | 7 +- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 2 +- 6 files changed, 52 insertions(+), 162 deletions(-) rename Mlem/{Views/Tabs/Feeds => Models/Content/Community}/Community List/CommunityListModel.swift (90%) create mode 100644 Mlem/Models/Content/Community/Community List/CommunityListSection.swift delete mode 100644 Mlem/Views/Tabs/Feeds/Community List/Community List View.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 6e0ace25a..f2be62b69 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -274,7 +274,7 @@ 6D7782362A48EED8008AC1BF /* APIPrivateMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D7782352A48EED8008AC1BF /* APIPrivateMessage.swift */; }; 6D8003792A45FD1300363206 /* Bundle+VersionNumbers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8003782A45FD1300363206 /* Bundle+VersionNumbers.swift */; }; 6D80037B2A46458800363206 /* Lazy Load Expanded Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D80037A2A46458800363206 /* Lazy Load Expanded Post.swift */; }; - 6D8F08FF2A4029AE003EB4FD /* Community List View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F08FE2A4029AE003EB4FD /* Community List View.swift */; }; + 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F08FE2A4029AE003EB4FD /* CommunityListSection.swift */; }; 6D91D4552A415994006B8F9A /* CommunityListSidebarEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */; }; 6D91D4582A4159D8006B8F9A /* CommunityListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */; }; 6DA61F812A55B83F001EA633 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA61F802A55B83F001EA633 /* SearchView.swift */; }; @@ -819,7 +819,7 @@ 6D7782352A48EED8008AC1BF /* APIPrivateMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPrivateMessage.swift; sourceTree = ""; }; 6D8003782A45FD1300363206 /* Bundle+VersionNumbers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+VersionNumbers.swift"; sourceTree = ""; }; 6D80037A2A46458800363206 /* Lazy Load Expanded Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Lazy Load Expanded Post.swift"; sourceTree = ""; }; - 6D8F08FE2A4029AE003EB4FD /* Community List View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Community List View.swift"; sourceTree = ""; }; + 6D8F08FE2A4029AE003EB4FD /* CommunityListSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunityListSection.swift; sourceTree = ""; }; 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListSidebarEntry.swift; sourceTree = ""; }; 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListRowViews.swift; sourceTree = ""; }; 6DA61F802A55B83F001EA633 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; @@ -1341,6 +1341,7 @@ 03FD64FD2AE538C600957AA9 /* Community */ = { isa = PBXGroup; children = ( + CD62D12D2B5ED48000395BD9 /* Community List */, 03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */, 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */, 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */, @@ -1640,8 +1641,6 @@ 6332FDD427F080FA0009A98A /* Community List */ = { isa = PBXGroup; children = ( - 6D8F08FE2A4029AE003EB4FD /* Community List View.swift */, - 505240E42A86E32700EA4558 /* CommunityListModel.swift */, 6D91D4532A41597B006B8F9A /* Components */, ); path = "Community List"; @@ -2494,6 +2493,15 @@ path = Links; sourceTree = ""; }; + CD62D12D2B5ED48000395BD9 /* Community List */ = { + isa = PBXGroup; + children = ( + 6D8F08FE2A4029AE003EB4FD /* CommunityListSection.swift */, + 505240E42A86E32700EA4558 /* CommunityListModel.swift */, + ); + path = "Community List"; + sourceTree = ""; + }; CD64832B2A38CE4200EE6CA3 /* Settings */ = { isa = PBXGroup; children = ( @@ -3397,7 +3405,7 @@ 03EF1D0C2B434CB10056175C /* CommunityStatsView.swift in Sources */, 6363D5C727EE196700E34822 /* ContentView.swift in Sources */, 03F4DC9D2B193F4C00556C67 /* MatrixLinkView.swift in Sources */, - 6D8F08FF2A4029AE003EB4FD /* Community List View.swift in Sources */, + 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */, 035EB0CA2A8687C200227859 /* JumpButtonView.swift in Sources */, 5016A2B12A67EB8600B257E8 /* UIViewController+TopMostViewController.swift in Sources */, 6372184C2A3A2AAD008C4816 /* APIPostView.swift in Sources */, diff --git a/Mlem/Views/Tabs/Feeds/Community List/CommunityListModel.swift b/Mlem/Models/Content/Community/Community List/CommunityListModel.swift similarity index 90% rename from Mlem/Views/Tabs/Feeds/Community List/CommunityListModel.swift rename to Mlem/Models/Content/Community/Community List/CommunityListModel.swift index 30a439fe5..ac03883f4 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/CommunityListModel.swift +++ b/Mlem/Models/Content/Community/Community List/CommunityListModel.swift @@ -1,9 +1,9 @@ -// +// // CommunityListModel.swift // Mlem // // Created by mormaer on 11/08/2023. -// +// // import Combine @@ -11,7 +11,6 @@ import Dependencies import Foundation class CommunityListModel: ObservableObject { - @Dependency(\.communityRepository) var communityRepository @Dependency(\.errorHandler) var errorHandler @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker @@ -41,7 +40,7 @@ class CommunityListModel: ObservableObject { // load our subscribed communities let subscriptions = try await communityRepository .loadSubscriptions() - .map { $0.community } + .map(\.community) // load our favourite communities let favorites = favoriteCommunitiesTracker.favoritesForCurrentAccount @@ -69,36 +68,37 @@ class CommunityListModel: ObservableObject { } } - var visibleSections: [CommunitySection] { + var visibleSections: [CommunityListSection] { allSections() - // Only show sections which have labels to show + // Only show sections which have labels to show .filter { communitySection -> Bool in communitySection.inlineHeaderLabel != nil } - // Only show letter headers for letters we have in our community list + // Only show letter headers for letters we have in our community list .filter { communitySection -> Bool in communities .contains(where: { communitySection.sidebarEntry - .contains(community: $0, isSubscribed: isSubscribed(to: $0)) }) + .contains(community: $0, isSubscribed: isSubscribed(to: $0)) + }) } } - func communities(for section: CommunitySection) -> [APICommunity] { + func communities(for section: CommunityListSection) -> [APICommunity] { // Filter down to sidebar entry which wants us - return communities + communities .filter { community -> Bool in section.sidebarEntry.contains(community: community, isSubscribed: isSubscribed(to: community)) } } - func allSections() -> [CommunitySection] { - var sections = [CommunitySection]() + func allSections() -> [CommunityListSection] { + var sections = [CommunityListSection]() sections.append( withDependencies(from: self) { - CommunitySection( + CommunityListSection( viewId: "top", sidebarEntry: EmptySidebarEntry( sidebarLabel: nil, @@ -112,7 +112,7 @@ class CommunityListModel: ObservableObject { sections.append( withDependencies(from: self) { - CommunitySection( + CommunityListSection( viewId: "favorites", sidebarEntry: FavoritesSidebarEntry( sidebarLabel: nil, @@ -128,7 +128,7 @@ class CommunityListModel: ObservableObject { sections.append( withDependencies(from: self) { - CommunitySection( + CommunityListSection( viewId: "non_letter_titles", sidebarEntry: RegexCommunityNameSidebarEntry( communityNameRegex: /^[^a-zA-Z]/, @@ -144,12 +144,12 @@ class CommunityListModel: ObservableObject { return sections } - func alphabeticSections() -> [CommunitySection] { + func alphabeticSections() -> [CommunityListSection] { let alphabet: [String] = .alphabet return alphabet.map { character in withDependencies(from: self) { // This looks sinister but I didn't know how to string replace in a non-string based regex - CommunitySection( + CommunityListSection( viewId: character, sidebarEntry: RegexCommunityNameSidebarEntry( communityNameRegex: (try? Regex("^[\(character.uppercased())\(character.lowercased())]"))!, @@ -218,7 +218,7 @@ class CommunityListModel: ObservableObject { private func combine(_ subscriptions: [APICommunity], _ favorites: [APICommunity]) { // store the values for future use... self.subscriptions = subscriptions - self.favoriteCommunities = favorites + favoriteCommunities = favorites // combine and sort the two lists, excluding duplicates let combined = subscriptions + favorites.filter { !subscriptions.contains($0) } diff --git a/Mlem/Models/Content/Community/Community List/CommunityListSection.swift b/Mlem/Models/Content/Community/Community List/CommunityListSection.swift new file mode 100644 index 000000000..049a3b97b --- /dev/null +++ b/Mlem/Models/Content/Community/Community List/CommunityListSection.swift @@ -0,0 +1,17 @@ +// +// CommunityListSection.swift +// Mlem +// +// Created by Jake Shirey on 17.06.2023. +// + +import Dependencies +import SwiftUI + +struct CommunityListSection: Identifiable { + let id = UUID() + let viewId: String + let sidebarEntry: any SidebarEntry + let inlineHeaderLabel: String? + let accessibilityLabel: String +} diff --git a/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift b/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift deleted file mode 100644 index 2a6d7b979..000000000 --- a/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// Community List View.swift -// Mlem -// -// Created by Jake Shirey on 17.06.2023. -// - -import Dependencies -import SwiftUI - -struct CommunitySection: Identifiable { - let id = UUID() - let viewId: String - let sidebarEntry: any SidebarEntry - let inlineHeaderLabel: String? - let accessibilityLabel: String -} - -struct CommunityListView: View { - @StateObject private var model: CommunityListModel = .init() - - @Binding var selectedCommunity: CommunityLinkWithContext? - - /// Set to `false` on disappear. - @State private var appeared: Bool = false - - init(selectedCommunity: Binding) { - self._selectedCommunity = selectedCommunity - } - - // MARK: - Body - - var body: some View { - ScrollViewReader { scrollProxy in - HStack { - List(selection: $selectedCommunity) { - HomepageFeedRowView(.subscribed) - .padding(.top, 5) - .id("top") // For "scroll to top" sidebar item - HomepageFeedRowView(.local) - HomepageFeedRowView(.all) - - ForEach(model.visibleSections) { section in - Section(header: headerView(for: section)) { - ForEach(model.communities(for: section)) { community in - CommuntiyFeedRowView( - community: community, - subscribed: model.isSubscribed(to: community), - communitySubscriptionChanged: model.updateSubscriptionStatus, - navigationContext: .sidebar - ) - } - } - } - } - .fancyTabScrollCompatible() - .navigationTitle("Communities") - .navigationBarColor() - .listStyle(PlainListStyle()) - .scrollIndicators(.hidden) - .onAppear { - appeared = true - } - .onDisappear { - appeared = false - } - - SectionIndexTitles(proxy: scrollProxy, communitySections: model.allSections()) - } - .reselectAction(tab: .feeds) { - guard appeared else { - return - } - withAnimation { - scrollProxy.scrollTo("top", anchor: .bottom) - } - } - } - .refreshable { - await model.load() - } - .onAppear { - Task(priority: .high) { - await model.load() - } - } - } - - // MARK: - Subviews - - private func headerView(for section: CommunitySection) -> some View { - HStack { - Text(section.inlineHeaderLabel!) - .accessibilityLabel(section.accessibilityLabel) - Spacer() - } - .id(section.viewId) - } -} - -// MARK: - Previews - -struct CommunityListViewPreview: PreviewProvider { - static var previews: some View { - Group { - NavigationStack { - CommunityListView(selectedCommunity: .constant(nil)) - } - .previewDisplayName("Populated") - - NavigationStack { - withDependencies { - // return no subscriptions... - $0.communityRepository.subscriptions = { _ in [] } - } operation: { - CommunityListView(selectedCommunity: .constant(nil)) - } - } - .previewDisplayName("Empty") - - NavigationStack { - withDependencies { - // return an error when calling subscriptions - $0.communityRepository.subscriptions = { _ in - throw APIClientError.response(.init(error: "Borked"), nil) - } - } operation: { - CommunityListView(selectedCommunity: .constant(nil)) - } - } - .previewDisplayName("Error") - } - } -} diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift b/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift index 835e4c01c..d1f61f930 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift @@ -1,9 +1,9 @@ -// +// // SectionIndexTitles.swift // Mlem // // Created by mormaer on 13/08/2023. -// +// // import Dependencies @@ -11,11 +11,10 @@ import SwiftUI // Original article here: https://www.fivestars.blog/code/section-title-index-swiftui.html struct SectionIndexTitles: View { - @Dependency(\.hapticManager) var hapticManager let proxy: ScrollViewProxy - let communitySections: [CommunitySection] + let communitySections: [CommunityListSection] @GestureState private var dragLocation: CGPoint = .zero // Track which sidebar label we picked last to we diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index dcc924681..64471bc89 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -86,7 +86,7 @@ struct FeedsView: View { } } - private func communitySectionHeaderView(for section: CommunitySection) -> some View { + private func communitySectionHeaderView(for section: CommunityListSection) -> some View { HStack { Text(section.inlineHeaderLabel!) .accessibilityLabel(section.accessibilityLabel) From 8e7ada4308f0507bd83aef3fcfe4f7075f7b1d1a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 12:47:29 -0500 Subject: [PATCH 38/69] added scroll thing back to community list --- Mlem.xcodeproj/project.pbxproj | 46 +++++------------ .../CommunityListRowViews.swift | 0 .../CommunityListSidebarEntry.swift | 1 - .../Community List}/SectionIndexTitles.swift | 16 +++--- .../NEW Feeds/Components/FeedRowView.swift | 2 +- Mlem/Views/Tabs/NEW Feeds/FeedsView.swift | 49 +++++++++++-------- 6 files changed, 51 insertions(+), 63 deletions(-) rename Mlem/Views/Tabs/{Feeds/Community List/Components => NEW Feeds/Community List}/CommunityListRowViews.swift (100%) rename Mlem/Views/Tabs/{Feeds/Community List/Components => NEW Feeds/Community List}/CommunityListSidebarEntry.swift (99%) rename Mlem/Views/Tabs/{Feeds/Community List/Components => NEW Feeds/Community List}/SectionIndexTitles.swift (83%) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index f2be62b69..504dcdda6 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -1638,14 +1638,6 @@ path = Accounts; sourceTree = ""; }; - 6332FDD427F080FA0009A98A /* Community List */ = { - isa = PBXGroup; - children = ( - 6D91D4532A41597B006B8F9A /* Components */, - ); - path = "Community List"; - sourceTree = ""; - }; 63344C522A07D189001BC616 /* Views */ = { isa = PBXGroup; children = ( @@ -1669,13 +1661,6 @@ path = Styles; sourceTree = ""; }; - 63344C6F2A098054001BC616 /* Components */ = { - isa = PBXGroup; - children = ( - ); - path = Components; - sourceTree = ""; - }; 6363D5B827EE196700E34822 = { isa = PBXGroup; children = ( @@ -1781,7 +1766,6 @@ isa = PBXGroup; children = ( CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */, - CD2E14782A6B283D004198DE /* Feeds */, 6DA61F7F2A55B831001EA633 /* Search */, 6DE1183A2A4A215F00810C7E /* Profile */, 6DFF50412A48DEC0001E648D /* Inbox */, @@ -2072,16 +2056,6 @@ path = Enums; sourceTree = ""; }; - 6D91D4532A41597B006B8F9A /* Components */ = { - isa = PBXGroup; - children = ( - 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */, - 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */, - 505240E62A88D36D00EA4558 /* SectionIndexTitles.swift */, - ); - path = Components; - sourceTree = ""; - }; 6DA61F7F2A55B831001EA633 /* Search */ = { isa = PBXGroup; children = ( @@ -2358,15 +2332,6 @@ path = TimeInterval; sourceTree = ""; }; - CD2E14782A6B283D004198DE /* Feeds */ = { - isa = PBXGroup; - children = ( - 6332FDD427F080FA0009A98A /* Community List */, - 63344C6F2A098054001BC616 /* Components */, - ); - path = Feeds; - sourceTree = ""; - }; CD2E14792A6B285F004198DE /* Posts */ = { isa = PBXGroup; children = ( @@ -2463,6 +2428,7 @@ CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */ = { isa = PBXGroup; children = ( + CD62D12F2B5EE18300395BD9 /* Community List */, CDEC95172B5D8D06004BA288 /* Feed Types */, CD4BAD392B4C6C2500A1E726 /* Components */, CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, @@ -2502,6 +2468,16 @@ path = "Community List"; sourceTree = ""; }; + CD62D12F2B5EE18300395BD9 /* Community List */ = { + isa = PBXGroup; + children = ( + 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */, + 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */, + 505240E62A88D36D00EA4558 /* SectionIndexTitles.swift */, + ); + path = "Community List"; + sourceTree = ""; + }; CD64832B2A38CE4200EE6CA3 /* Settings */ = { isa = PBXGroup; children = ( diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift b/Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListRowViews.swift similarity index 100% rename from Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift rename to Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListRowViews.swift diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListSidebarEntry.swift b/Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListSidebarEntry.swift similarity index 99% rename from Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListSidebarEntry.swift rename to Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListSidebarEntry.swift index b146d01c8..f245b059b 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListSidebarEntry.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListSidebarEntry.swift @@ -42,7 +42,6 @@ struct RegexCommunityNameSidebarEntry: SidebarEntry { // Filters to favorited communities struct FavoritesSidebarEntry: SidebarEntry { - @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker var sidebarLabel: String? diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift b/Mlem/Views/Tabs/NEW Feeds/Community List/SectionIndexTitles.swift similarity index 83% rename from Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift rename to Mlem/Views/Tabs/NEW Feeds/Community List/SectionIndexTitles.swift index d1f61f930..5380799e1 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Components/SectionIndexTitles.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Community List/SectionIndexTitles.swift @@ -25,12 +25,10 @@ struct SectionIndexTitles: View { VStack { ForEach(communitySections) { communitySection in HStack { - if communitySection.sidebarEntry.sidebarIcon != nil { - SectionIndexImage(image: communitySection.sidebarEntry.sidebarIcon!) - .padding(.trailing) - } else if communitySection.sidebarEntry.sidebarLabel != nil { - SectionIndexText(label: communitySection.sidebarEntry.sidebarLabel!) - .padding(.trailing) + if let icon = communitySection.sidebarEntry.sidebarIcon { + SectionIndexImage(image: icon) + } else if let label = communitySection.sidebarEntry.sidebarLabel { + SectionIndexText(label: label) } else { EmptyView() } @@ -38,6 +36,12 @@ struct SectionIndexTitles: View { .background(dragObserver(viewId: communitySection.viewId)) } } + .padding(5) + .padding(.top, 7) // top looks a little funky otherwise + .background { + Capsule() + .foregroundStyle(.regularMaterial) + } .gesture( DragGesture(minimumDistance: 0, coordinateSpace: .global) .updating($dragLocation) { value, state, _ in diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift b/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift index bd89f24bb..64c18e3d3 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift @@ -46,7 +46,7 @@ struct CommunityFeedRowView: View { } .buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) .accessibilityHidden(true) - }.swipeActions { + }.swipeActions(edge: .leading) { if subscribed { Button("Unsubscribe") { Task(priority: .userInitiated) { diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift index 64471bc89..9aecda2c9 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift @@ -22,6 +22,7 @@ struct FeedsView: View { var body: some View { content + // .navigationTitle("Communities") .onAppear { Task(priority: .high) { await communityListModel.load() @@ -35,32 +36,40 @@ struct FeedsView: View { } var content: some View { - ScrollViewReader { _ in + ScrollViewReader { scrollProxy in NavigationSplitView { - // Note on navigation: nesting List(selection: $selectedFeed) inside a NavigationSplitView here automagically sets up navigation so that nav links inside this block update selectedFeed, which is then handled by the switch in the detail. This can also be achieved by defining .navigationDestinations on the List; those will then propagate to the detail when selected, but due to the amount of manual navigation stuff we're doing this approach seems less troublesome [Eric 2023.01.11] - List(selection: $selectedFeed) { - ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in - // These are automagically turned into NavigationLinks - NavigationLink(value: feedType) { - FeedRowView(feedType: feedType) + // Note that NavigationLinks in here update selectedFeed and are handled by the detail switch, not the general navigation handler + ZStack(alignment: .trailing) { + List(selection: $selectedFeed) { + ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in + NavigationLink(value: feedType) { + FeedRowView(feedType: feedType) + } } - } - - ForEach(communityListModel.visibleSections) { section in - Section(header: communitySectionHeaderView(for: section)) { - ForEach(communityListModel.communities(for: section)) { community in - // These are not automagically turned into NavigationLinks, so we do it manually - NavigationLink(value: NewFeedType.community(.init(from: community, subscribed: true))) { - CommunityFeedRowView( - community: community, - subscribed: communityListModel.isSubscribed(to: community), - communitySubscriptionChanged: communityListModel.updateSubscriptionStatus, - navigationContext: .sidebar - ) + // .padding(.trailing, 22) + + ForEach(communityListModel.visibleSections) { section in + Section(header: communitySectionHeaderView(for: section)) { + ForEach(communityListModel.communities(for: section)) { community in + NavigationLink(value: NewFeedType.community(.init(from: community, subscribed: true))) { + CommunityFeedRowView( + community: community, + subscribed: communityListModel.isSubscribed(to: community), + communitySubscriptionChanged: communityListModel.updateSubscriptionStatus, + navigationContext: .sidebar + ) + } } } } + // .padding(.trailing, 22) } + .scrollIndicators(.hidden) + .navigationTitle("Communities") + .listStyle(PlainListStyle()) + + SectionIndexTitles(proxy: scrollProxy, communitySections: communityListModel.allSections()) + .padding(.trailing, 7) } } detail: { NavigationStack(path: $feedTabNavigation.path) { From 8dea7cc1e882d9ee1ac611b2b19885b8b020a28e Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 12:51:52 -0500 Subject: [PATCH 39/69] refactored names --- Mlem.xcodeproj/project.pbxproj | 46 +++++++++---------- .../View+HandleLemmyLinks.swift | 2 +- .../CommunityListRowViews.swift | 0 .../CommunityListSidebarEntry.swift | 0 .../Community List/SectionIndexTitles.swift | 0 .../Components/CommunityStatsView.swift | 0 .../Components/FeedRowView.swift | 0 .../Components/NoPostsView.swift} | 2 +- .../Components/PostFeedView+Logic.swift} | 4 +- .../PostFeedView+MenuFunctions.swift} | 2 +- .../Components/PostFeedView.swift} | 6 +-- .../Feed Types/AggregateFeedView+Logic.swift | 0 .../Feed Types/AggregateFeedView.swift | 2 +- .../Feed Types/CommunityFeedView.swift} | 6 +-- .../Feed Types/SavedFeedView.swift | 0 .../Tabs/{NEW Feeds => Feeds}/FeedsView.swift | 2 +- 16 files changed, 36 insertions(+), 36 deletions(-) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Community List/CommunityListRowViews.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Community List/CommunityListSidebarEntry.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Community List/SectionIndexTitles.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Components/CommunityStatsView.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Components/FeedRowView.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds/Components/NEW NoPostsView.swift => Feeds/Components/NoPostsView.swift} (98%) rename Mlem/Views/Tabs/{NEW Feeds/Components/NEW PostFeedView+Logic.swift => Feeds/Components/PostFeedView+Logic.swift} (92%) rename Mlem/Views/Tabs/{NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift => Feeds/Components/PostFeedView+MenuFunctions.swift} (98%) rename Mlem/Views/Tabs/{NEW Feeds/Components/NEW PostFeedView.swift => Feeds/Components/PostFeedView.swift} (96%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Feed Types/AggregateFeedView+Logic.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Feed Types/AggregateFeedView.swift (98%) rename Mlem/Views/Tabs/{NEW Feeds/Feed Types/NEW CommunityView.swift => Feeds/Feed Types/CommunityFeedView.swift} (98%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/Feed Types/SavedFeedView.swift (100%) rename Mlem/Views/Tabs/{NEW Feeds => Feeds}/FeedsView.swift (98%) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 504dcdda6..ac61a0a53 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -445,8 +445,8 @@ CDB45C602AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C5F2AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift */; }; CDB45C622AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */; }; CDB45C642AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */; }; - CDBCBA202B537A4B0070F60D /* NEW PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */; }; - CDBCBA242B54A5F40070F60D /* NEW NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */; }; + CDBCBA202B537A4B0070F60D /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA1F2B537A4B0070F60D /* PostFeedView.swift */; }; + CDBCBA242B54A5F40070F60D /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBCBA232B54A5F40070F60D /* NoPostsView.swift */; }; CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */; }; CDC1C93F2A7AB8C700072E3D /* AccessibilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */; }; CDC1C9412A7ABA9C00072E3D /* ReadMarkStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */; }; @@ -455,7 +455,7 @@ CDC65D8F2A86B6DD007205E5 /* DeleteUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC65D8E2A86B6DD007205E5 /* DeleteUser.swift */; }; CDC65D912A86B830007205E5 /* DeleteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC65D902A86B830007205E5 /* DeleteAccountView.swift */; }; CDC6A8CA2A6F1C8D00CC11AC /* AssociatedIconProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6A8C92A6F1C8D00CC11AC /* AssociatedIconProtocol.swift */; }; - CDCA28D42B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */; }; + CDCA28D42B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA28D32B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift */; }; CDCBD7242A8D62FF00387A2C /* InstanceMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7232A8D62FF00387A2C /* InstanceMetadata.swift */; }; CDCBD7262A8D69A200387A2C /* Instance Picker View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7252A8D69A200387A2C /* Instance Picker View.swift */; }; CDCBD7282A8D6B7700387A2C /* Instance Picker View Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCBD7272A8D6B7700387A2C /* Instance Picker View Logic.swift */; }; @@ -494,10 +494,10 @@ CDEBC32C2A9A582500518D9D /* Votes Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32B2A9A582500518D9D /* Votes Model.swift */; }; CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32D2A9A583900518D9D /* Post Tracker.swift */; }; CDEBC3392A9ADE6C00518D9D /* APIClient+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */; }; - CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */; }; + CDEC95122B5B318B004BA288 /* CommunityFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* CommunityFeedView.swift */; }; CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */; }; CDEC95162B5D8C05004BA288 /* SavedFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */; }; - CDEC95192B5D950D004BA288 /* NEW PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */; }; + CDEC95192B5D950D004BA288 /* PostFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95182B5D950D004BA288 /* PostFeedView+Logic.swift */; }; CDF1EF162A6C3BC2003594B6 /* End Of Feed View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */; }; CDF1EF182A6C40C9003594B6 /* Menu Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1EF172A6C40C9003594B6 /* Menu Button.swift */; }; CDF8425C2A49E4C000723DA0 /* APIPersonMentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */; }; @@ -987,8 +987,8 @@ CDB45C5F2AF1AF4900A1FF08 /* MentionModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MentionModel+TrackerItem.swift"; sourceTree = ""; }; CDB45C612AF1AF9B00A1FF08 /* ReplyModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReplyModel+TrackerItem.swift"; sourceTree = ""; }; CDB45C632AF1AFB900A1FF08 /* MessageModel+TrackerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageModel+TrackerItem.swift"; sourceTree = ""; }; - CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView.swift"; sourceTree = ""; }; - CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW NoPostsView.swift"; sourceTree = ""; }; + CDBCBA1F2B537A4B0070F60D /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = ""; }; + CDBCBA232B54A5F40070F60D /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = ""; }; CDC1C93B2A7AA76000072E3D /* InternetSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetSpeed.swift; sourceTree = ""; }; CDC1C93E2A7AB8C700072E3D /* AccessibilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilitySettingsView.swift; sourceTree = ""; }; CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkStyle.swift; sourceTree = ""; }; @@ -997,7 +997,7 @@ CDC65D8E2A86B6DD007205E5 /* DeleteUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteUser.swift; sourceTree = ""; }; CDC65D902A86B830007205E5 /* DeleteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountView.swift; sourceTree = ""; }; CDC6A8C92A6F1C8D00CC11AC /* AssociatedIconProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedIconProtocol.swift; sourceTree = ""; }; - CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; + CDCA28D32B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+MenuFunctions.swift"; sourceTree = ""; }; CDCBD7232A8D62FF00387A2C /* InstanceMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceMetadata.swift; sourceTree = ""; }; CDCBD7252A8D69A200387A2C /* Instance Picker View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance Picker View.swift"; sourceTree = ""; }; CDCBD7272A8D6B7700387A2C /* Instance Picker View Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance Picker View Logic.swift"; sourceTree = ""; }; @@ -1036,10 +1036,10 @@ CDEBC32B2A9A582500518D9D /* Votes Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Votes Model.swift"; sourceTree = ""; }; CDEBC32D2A9A583900518D9D /* Post Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Tracker.swift"; sourceTree = ""; }; CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIClient+Post.swift"; sourceTree = ""; }; - CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW CommunityView.swift"; sourceTree = ""; }; + CDEC95112B5B318B004BA288 /* CommunityFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityFeedView.swift; sourceTree = ""; }; CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AggregateFeedView+Logic.swift"; sourceTree = ""; }; CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedFeedView.swift; sourceTree = ""; }; - CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW PostFeedView+Logic.swift"; sourceTree = ""; }; + CDEC95182B5D950D004BA288 /* PostFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostFeedView+Logic.swift"; sourceTree = ""; }; CDF1EF152A6C3BC2003594B6 /* End Of Feed View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "End Of Feed View.swift"; sourceTree = ""; }; CDF1EF172A6C40C9003594B6 /* Menu Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Menu Button.swift"; sourceTree = ""; }; CDF8425B2A49E4C000723DA0 /* APIPersonMentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPersonMentionView.swift; sourceTree = ""; }; @@ -1765,7 +1765,7 @@ 6363D5F427EE1BAE00E34822 /* Tabs */ = { isa = PBXGroup; children = ( - CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */, + CD4BAD382B4C6C1B00A1E726 /* Feeds */, 6DA61F7F2A55B831001EA633 /* Search */, 6DE1183A2A4A215F00810C7E /* Profile */, 6DFF50412A48DEC0001E648D /* Inbox */, @@ -2425,7 +2425,7 @@ path = "Tracker Items"; sourceTree = ""; }; - CD4BAD382B4C6C1B00A1E726 /* NEW Feeds */ = { + CD4BAD382B4C6C1B00A1E726 /* Feeds */ = { isa = PBXGroup; children = ( CD62D12F2B5EE18300395BD9 /* Community List */, @@ -2433,7 +2433,7 @@ CD4BAD392B4C6C2500A1E726 /* Components */, CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */, ); - path = "NEW Feeds"; + path = Feeds; sourceTree = ""; }; CD4BAD392B4C6C2500A1E726 /* Components */ = { @@ -2441,10 +2441,10 @@ children = ( 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, - CDBCBA1F2B537A4B0070F60D /* NEW PostFeedView.swift */, - CDBCBA232B54A5F40070F60D /* NEW NoPostsView.swift */, - CDCA28D32B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift */, - CDEC95182B5D950D004BA288 /* NEW PostFeedView+Logic.swift */, + CDBCBA1F2B537A4B0070F60D /* PostFeedView.swift */, + CDBCBA232B54A5F40070F60D /* NoPostsView.swift */, + CDCA28D32B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift */, + CDEC95182B5D950D004BA288 /* PostFeedView+Logic.swift */, ); path = Components; sourceTree = ""; @@ -2718,7 +2718,7 @@ CDEC95172B5D8D06004BA288 /* Feed Types */ = { isa = PBXGroup; children = ( - CDEC95112B5B318B004BA288 /* NEW CommunityView.swift */, + CDEC95112B5B318B004BA288 /* CommunityFeedView.swift */, CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */, CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */, CDEC95152B5D8C05004BA288 /* SavedFeedView.swift */, @@ -3119,7 +3119,7 @@ CDB45C5E2AF1A96C00A1FF08 /* AssociatedColorProtocol.swift in Sources */, CD3FBCE92A4B482700B2063F /* Generic Merge.swift in Sources */, E47B2B762A902DE200629AF7 /* SettingsValues.swift in Sources */, - CDBCBA242B54A5F40070F60D /* NEW NoPostsView.swift in Sources */, + CDBCBA242B54A5F40070F60D /* NoPostsView.swift in Sources */, CDA145ED2A510AC100DDAFC9 /* MarkCommentReplyAsReadRequest.swift in Sources */, CD391F982A537E8E00E213B5 /* ReplyToComment.swift in Sources */, 5064D03D2A6DE0AA00B22EE3 /* Notifier.swift in Sources */, @@ -3198,7 +3198,7 @@ 6D7782362A48EED8008AC1BF /* APIPrivateMessage.swift in Sources */, CDE3BA892A8C64BD00B972E2 /* Collapsible Text Item.swift in Sources */, 50CC4A742A9CB10B0074C845 /* TimestampedValue.swift in Sources */, - CDCA28D42B58AF53009D9F54 /* NEW PostFeedView+MenuFunctions.swift in Sources */, + CDCA28D42B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift in Sources */, 505240E72A88D36D00EA4558 /* SectionIndexTitles.swift in Sources */, 5064D0452A71549C00B22EE3 /* NotificationMessage.swift in Sources */, E4F0B56F2ABD00A000BC3E4A /* View+PresentationBackgroundInteraction.swift in Sources */, @@ -3231,7 +3231,7 @@ CD29ED3B2B2E8624006937CE /* String+IsNotEmpty.swift in Sources */, CD391F9A2A537EF900E213B5 /* CommentBodyView.swift in Sources */, 63344C562A07D81D001BC616 /* Array+Prepend.swift in Sources */, - CDBCBA202B537A4B0070F60D /* NEW PostFeedView.swift in Sources */, + CDBCBA202B537A4B0070F60D /* PostFeedView.swift in Sources */, CDDCF64F2A672C0A003DA3AC /* FancyTabBarLabel.swift in Sources */, CD04D5D92A3614BE008EF95B /* Large Post.swift in Sources */, CDF8425E2A49E61A00723DA0 /* APIPersonMention.swift in Sources */, @@ -3241,7 +3241,7 @@ CD863FBC2A6B026400A31ED9 /* DocumentView.swift in Sources */, CD8461662A96F9EB0026A627 /* Website Indicator View.swift in Sources */, 038A16E92A7A9C640087987E /* LayoutWidget.swift in Sources */, - CDEC95192B5D950D004BA288 /* NEW PostFeedView+Logic.swift in Sources */, + CDEC95192B5D950D004BA288 /* PostFeedView+Logic.swift in Sources */, CD9A03C82B389F7000C16276 /* EnvironmentValues+FeedType.swift in Sources */, 50811B2E2A92046D006BA3F2 /* URL+Mock.swift in Sources */, 03C905CC2B3C88F700B9082F /* SearchTab.swift in Sources */, @@ -3419,7 +3419,7 @@ 039C8DB92B35A81C0096BAAF /* AccountIconStack.swift in Sources */, CDCBD7262A8D69A200387A2C /* Instance Picker View.swift in Sources */, 03C905CE2B3C8DC400B9082F /* UserView+Logic.swift in Sources */, - CDEC95122B5B318B004BA288 /* NEW CommunityView.swift in Sources */, + CDEC95122B5B318B004BA288 /* CommunityFeedView.swift in Sources */, 6372185B2A3A2AAD008C4816 /* APICommunityView.swift in Sources */, CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */, 030E86442AC6F6D5000283A6 /* SearchBar+NavigationView.swift in Sources */, diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index 98508620d..e53396b17 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -29,7 +29,7 @@ struct HandleLemmyLinksDisplay: ViewModifier { .navigationDestination(for: AppRoute.self) { route in switch route { case let .community(community): - NewCommunityFeedView(communityModel: community) + CommunityFeedView(communityModel: community) .environmentObject(appState) .environmentObject(filtersTracker) .environmentObject(quickLookState) diff --git a/Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListRowViews.swift b/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListRowViews.swift rename to Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListSidebarEntry.swift b/Mlem/Views/Tabs/Feeds/Community List/CommunityListSidebarEntry.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Community List/CommunityListSidebarEntry.swift rename to Mlem/Views/Tabs/Feeds/Community List/CommunityListSidebarEntry.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Community List/SectionIndexTitles.swift b/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Community List/SectionIndexTitles.swift rename to Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/CommunityStatsView.swift b/Mlem/Views/Tabs/Feeds/Components/CommunityStatsView.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Components/CommunityStatsView.swift rename to Mlem/Views/Tabs/Feeds/Components/CommunityStatsView.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift b/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Components/FeedRowView.swift rename to Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift rename to Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index 6e19a9128..8e741bc03 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -7,7 +7,7 @@ import SwiftUI -struct NewNoPostsView: View { +struct NoPostsView: View { @EnvironmentObject var postTracker: StandardPostTracker let loadingState: LoadingState diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift similarity index 92% rename from Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+Logic.swift rename to Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index bce8ff85b..86c93081a 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -1,5 +1,5 @@ // -// NEW PostFeedView+Logic.swift +// PostFeedView+Logic.swift // Mlem // // Created by Eric Andrews on 2024-01-21. @@ -8,7 +8,7 @@ import Dependencies import SwiftUI -extension NewPostFeedView { +extension PostFeedView { func setDefaultSortMode() { @Dependency(\.siteInformation) var siteInformationn diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+MenuFunctions.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift rename to Mlem/Views/Tabs/Feeds/Components/PostFeedView+MenuFunctions.swift index 81febfc41..e8e3cdc71 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView+MenuFunctions.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+MenuFunctions.swift @@ -7,7 +7,7 @@ import Foundation -extension NewPostFeedView { +extension PostFeedView { func genOuterSortMenuFunctions() -> [MenuFunction] { PostSortType.availableOuterTypes.map { type in let isSelected = postSortType == type diff --git a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift similarity index 96% rename from Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift rename to Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 727fa9a91..487b1991e 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Components/NEW PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -1,5 +1,5 @@ // -// NEW PostFeedView.swift +// PostFeedView.swift // Mlem // // Created by Eric Andrews on 2024-01-13. @@ -9,7 +9,7 @@ import Dependencies import Foundation import SwiftUI -struct NewPostFeedView: View { +struct PostFeedView: View { @Dependency(\.errorHandler) var errorHandler @Dependency(\.siteInformation) var siteInformation @@ -115,7 +115,7 @@ struct NewPostFeedView: View { ErrorView(errorDetails) .frame(maxWidth: .infinity) } else { - NewNoPostsView(loadingState: postTracker.loadingState, postSortType: $postSortType, showReadPosts: $showReadPosts) + NoPostsView(loadingState: postTracker.loadingState, postSortType: $postSortType, showReadPosts: $showReadPosts) .transition(.scale(scale: 0.9).combined(with: .opacity)) .padding(.top, 25) } diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView+Logic.swift rename to Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift rename to Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift index d22a8c31e..e6346c1c7 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Feed Types/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift @@ -96,7 +96,7 @@ struct AggregateFeedView: View { .padding(.top, -1) } - NewPostFeedView(postSortType: $postSortType, showCommunity: true) + PostFeedView(postSortType: $postSortType, showCommunity: true) .environmentObject(postTracker) } } diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift rename to Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index cc05ee585..bd173471b 100644 --- a/Mlem/Views/Tabs/NEW Feeds/Feed Types/NEW CommunityView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -1,5 +1,5 @@ // -// NEW CommunityView.swift +// CommunityFeedView.swift // Mlem // // Created by Eric Andrews on 2024-01-19. @@ -10,7 +10,7 @@ import Foundation import SwiftUI /// View for a single community -struct NewCommunityFeedView: View { +struct CommunityFeedView: View { enum Tab: String, Identifiable, CaseIterable { var id: Self { self } case posts, about, moderators, statistics @@ -165,7 +165,7 @@ struct NewCommunityFeedView: View { } func posts() -> some View { - NewPostFeedView(postSortType: $postSortType, showCommunity: false) + PostFeedView(postSortType: $postSortType, showCommunity: false) .environmentObject(postTracker) } diff --git a/Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift similarity index 100% rename from Mlem/Views/Tabs/NEW Feeds/Feed Types/SavedFeedView.swift rename to Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift diff --git a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift similarity index 98% rename from Mlem/Views/Tabs/NEW Feeds/FeedsView.swift rename to Mlem/Views/Tabs/Feeds/FeedsView.swift index 9aecda2c9..99e088b34 100644 --- a/Mlem/Views/Tabs/NEW Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -84,7 +84,7 @@ struct FeedsView: View { case .saved: SavedFeedView() case let .community(communityModel): - NewCommunityFeedView(communityModel: communityModel) + CommunityFeedView(communityModel: communityModel) case .none: Text("Please select a feed") } From 977f78299feb3b2a6710b63365130ad9ed36bc66 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 13:12:39 -0500 Subject: [PATCH 40/69] removed old post tracker --- Mlem.xcodeproj/project.pbxproj | 4 - .../View+HandleLemmyLinks.swift | 17 - Mlem/Models/Trackers/Post Tracker.swift | 541 ------------------ Mlem/Navigation/Routes/AppRoutes.swift | 16 - .../Comments/Components/Embedded Post.swift | 6 - Mlem/Views/Shared/Posts/Expanded Post.swift | 2 +- .../Shared/Posts/ExpandedPostLogic.swift | 4 +- .../Posts/Lazy Load Expanded Post.swift | 7 +- .../Posts/Post Sizes/Compact Post.swift | 2 - Mlem/Views/Tabs/Inbox/Inbox View.swift | 3 - 10 files changed, 7 insertions(+), 595 deletions(-) delete mode 100644 Mlem/Models/Trackers/Post Tracker.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index ac61a0a53..17d914fdf 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -492,7 +492,6 @@ CDEBC3282A9A57F200518D9D /* Content Model Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */; }; CDEBC32A2A9A580B00518D9D /* Post Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3292A9A580B00518D9D /* Post Model.swift */; }; CDEBC32C2A9A582500518D9D /* Votes Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32B2A9A582500518D9D /* Votes Model.swift */; }; - CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC32D2A9A583900518D9D /* Post Tracker.swift */; }; CDEBC3392A9ADE6C00518D9D /* APIClient+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */; }; CDEC95122B5B318B004BA288 /* CommunityFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95112B5B318B004BA288 /* CommunityFeedView.swift */; }; CDEC95142B5CBC42004BA288 /* AggregateFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */; }; @@ -1034,7 +1033,6 @@ CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content Model Identifier.swift"; sourceTree = ""; }; CDEBC3292A9A580B00518D9D /* Post Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Model.swift"; sourceTree = ""; }; CDEBC32B2A9A582500518D9D /* Votes Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Votes Model.swift"; sourceTree = ""; }; - CDEBC32D2A9A583900518D9D /* Post Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post Tracker.swift"; sourceTree = ""; }; CDEBC3382A9ADE6C00518D9D /* APIClient+Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIClient+Post.swift"; sourceTree = ""; }; CDEC95112B5B318B004BA288 /* CommunityFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityFeedView.swift; sourceTree = ""; }; CDEC95132B5CBC42004BA288 /* AggregateFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AggregateFeedView+Logic.swift"; sourceTree = ""; }; @@ -1994,7 +1992,6 @@ B1955A1E2A606F010056CF99 /* EasterFlagsTracker.swift */, CDB0117E2A6F70A000D043EB /* Editor Tracker.swift */, 50785F702A98C4F600117245 /* SiteInformationTracker.swift */, - CDEBC32D2A9A583900518D9D /* Post Tracker.swift */, 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */, ); path = Trackers; @@ -3045,7 +3042,6 @@ 5064D0432A6E645D00B22EE3 /* Notifiable.swift in Sources */, 039439932A99098900463032 /* InternetConnectionManager.swift in Sources */, CD82A2592A71775E00111034 /* UnreadTracker.swift in Sources */, - CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */, CD4368CA2AE2428C00BD8BD1 /* ContentIdentifiable.swift in Sources */, CD3FBCE32A4A844800B2063F /* Replies Feed View.swift in Sources */, 637218652A3A2AAD008C4816 /* GetPosts.swift in Sources */, diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index e53396b17..fab2eacb5 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -33,23 +33,6 @@ struct HandleLemmyLinksDisplay: ViewModifier { .environmentObject(appState) .environmentObject(filtersTracker) .environmentObject(quickLookState) - case let .apiPostView(post): - let postModel = PostModel(from: post) - let postTracker = PostTracker( - shouldPerformMergeSorting: false, - internetSpeed: internetSpeed, - initialItems: [postModel], - upvoteOnSave: upvoteOnSave - ) - // swiftlint:disable:next redundant_discardable_let - let _ = postTracker.add([postModel]) - ExpandedPost(post: postModel) - .environmentObject(postTracker) - .environmentObject(appState) - .environmentObject(quickLookState) - case let .apiPost(post): - LazyLoadExpandedPost(post: post) - .environmentObject(quickLookState) case let .apiPerson(user): UserView(user: UserModel(from: user)) .environmentObject(appState) diff --git a/Mlem/Models/Trackers/Post Tracker.swift b/Mlem/Models/Trackers/Post Tracker.swift deleted file mode 100644 index 334c3ab75..000000000 --- a/Mlem/Models/Trackers/Post Tracker.swift +++ /dev/null @@ -1,541 +0,0 @@ -// -// Post Tracker.swift -// Mlem -// -// Created by Eric Andrews on 2023-08-26. -// - -import Dependencies -import Foundation -import Nuke -import SwiftUI - -enum PostFilterReason { - case read, keyword -} - -// swiftlint:disable type_body_length -// swiftlint:disable file_length -/// New post tracker built on top of the PostRepository instead of calling the API directly. Because this thing works fundamentally differently from the old one, it can't conform to FeedTracker--that's going to need a revamp down the line once everything uses nice shiny middleware models, so for now we're going to have to put up with some ugly -class PostTracker: ObservableObject { - // dependencies - @Dependency(\.postRepository) var postRepository - @Dependency(\.apiClient) var apiClient - @Dependency(\.hapticManager) var hapticManager - @Dependency(\.errorHandler) var errorHandler - - enum LoaderType: Equatable { - case feed(FeedType, sortedBy: PostSortType) - case community(CommunityModel, sortedBy: PostSortType) - } - - enum PostTrackerError: Error { - case notConfiguredForPageLoading - } - - // behavior governors - private let shouldPerformMergeSorting: Bool - private let internetSpeed: InternetSpeed - private let upvoteOnSave: Bool - - // state drivers - @Published private(set) var items: [PostModel] - @Published var type: LoaderType? - @Published var showLoadingIcon: Bool = true - - // utility - private var ids: Set = .init(minimumCapacity: 1000) - private(set) var isLoading: Bool = false // accessible but not published because it causes lots of bad view redraws - private(set) var page: Int = 1 - private(set) var hiddenItems: [PostFilterReason: Int] = .init() - private(set) var currentCursor: String? - - private var hasReachedEnd: Bool = false - - var filter: (PostModel) -> PostFilterReason? - var handleError: ((Error) -> Void)! - - // prefetching - private let prefetcher = ImagePrefetcher( - pipeline: ImagePipeline.shared, - destination: .memoryCache, - maxConcurrentRequestCount: 40 - ) - - init( - shouldPerformMergeSorting: Bool = true, - internetSpeed: InternetSpeed, - initialItems: [PostModel] = .init(), - upvoteOnSave: Bool, - type: LoaderType? = nil, - filter: @escaping (PostModel) -> PostFilterReason? = { _ in nil }, - handleError: ((Error) -> Void)? = nil - ) { - self.shouldPerformMergeSorting = shouldPerformMergeSorting - self.internetSpeed = internetSpeed - self.items = initialItems - self.upvoteOnSave = upvoteOnSave - self.type = type - self.filter = filter - self.handleError = handleError - if self.handleError == nil { - self.handleError = { error in self.errorHandler.handle(error) } - } - } - - // MARK: - Loading Methods - - // TODO: ERIC handle loading state properly - - func getNextPageFromRepository() async throws -> (posts: [PostModel], cursor: String?) { - switch type { - case let .feed(feedType, postSortType): - return try await postRepository.loadPage( - communityId: nil, - page: page, - cursor: currentCursor, - sort: postSortType, - type: feedType, - limit: internetSpeed.pageSize - ) - case let .community(community, postSortType): - return try await postRepository.loadPage( - communityId: community.communityId, - page: page, - cursor: currentCursor, - sort: postSortType, - type: FeedType.subscribed, - limit: internetSpeed.pageSize - ) - case nil: - throw PostTrackerError.notConfiguredForPageLoading - } - } - - func loadNextPage() async { - defer { DispatchQueue.main.async { self.showLoadingIcon = false } } - DispatchQueue.main.async { self.showLoadingIcon = true } - do { - let currentPage = page - - // retry this until we get enough items through the filter to enable autoload - var newPosts: [PostModel] = .init() - let numItems = items.count - repeat { - let (posts, cursor) = try await getNextPageFromRepository() - newPosts = posts - - if newPosts.isEmpty { - hasReachedEnd = true - } else if let currentCursor, cursor == currentCursor { - hasReachedEnd = true - } else { - await add(newPosts) - page += 1 - currentCursor = cursor - } - } while !hasReachedEnd && numItems > items.count + AppConstants.infiniteLoadThresholdOffset - - // so although the API kindly returns `400`/"not_logged_in" for expired - // sessions _without_ 2FA enabled, currently once you enable 2FA on an account - // an expired session for a call with optional authentication such as loading - // posts returns a `200` with an empty list of data 😭 - // if we get back an empty list for page 1, chances are this session is borked and - // the API doesn't want to tell us - so to avoid the user being confused, we'll fire - // off an authenticated call in the background and if appropriate show the expired - // session modal. We should be able to remove this once the API behaves as expected. - if currentPage == 1, newPosts.isEmpty { - try await apiClient.attemptAuthenticatedCall() - } - - // don't preload filtered images - preloadImages(filterItems(items: newPosts)) - } catch { - handleError(error) - } - } - - /// Loads a single post and adds it to the tracker - /// - Parameter postId: id of the post to load - /// - Returns: PostModel of the newly loaded post - @discardableResult - func loadPost(postId: Int) async throws -> PostModel { - let newPost = try await postRepository.loadPost(postId: postId) - await add([newPost], preload: true) - return newPost - } - - @discardableResult - func refresh(clearBeforeFetch: Bool = false) async -> Bool { - defer { DispatchQueue.main.async { self.showLoadingIcon = false } } - DispatchQueue.main.async { self.showLoadingIcon = true } - if clearBeforeFetch { - await reset() - } - - page = 1 - currentCursor = nil - - do { - let (newPosts, cursor) = try await getNextPageFromRepository() - - currentCursor = cursor - await reset(with: newPosts, cursor: cursor) - return true - } catch { - handleError(error) - return false - } - } - - func initFeed() async { - DispatchQueue.main.async { self.showLoadingIcon = true } - if items.isEmpty { - print("Post tracker is empty") - await loadNextPage() - } else { - print("Post tracker is not empty") - DispatchQueue.main.async { self.showLoadingIcon = false } - } - } - - @MainActor - /// Adds a given list of posts to items. Can be configured to perform filtering and preloading. - /// - Parameters: - /// - newItems: list of PostModels to add - /// - preload: true if the new post's image should be preloaded - func add( - _ newItems: [PostModel], - preload: Bool = false - ) { - let accepted = dedupedItems(from: filterItems(items: newItems)) - - if preload { preloadImages(newItems) } - - if !shouldPerformMergeSorting { - RunLoop.main.perform { [self] in - items.append(contentsOf: accepted) - } - return - } - - let merged = merge(arr1: items, arr2: accepted, compare: { $0.published > $1.published }) - RunLoop.main.perform { [self] in - items = merged - } - } - - @MainActor - func reset( - with newItems: [PostModel] = .init(), - cursor: String? = nil - ) { - hasReachedEnd = false - page = newItems.isEmpty ? 1 : 2 - currentCursor = cursor - if page == 1 { - hiddenItems.removeAll() - } - ids = .init(minimumCapacity: 1000) - items = dedupedItems(from: filterItems(items: newItems)) - } - - /// Determines whether the tracker should load more items - /// NOTE: this is equivalent to the old shouldLoadContentPreciselyAfter - @MainActor - func shouldLoadContentAfter(after item: PostModel) -> Bool { - guard !isLoading, !hasReachedEnd else { return false } - - let thresholdIndex = max(0, items.index(items.endIndex, offsetBy: AppConstants.infiniteLoadThresholdOffset)) - if thresholdIndex >= 0, - let itemIndex = items.firstIndex(where: { $0.uid == item.uid }), - itemIndex == thresholdIndex { - return true - } - - return false - } - - // MARK: - Post Management Methods - - /// If a post with the same id as the given post is present in the tracker, replaces it with the given post; otherwise does nothing and quietly returns. - /// - Parameter updatedPost: PostModel representing a post already present in the tracker with a new state - @MainActor - func update(with updatedPost: PostModel) { - guard let index = items.firstIndex(where: { $0.uid == updatedPost.uid }) else { - return - } - - items[index] = updatedPost - } - - @MainActor - func prepend(_ newPost: PostModel) { - guard ids.insert(newPost.uid).inserted else { return } - items.prepend(newPost) - } - - @MainActor - func removeUserPosts(from personId: Int) { - filter { - $0.creator.userId != personId - } - } - - @MainActor - func removeCommunityPosts(from communityId: Int) { - filter { - $0.community.communityId != communityId - } - } - - /// Takes a callback and filters out any entry that returns false - /// Returns the number of entries removed - @discardableResult func filter(_ callback: (PostModel) -> Bool) -> Int { - var removedElements = 0 - - items = items.filter { - let filterResult = callback($0) - - // Remove the ID from the IDs set as well - if !filterResult { - ids.remove($0.uid) - removedElements += 1 - } - return filterResult - } - - return removedElements - } - - // MARK: - Interaction Methods - - /// Applies the given scoring operation to the given post, provided the post is present in ids. If the given operation has already been applied, it will instead send .resetVote. - /// Performs state faking--posts will updated immediately with the predicted state of the post post-update, then updated to match the source of truth when the call returns. - /// - Parameters: - /// - post: PostModel of the post to vote on - /// - inputOp: ScoringOperation to apply to the given post - /// - Returns: PostModel with the updated post state (if the call fails, returns the original post model) - @discardableResult - func voteOnPost(post: PostModel, inputOp: ScoringOperation) async -> PostModel { - // TODO: returning the post does sometimes cause weird unwanted state flickers when spamming interactions - guard !isLoading else { return post } - defer { isLoading = false } - isLoading = true - - // ensure this is a valid post to vote on - guard ids.contains(post.uid) else { - assertionFailure("Upvote called on post not present in tracker") - hapticManager.play(haptic: .failure, priority: .high) - return post - } - - // compute appropriate operation - let operation = post.votes.myVote == inputOp ? ScoringOperation.resetVote : inputOp - - // fake state - let stateFakedPost = PostModel(from: post, votes: post.votes.applyScoringOperation(operation: operation)) - await update(with: stateFakedPost) - hapticManager.play(haptic: .lightSuccess, priority: .low) - - // perform real upvote - do { - let response = try await postRepository.ratePost(postId: post.postId, operation: operation) - await update(with: response) - return response - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - return post - } - } - - /// Toggles the save state of the given post. Performs state faking. - /// - Parameter post: PostModel of the post to save - /// - Returns: PostModel with the updated post state (if the call fails, returns the original post model) - @discardableResult - func toggleSave(post: PostModel) async -> PostModel { - guard !isLoading else { return post } - defer { isLoading = false } - isLoading = true - - // ensure this is a valid post to save - guard ids.contains(post.uid) else { - assertionFailure("Save called on post not present in tracker") - hapticManager.play(haptic: .failure, priority: .high) - return post - } - - let shouldSave: Bool = !post.saved - - // fake state - var stateFakedPost = PostModel(from: post, saved: shouldSave) - if upvoteOnSave, stateFakedPost.votes.myVote != .upvote { - stateFakedPost.votes = stateFakedPost.votes.applyScoringOperation(operation: .upvote) - } - await update(with: stateFakedPost) - hapticManager.play(haptic: .success, priority: .high) - - // perform real save - do { - let saveResponse = try await postRepository.savePost(postId: post.postId, shouldSave: shouldSave) - - if shouldSave, upvoteOnSave { - let voteResponse = try await postRepository.ratePost(postId: saveResponse.postId, operation: .upvote) - await update(with: voteResponse) - return voteResponse - } else { - await update(with: saveResponse) - return saveResponse - } - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - return post - } - } - - /// Marks the given post as read (does not toggle) - func markRead(post: PostModel) async { - guard !isLoading else { return } - defer { isLoading = false } - isLoading = true - - // ensure this is a valid post to mark read - guard ids.contains(post.uid) else { - assertionFailure("markRead called on post not present in tracker") - hapticManager.play(haptic: .failure, priority: .high) - return - } - - // fake state - let stateFakedPost = PostModel(from: post, read: true) - await update(with: stateFakedPost) - - // perform real read - do { - let response = try await postRepository.markRead(post: post, read: true) - await update(with: response) - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - } - } - - func delete(post: PostModel) async { - guard !isLoading else { return } - defer { isLoading = false } - isLoading = true - - // ensure this is a valid post to delete - guard ids.contains(post.uid) else { - assertionFailure("delete called on post not present in tracker") - hapticManager.play(haptic: .failure, priority: .high) - return - } - - // TODO: state faking (should wait until APIPost is replaced with PostContentModel) - - do { - hapticManager.play(haptic: .destructiveSuccess, priority: .high) - let response = try await postRepository.deletePost(postId: post.postId, shouldDelete: true) - await update(with: response) - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - } - } - - /// Edits the given post and updates the tracker. Only non-nil fields will be updated. - /// - Parameters: - /// - post: PostModel representing the new state of the post (current state of tracker) - @discardableResult - func edit( - post: PostModel, - name: String?, - url: String?, - body: String?, - nsfw: Bool? - ) async -> PostModel { - guard !isLoading else { return post } - defer { isLoading = false } - isLoading = true - - // ensure this is a valid post to delete - guard ids.contains(post.uid) else { - assertionFailure("edit called on post not present in tracker") - hapticManager.play(haptic: .failure, priority: .high) - return post - } - - // TODO: state faking (should wait until APIPost is replaced with PostContentModel) - - do { - hapticManager.play(haptic: .success, priority: .high) - let response = try await postRepository.editPost(postId: post.postId, name: name, url: url, body: body, nsfw: nsfw) - await update(with: response) - return response - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - return post - } - } - - // MARK: - Private Methods - - private func preloadImages(_ newPosts: [PostModel]) { - URLSession.shared.configuration.urlCache = AppConstants.urlCache - var imageRequests: [ImageRequest] = [] - for post in newPosts { - // preload user and community avatars--fetching both because we don't know which we'll need, but these are super tiny - // so it's probably not an API crime, right? - if let communityAvatarLink = post.community.avatar { - imageRequests.append(ImageRequest(url: communityAvatarLink.withIconSize(Int(AppConstants.smallAvatarSize * 2)))) - } - - if let userAvatarLink = post.creator.avatar { - imageRequests.append(ImageRequest(url: userAvatarLink.withIconSize(Int(AppConstants.largeAvatarSize * 2)))) - } - - switch post.postType { - case let .image(url): - // images: only load the image - imageRequests.append(ImageRequest(url: url, priority: .high)) - case let .link(url): - // websites: load image and favicon - if let baseURL = post.post.linkUrl?.host, - let favIconURL = URL(string: "https://www.google.com/s2/favicons?sz=64&domain=\(baseURL)") { - imageRequests.append(ImageRequest(url: favIconURL)) - } - if let url { - imageRequests.append(ImageRequest(url: url, priority: .high)) - } - default: - break - } - } - - prefetcher.startPrefetching(with: imageRequests) - } - - /// Filters a list of PostModels to only those PostModels not present in ids. Updates ids. - private func dedupedItems(from newItems: [PostModel]) -> [PostModel] { - newItems.filter { ids.insert($0.uid).inserted } - } - - private func filterItems( - items: [PostModel] - ) -> [PostModel] { - items.filter { item in - if let reason = self.filter(item) { - self.hiddenItems[reason] = self.hiddenItems[reason, default: 0] + 1 - return false - } - return true - } - } -} - -// swiftlint:enable type_body_length -// swiftlint:enable file_length diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index 21c090b1d..ef7e2a107 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -12,14 +12,6 @@ import Foundation /// For simple (i.e. linear) navigation flows, you may wish to define a separate set of routes. For example, see `OnboardingRoutes`. /// enum AppRoute: Routable { - // case feed(NewFeedType) - - // TODO: ERIC remove this - // case communityLinkWithContext(CommunityLinkWithContext) - - case apiPostView(APIPostView) - case apiPost(APIPost) - case community(CommunityModel) @available(*, deprecated, message: "Use .userProfile instead.") @@ -42,12 +34,6 @@ enum AppRoute: Routable { // swiftlint:disable cyclomatic_complexity static func makeRoute(_ value: some Hashable) throws -> AppRoute { switch value { -// case let value as CommunityLinkWithContext: -// return .communityLinkWithContext(value) - case let value as APIPostView: - return .apiPostView(value) - case let value as APIPost: - return .apiPost(value) case let value as CommunityModel: return .community(value) case let value as APIPerson: @@ -56,8 +42,6 @@ enum AppRoute: Routable { return .userProfile(value) case let value as PostLinkWithContext: return .postLinkWithContext(value) -// case let value as NewPostLinkWithContext: -// return .newPostLinkWithContext(value) case let value as LazyLoadPostLinkWithContext: return .lazyLoadPostLinkWithContext(value) case let value as SettingsPage: diff --git a/Mlem/Views/Shared/Comments/Components/Embedded Post.swift b/Mlem/Views/Shared/Comments/Components/Embedded Post.swift index 52477bae4..8688d3afb 100644 --- a/Mlem/Views/Shared/Comments/Components/Embedded Post.swift +++ b/Mlem/Views/Shared/Comments/Components/Embedded Post.swift @@ -8,9 +8,6 @@ import SwiftUI struct EmbeddedPost: View { - // used to handle the lazy load embedded post--speed doesn't matter because it's not a "real" post tracker - @StateObject var postTracker: PostTracker - let community: APICommunity let post: APIPost let comment: APIComment @@ -19,9 +16,6 @@ struct EmbeddedPost: View { self.community = community self.post = post self.comment = comment - - @AppStorage("upvoteOnSave") var upvoteOnSave = false - self._postTracker = StateObject(wrappedValue: .init(internetSpeed: .slow, upvoteOnSave: upvoteOnSave)) } @State var loadedPostDetails: PostModel? diff --git a/Mlem/Views/Shared/Posts/Expanded Post.swift b/Mlem/Views/Shared/Posts/Expanded Post.swift index 1def3d426..994cca1f4 100644 --- a/Mlem/Views/Shared/Posts/Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Expanded Post.swift @@ -54,7 +54,7 @@ struct ExpandedPost: View { @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @StateObject var commentTracker: CommentTracker = .init() - @EnvironmentObject var postTracker: PostTracker + @EnvironmentObject var postTracker: StandardPostTracker @StateObject var post: PostModel var community: CommunityModel? diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index 83cbaf9e8..17d156daf 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -49,7 +49,7 @@ extension ExpandedPost { do { let response = try await apiClient.blockPerson(id: post.creator.userId, shouldBlock: true) if response.blocked { - postTracker.removeUserPosts(from: post.creator.userId) + await postTracker.applyFilter(.blockedUser(post.creator.userId)) hapticManager.play(haptic: .violentSuccess, priority: .high) await notifier.add(.success("Blocked \(post.creator.name)")) } @@ -144,7 +144,7 @@ extension ExpandedPost { enabled: !post.post.deleted ) { Task(priority: .userInitiated) { - await postTracker.delete(post: post) + await post.delete() } }) } diff --git a/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift b/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift index 930b47827..90882625e 100644 --- a/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift @@ -13,20 +13,21 @@ import SwiftUI */ struct LazyLoadExpandedPost: View { @Dependency(\.errorHandler) var errorHandler + @Dependency(\.postRepository) var postRepository let post: APIPost let scrollTarget: Int? @State private var loadedPostView: PostModel? - @StateObject private var postTracker: PostTracker // = PostTracker(internetSpeed: .slow) + @StateObject private var postTracker: StandardPostTracker init(post: APIPost, scrollTarget: Int? = nil) { self.post = post self.scrollTarget = scrollTarget @AppStorage("upvoteOnSave") var upvoteOnSave = false - self._postTracker = StateObject(wrappedValue: .init(internetSpeed: .slow, upvoteOnSave: upvoteOnSave)) + self._postTracker = StateObject(wrappedValue: .init(internetSpeed: .slow, sortType: .new, showReadPosts: true, feedType: .all)) } var body: some View { @@ -47,7 +48,7 @@ struct LazyLoadExpandedPost: View { } .task(priority: .background) { do { - loadedPostView = try await postTracker.loadPost(postId: post.id) + loadedPostView = try await postRepository.loadPost(postId: post.id) } catch { // TODO: Some sort of common alert banner? // we can show a toast here by passing a `message` and `style: .toast` by using a `ContextualError` below... diff --git a/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift index 0f6f8e3ed..74a9eeb66 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift @@ -24,8 +24,6 @@ struct CompactPost: View { @Dependency(\.errorHandler) var errorHandler @Environment(\.accessibilityDifferentiateWithoutColor) var diffWithoutColor: Bool - - @EnvironmentObject var postTracker: PostTracker // constants let thumbnailSize: CGFloat = 60 diff --git a/Mlem/Views/Tabs/Inbox/Inbox View.swift b/Mlem/Views/Tabs/Inbox/Inbox View.swift index b18496f74..e83d6e335 100644 --- a/Mlem/Views/Tabs/Inbox/Inbox View.swift +++ b/Mlem/Views/Tabs/Inbox/Inbox View.swift @@ -62,7 +62,6 @@ struct InboxView: View { @StateObject var replyTracker: ReplyTracker @StateObject var mentionTracker: MentionTracker @StateObject var messageTracker: MessageTracker - @StateObject var dummyPostTracker: PostTracker // used for navigation init() { // TODO: once the post tracker is changed we won't need this here... @@ -92,8 +91,6 @@ struct InboxView: View { self._replyTracker = StateObject(wrappedValue: newReplyTracker) self._mentionTracker = StateObject(wrappedValue: newMentionTracker) self._messageTracker = StateObject(wrappedValue: newMessageTracker) - - self._dummyPostTracker = StateObject(wrappedValue: .init(internetSpeed: internetSpeed, upvoteOnSave: upvoteOnSave)) } // input state handling From 5e6dd5696b0cdf2ef487ae8cbc8627cea437fc8f Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 13:42:24 -0500 Subject: [PATCH 41/69] tweaked scroll bar --- .../Feeds/Community List/SectionIndexTitles.swift | 9 ++++----- Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift | 13 +++++++++---- Mlem/Views/Tabs/Feeds/FeedsView.swift | 5 ++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift b/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift index 5380799e1..61d31d111 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift @@ -36,11 +36,11 @@ struct SectionIndexTitles: View { .background(dragObserver(viewId: communitySection.viewId)) } } - .padding(5) - .padding(.top, 7) // top looks a little funky otherwise + .padding(2) + .padding(.top, 4) .background { Capsule() - .foregroundStyle(.regularMaterial) + .foregroundStyle(.ultraThinMaterial) } .gesture( DragGesture(minimumDistance: 0, coordinateSpace: .global) @@ -64,7 +64,6 @@ struct SectionIndexTitles: View { proxy.scrollTo(viewId, anchor: .center) // Play nice tappy taps - // HapticManager.shared.rigidInfo() hapticManager.play(haptic: .rigidInfo, priority: .low) } } @@ -77,7 +76,7 @@ struct SectionIndexTitles: View { struct SectionIndexText: View { let label: String var body: some View { - Text(label).font(.system(size: 12)).bold() + Text(label).font(.system(size: 11)).fontWeight(.semibold) } } diff --git a/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift b/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift index 64c18e3d3..66a2353ab 100644 --- a/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift @@ -46,18 +46,23 @@ struct CommunityFeedRowView: View { } .buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) .accessibilityHidden(true) - }.swipeActions(edge: .leading) { + }.swipeActions { if subscribed { - Button("Unsubscribe") { + Button { Task(priority: .userInitiated) { await subscribe(communityId: community.id, shouldSubscribe: false) } - }.tint(.red) // Destructive role seems to remove from list so just make it red + } label: { + Label("Unsubscribe", systemImage: Icons.unsubscribe) + } + .tint(.red) // Destructive role seems to remove from list so just make it red } else { - Button("Subscribe") { + Button { Task(priority: .userInitiated) { await subscribe(communityId: community.id, shouldSubscribe: true) } + } label: { + Label("Subscribe", systemImage: Icons.subscribe) }.tint(.blue) } } diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index 99e088b34..2fd2ef711 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -46,7 +46,7 @@ struct FeedsView: View { FeedRowView(feedType: feedType) } } - // .padding(.trailing, 22) + .padding(.trailing, 10) ForEach(communityListModel.visibleSections) { section in Section(header: communitySectionHeaderView(for: section)) { @@ -62,14 +62,13 @@ struct FeedsView: View { } } } - // .padding(.trailing, 22) + .padding(.trailing, 10) } .scrollIndicators(.hidden) .navigationTitle("Communities") .listStyle(PlainListStyle()) SectionIndexTitles(proxy: scrollProxy, communitySections: communityListModel.allSections()) - .padding(.trailing, 7) } } detail: { NavigationStack(path: $feedTabNavigation.path) { From 76ab4de72d2561092cf9abe40448435f8ae525bc Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:05:26 -0500 Subject: [PATCH 42/69] got default feed working --- Mlem.xcodeproj/project.pbxproj | 16 +- Mlem/API/APIClient/APIClient+Comment.swift | 2 +- Mlem/API/APIClient/APIClient+Post.swift | 29 +- Mlem/API/APIClient/APIClient.swift | 2 +- Mlem/API/Models/ListingType.swift | 39 +-- Mlem/API/Requests/Comment/GetComments.swift | 2 +- Mlem/API/Requests/Post/GetPosts.swift | 52 +--- Mlem/API/Requests/SearchRequest.swift | 2 +- Mlem/Enums/FeedType.swift | 132 +++++++-- Mlem/Enums/NEW FeedType.swift | 151 ---------- Mlem/Enums/Settings/DefaultFeedType.swift | 34 +++ .../EnvironmentValues+FeedType.swift | 4 +- Mlem/MlemApp.swift | 21 +- .../Navigation Contexts/Community Link.swift | 2 +- .../Trackers/Feeds/StandardPostTracker.swift | 10 +- Mlem/Repositories/CommunityRepository.swift | 4 +- Mlem/Repositories/PostRepository.swift | 29 +- .../Composer/PostComposerView+Logic.swift | 1 - .../CommunityListRowViews.swift | 276 +++++++++--------- .../Tabs/Feeds/Components/FeedRowView.swift | 2 +- .../Feed Types/AggregateFeedView+Logic.swift | 2 +- .../Feeds/Feed Types/AggregateFeedView.swift | 2 +- .../Tabs/Feeds/Feed Types/SavedFeedView.swift | 2 +- Mlem/Views/Tabs/Feeds/FeedsView.swift | 18 +- .../Views/General/GeneralSettingsView.swift | 6 +- 25 files changed, 333 insertions(+), 507 deletions(-) delete mode 100644 Mlem/Enums/NEW FeedType.swift create mode 100644 Mlem/Enums/Settings/DefaultFeedType.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 17d914fdf..683b47035 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -161,7 +161,6 @@ 50DBB8E02A805836002870B1 /* MockErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DBB8DF2A805836002870B1 /* MockErrorHandler.swift */; }; 50EC39B22A346DDC00E014C2 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EC39B12A346DDC00E014C2 /* URLHandler.swift */; }; 50F2851C2A5C5C1500CF8865 /* TokenRefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F2851B2A5C5C1500CF8865 /* TokenRefreshView.swift */; }; - 6317ABCB2A37292700603D76 /* FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6317ABCA2A37292700603D76 /* FeedType.swift */; }; 6318DE5427FB958800CC2AD6 /* Stickied Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6318DE5327FB958800CC2AD6 /* Stickied Tag.swift */; }; 6318EDC327EE4D7F00BFCAE8 /* Feed Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6318EDC227EE4D7F00BFCAE8 /* Feed Post.swift */; }; 6322A5CB27F77A4D00135D4F /* Loading View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6322A5CA27F77A4D00135D4F /* Loading View.swift */; }; @@ -386,7 +385,7 @@ CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */; }; - CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */; }; + CD4BAD3D2B4C6C8E00A1E726 /* FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* FeedType.swift */; }; CD4BAD432B507F2B00A1E726 /* AggregateFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */; }; CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */; }; CD525F652A4B6D8F00BCA794 /* CommunityLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */; }; @@ -421,6 +420,7 @@ CD863FBC2A6B026400A31ED9 /* DocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD863FBB2A6B026400A31ED9 /* DocumentView.swift */; }; CD8C55342A95515C0060B75B /* Onboarding Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8C55332A95515C0060B75B /* Onboarding Text.swift */; }; CD8CF2092AF3F131009FFC23 /* Firm Info.ahap in Resources */ = {isa = PBXBuildFile; fileRef = CD8CF2082AF3F131009FFC23 /* Firm Info.ahap */; }; + CD963FCB2B5F0388002352FD /* DefaultFeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD963FCA2B5F0388002352FD /* DefaultFeedType.swift */; }; CD9A03C62B34D20500C16276 /* EnvironmentValues+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9A03C52B34D20500C16276 /* EnvironmentValues+Navigation.swift */; }; CD9A03C82B389F7000C16276 /* EnvironmentValues+FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9A03C72B389F7000C16276 /* EnvironmentValues+FeedType.swift */; }; CD9A49D12B045B64001E18A0 /* ZoomableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9A49D02B045B64001E18A0 /* ZoomableContainer.swift */; }; @@ -704,7 +704,6 @@ 50EC39B12A346DDC00E014C2 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = ""; }; 50F2851B2A5C5C1500CF8865 /* TokenRefreshView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefreshView.swift; sourceTree = ""; }; 630D753C27F65E44006E60C9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 6317ABCA2A37292700603D76 /* FeedType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedType.swift; sourceTree = ""; }; 6318DE5327FB958800CC2AD6 /* Stickied Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stickied Tag.swift"; sourceTree = ""; }; 6318EDC227EE4D7F00BFCAE8 /* Feed Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feed Post.swift"; sourceTree = ""; }; 6322A5CA27F77A4D00135D4F /* Loading View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Loading View.swift"; sourceTree = ""; }; @@ -927,7 +926,7 @@ CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRowView.swift; sourceTree = ""; }; - CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEW FeedType.swift"; sourceTree = ""; }; + CD4BAD3C2B4C6C8E00A1E726 /* FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedType.swift; sourceTree = ""; }; CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregateFeedView.swift; sourceTree = ""; }; CD4DBC022A6F803C001A1E61 /* ReplyToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyToPost.swift; sourceTree = ""; }; CD525F642A4B6D8F00BCA794 /* CommunityLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLinkView.swift; sourceTree = ""; }; @@ -962,6 +961,7 @@ CD863FBB2A6B026400A31ED9 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = ""; }; CD8C55332A95515C0060B75B /* Onboarding Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Onboarding Text.swift"; sourceTree = ""; }; CD8CF2082AF3F131009FFC23 /* Firm Info.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Firm Info.ahap"; sourceTree = ""; }; + CD963FCA2B5F0388002352FD /* DefaultFeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultFeedType.swift; sourceTree = ""; }; CD9A03C52B34D20500C16276 /* EnvironmentValues+Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+Navigation.swift"; sourceTree = ""; }; CD9A03C72B389F7000C16276 /* EnvironmentValues+FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedType.swift"; sourceTree = ""; }; CD9A49D02B045B64001E18A0 /* ZoomableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableContainer.swift; sourceTree = ""; }; @@ -2038,7 +2038,6 @@ children = ( 6DA7E9A02A50763B0095AB68 /* User */, CD64832B2A38CE4200EE6CA3 /* Settings */, - 6317ABCA2A37292700603D76 /* FeedType.swift */, CD6483352A39F20800EE6CA3 /* Post Type.swift */, 03C905CB2B3C88F700B9082F /* SearchTab.swift */, 6DCE71282A53C26600CFEB5E /* ServerInstanceLocation.swift */, @@ -2048,7 +2047,7 @@ CD2053132ACBAF150000AA38 /* AvatarType.swift */, CD4368BD2AE23FA600BD8BD1 /* LoadingState.swift */, CD4368C92AE2428C00BD8BD1 /* ContentIdentifiable.swift */, - CD4BAD3C2B4C6C8E00A1E726 /* NEW FeedType.swift */, + CD4BAD3C2B4C6C8E00A1E726 /* FeedType.swift */, ); path = Enums; sourceTree = ""; @@ -2486,6 +2485,7 @@ CDC1C9402A7ABA9C00072E3D /* ReadMarkStyle.swift */, CD6483A52A82FAF200A5AE84 /* ProfileTabLabel.swift */, CDDB2EDD2A85C2F1001D4B16 /* HapticPriority.swift */, + CD963FCA2B5F0388002352FD /* DefaultFeedType.swift */, ); path = Settings; sourceTree = ""; @@ -3071,7 +3071,7 @@ ADDC9E3A2A5CEAA100383D58 /* BlockPerson.swift in Sources */, CD6F29A82A77FF1700F20B6B /* MarkPostRead.swift in Sources */, 031A617E2B1CE90F00ABF23B /* ChangePasswordView.swift in Sources */, - CD4BAD3D2B4C6C8E00A1E726 /* NEW FeedType.swift in Sources */, + CD4BAD3D2B4C6C8E00A1E726 /* FeedType.swift in Sources */, 6372186B2A3A2AAD008C4816 /* GetComments.swift in Sources */, B1DD00BD2A62DDEC002A7B39 /* RecognizedLemmyInstances.swift in Sources */, 6DA61F892A575DF1001EA633 /* URL+WithIconSize.swift in Sources */, @@ -3176,7 +3176,6 @@ CD4368B82AE23F5400BD8BD1 /* ParentTrackerProtocol.swift in Sources */, 637218492A3A2AAD008C4816 /* APICommentReplyView.swift in Sources */, CD4368C82AE2426700BD8BD1 /* ReplyModel.swift in Sources */, - 6317ABCB2A37292700603D76 /* FeedType.swift in Sources */, CDC65D8F2A86B6DD007205E5 /* DeleteUser.swift in Sources */, CD6483382A3A0F2200EE6CA3 /* NSFW Tag.swift in Sources */, 503422582AAB798600EFE88D /* AppFlow.swift in Sources */, @@ -3466,6 +3465,7 @@ 6363D5C527EE196700E34822 /* MlemApp.swift in Sources */, CDF1EF162A6C3BC2003594B6 /* End Of Feed View.swift in Sources */, 637218542A3A2AAD008C4816 /* APILanguage.swift in Sources */, + CD963FCB2B5F0388002352FD /* DefaultFeedType.swift in Sources */, 50CC4A722A9CB07F0074C845 /* TimeInterval+Period.swift in Sources */, AD1B0D372A5F7A260006F554 /* Licenses.swift in Sources */, 6372186E2A3A2AAD008C4816 /* DeleteComment.swift in Sources */, diff --git a/Mlem/API/APIClient/APIClient+Comment.swift b/Mlem/API/APIClient/APIClient+Comment.swift index 641463aa1..99c6430af 100644 --- a/Mlem/API/APIClient/APIClient+Comment.swift +++ b/Mlem/API/APIClient/APIClient+Comment.swift @@ -12,7 +12,7 @@ extension APIClient { func loadComments( for postId: Int, maxDepth: Int = 15, - type: FeedType = .all, + type: APIListingType = .all, sort: CommentSortType? = nil, page: Int? = nil, limit: Int? = nil, diff --git a/Mlem/API/APIClient/APIClient+Post.swift b/Mlem/API/APIClient/APIClient+Post.swift index c3d2ae7a9..055ba6fe7 100644 --- a/Mlem/API/APIClient/APIClient+Post.swift +++ b/Mlem/API/APIClient/APIClient+Post.swift @@ -8,40 +8,13 @@ import Foundation extension APIClient { - // TODO: ERIC delete this // swiftlint:disable function_parameter_count func loadPosts( communityId: Int?, page: Int, cursor: String?, sort: PostSortType?, - type: FeedType, - limit: Int?, - savedOnly: Bool?, - communityName: String? - ) async throws -> GetPostsResponse { - let request = try OldGetPostsRequest( - session: session, - communityId: communityId, - page: page, - cursor: cursor, - sort: sort, - type: type, - limit: limit, - savedOnly: savedOnly, - communityName: communityName - ) - - return try await perform(request: request) - } - - // swiftlint:disable function_parameter_count - func loadPosts( - communityId: Int?, - page: Int, - cursor: String?, - sort: PostSortType?, - type: NewFeedType, + type: APIListingType, limit: Int?, savedOnly: Bool?, communityName: String? diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index d3fd5b270..8ba95cf80 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -389,7 +389,7 @@ extension APIClient { query: String, searchType: SearchType, sortOption: PostSortType, - listingType: FeedType, + listingType: APIListingType, page: Int?, limit: Int? ) async throws -> SearchResponse { diff --git a/Mlem/API/Models/ListingType.swift b/Mlem/API/Models/ListingType.swift index 2f14e1d52..fbd70f967 100644 --- a/Mlem/API/Models/ListingType.swift +++ b/Mlem/API/Models/ListingType.swift @@ -11,42 +11,5 @@ enum APIListingType: String, Codable { case all = "All" case local = "Local" case subscribed = "Subscribed" - - // Pre 0.18.0 it appears that they used integers instead of strings here. We can remove this intialiser once we drop support for old versions. To fully support both systems, we'd also need to *encode* back into the correct integer or string format. I'd rather not go through the effort for instance versions that most people don't use any more, so I've disabled the option to edit account settings on instances running <0.18.0 - // - sjmarf - - // TODO: 0.17 deprecation remove this initialiser - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let stringValue = try? container.decode(String.self) { - guard let value = APIListingType(rawValue: stringValue) else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Value not one of \"All\", \"Local\" or \"Subscribed\"." - ) - } - self = value - } else if let intValue = try? container.decode(Int.self) { - guard 0...2 ~= intValue else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Must be an integer in range 0...2." - ) - } - switch intValue { - case 0: - self = .all - case 1: - self = .local - default: - self = .subscribed - } - } else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Invalid value" - ) - } - } + case moderatorView = "ModeratorView" } diff --git a/Mlem/API/Requests/Comment/GetComments.swift b/Mlem/API/Requests/Comment/GetComments.swift index ee0864cb7..e920e48e4 100644 --- a/Mlem/API/Requests/Comment/GetComments.swift +++ b/Mlem/API/Requests/Comment/GetComments.swift @@ -19,7 +19,7 @@ struct GetCommentsRequest: APIGetRequest { session: APISession, postId: Int, maxDepth: Int, - type: FeedType, + type: APIListingType, sort: CommentSortType?, page: Int?, limit: Int?, diff --git a/Mlem/API/Requests/Post/GetPosts.swift b/Mlem/API/Requests/Post/GetPosts.swift index 5c915797c..48892cf70 100644 --- a/Mlem/API/Requests/Post/GetPosts.swift +++ b/Mlem/API/Requests/Post/GetPosts.swift @@ -21,57 +21,7 @@ struct GetPostsRequest: APIGetRequest { page: Int, cursor: String?, sort: PostSortType?, - type: NewFeedType, - limit: Int? = nil, - savedOnly: Bool? = nil, - communityName: String? = nil - // TODO: 0.19 support add liked_only and disliked_only fields - ) throws { - self.instanceURL = try session.instanceUrl - var queryItems: [URLQueryItem] = [ - .init(name: "type_", value: type.typeString), - .init(name: "sort", value: sort.map(\.rawValue)), - .init(name: "community_id", value: communityId.map(String.init)), - .init(name: "community_name", value: communityName), - .init(name: "limit", value: limit.map(String.init)), - .init(name: "saved_only", value: savedOnly.map(String.init)) - ] - - let paginationParameter: URLQueryItem - if let cursor { - paginationParameter = .init(name: "page_cursor", value: cursor) - } else { - paginationParameter = .init(name: "page", value: "\(page)") - } - - queryItems.append(paginationParameter) - - if let token = try? session.token { - queryItems.append( - .init(name: "auth", value: token) - ) - } - - self.queryItems = queryItems - } -} - -// TODO: ERIC delete this -// lemmy_api_common::post::GetPosts -struct OldGetPostsRequest: APIGetRequest { - typealias Response = GetPostsResponse - - let instanceURL: URL - let path = "post/list" - let queryItems: [URLQueryItem] - - init( - session: APISession, - communityId: Int?, - page: Int, - cursor: String?, - sort: PostSortType?, - type: FeedType, + type: APIListingType, limit: Int? = nil, savedOnly: Bool? = nil, communityName: String? = nil diff --git a/Mlem/API/Requests/SearchRequest.swift b/Mlem/API/Requests/SearchRequest.swift index 713d42c0c..5ea200cd9 100644 --- a/Mlem/API/Requests/SearchRequest.swift +++ b/Mlem/API/Requests/SearchRequest.swift @@ -29,7 +29,7 @@ struct SearchRequest: APIGetRequest { query: String, searchType: SearchType, sortOption: PostSortType, - listingType: FeedType, + listingType: APIListingType, page: Int?, communityId: Int?, communityName: String?, diff --git a/Mlem/Enums/FeedType.swift b/Mlem/Enums/FeedType.swift index dc7a3dab0..f33052fde 100644 --- a/Mlem/Enums/FeedType.swift +++ b/Mlem/Enums/FeedType.swift @@ -2,76 +2,144 @@ // FeedType.swift // Mlem // -// Created by Jonathan de Jong on 12.06.2023. +// Created by Eric Andrews on 2024-01-08. // +import Foundation import SwiftUI -enum FeedType: String, Encodable, SettingsOptions, AssociatedColor { - var id: Self { self } - +enum FeedType { + case all, local, subscribed, saved + case community(CommunityModel) + + static var allAggregateFeedCases: [FeedType] = [.all, .local, .subscribed, .saved] + var label: String { - return rawValue + switch self { + case .all: "All" + case .local: "Local" + case .subscribed: "Subscribed" + case .saved: "Saved" + case let .community(communityModel): communityModel.name + } } - var description: String { + /// Maps FeedType to APIListingType + var toApiListingType: APIListingType { switch self { - case .all: - return "Subscribed communities from all instances" - case .local: - return "Local communities from your server" - case .subscribed: - return "All communities that federate with your server" + case .all: .all + case .local: .local + case .subscribed: .subscribed + case .saved: .all // TODO: change this? + case .community: .subscribed } } - var color: Color? { + /// String for use in shortcuts + var toShortcutString: String { + switch self { + case .all: "All" + case .local: "Local" + case .subscribed: "Subscribed" + case .saved: "Saved" // TODO: change this? + case .community: "Subscribed" + } + } + + static func fromShortcutString(shortcut: String?) -> FeedType? { + switch shortcut { + case "All": + return .all + case "Local": + return .local + case "Subscribed": + return .subscribed + case "Saved": + return .saved + default: + return nil + } + } + + var communityId: Int? { + switch self { + case let .community(communityModel): communityModel.communityId + default: nil + } + } +} + +extension FeedType: Hashable, Identifiable { + func hash(into hasher: inout Hasher) { switch self { case .all: - return .blue + hasher.combine("all") case .local: - return .green + hasher.combine("local") case .subscribed: - return .red + hasher.combine("subscribed") + case .saved: + hasher.combine("saved") + case let .community(communityModel): + hasher.combine("community") + hasher.combine(communityModel.communityId) } } - - case subscribed = "Subscribed" - case local = "Local" - case all = "All" + + var id: Int { hashValue } } extension FeedType: AssociatedIcon { var iconName: String { switch self { - case .all: return Icons.federatedFeed - case .local: return Icons.localFeed - case .subscribed: return Icons.subscribedFeed + case .all: Icons.federatedFeed + case .local: Icons.localFeed + case .subscribed: Icons.subscribedFeed + case .saved: Icons.savedFeed + case .community: Icons.community } } var iconNameFill: String { switch self { - case .all: return Icons.federatedFeedFill - case .local: return Icons.localFeedFill - case .subscribed: return Icons.subscribedFeedFill + case .all: Icons.federatedFeedFill + case .local: Icons.localFeedFill + case .subscribed: Icons.subscribedFeedFill + case .saved: Icons.savedFeedFill + case .community: Icons.communityFill } } var iconNameCircle: String { switch self { - case .all: return Icons.federatedFeedCircle - case .local: return Icons.localFeedCircle - case .subscribed: return Icons.subscribedFeedCircle + case .all: Icons.federatedFeedCircle + case .local: Icons.localFeedCircle + case .subscribed: Icons.subscribedFeedCircle + case .saved: Icons.savedFeedCircle + case .community: Icons.community } } /// Icon to use in system settings. This should be removed when the "unified symbol handling" is closed var settingsIconName: String { switch self { - case .all: return "circle.hexagongrid" - case .local: return "house" - case .subscribed: return "newspaper" + case .all: "circle.hexagongrid" + case .local: "house" + case .subscribed: "newspaper" + case .saved: Icons.save + case .community: Icons.community + } + } +} + +extension FeedType: AssociatedColor { + var color: Color? { + switch self { + case .all: .blue + case .local: .purple + case .subscribed: .red + case .saved: .green + case .community: .blue } } } diff --git a/Mlem/Enums/NEW FeedType.swift b/Mlem/Enums/NEW FeedType.swift deleted file mode 100644 index 884d72782..000000000 --- a/Mlem/Enums/NEW FeedType.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// NEW FeedType.swift -// Mlem -// -// Created by Eric Andrews on 2024-01-08. -// - -import Foundation -import SwiftUI - -enum NewFeedType { - case all, local, subscribed, saved - case community(CommunityModel) - - static var allAggregateFeedCases: [NewFeedType] = [.all, .local, .subscribed, .saved] - - var label: String { - switch self { - case .all: "All" - case .local: "Local" - case .subscribed: "Subscribed" - case .saved: "Saved" - case let .community(communityModel): communityModel.name - } - } - - /// String to pass into the API call - var typeString: String { - switch self { - case .all: "All" - case .local: "Local" - case .subscribed: "Subscribed" - case .saved: "Saved" // TODO: change this? - case .community: "Subscribed" - } - } - - static func fromShortcut(shortcut: String?) -> NewFeedType? { - switch shortcut { - case "All": - return .all - case "Local": - return .local - case "Subscribed": - return .subscribed - case "Saved": - return .saved - default: - return nil - } - } - - var toLegacyFeedType: FeedType { - switch self { - case .all: - return .all - case .local: - return .local - case .subscribed: - return .subscribed - case .saved: - assertionFailure("Incompatible feed type!") - return .all - default: - assertionFailure("Incompatible feed type!") - return .all - } - } - - var communityId: Int? { - switch self { - case let .community(communityModel): communityModel.communityId - default: nil - } - } -} - -extension NewFeedType: Hashable, Identifiable { - func hash(into hasher: inout Hasher) { - switch self { - case .all: - hasher.combine("all") - case .local: - hasher.combine("local") - case .subscribed: - hasher.combine("subscribed") - case .saved: - hasher.combine("saved") - case let .community(communityModel): - hasher.combine("community") - hasher.combine(communityModel.communityId) - } - } - - var id: Int { hashValue } -} - -extension NewFeedType: AssociatedIcon { - var iconName: String { - switch self { - case .all: Icons.federatedFeed - case .local: Icons.localFeed - case .subscribed: Icons.subscribedFeed - case .saved: Icons.savedFeed - case .community: Icons.community - } - } - - var iconNameFill: String { - switch self { - case .all: Icons.federatedFeedFill - case .local: Icons.localFeedFill - case .subscribed: Icons.subscribedFeedFill - case .saved: Icons.savedFeedFill - case .community: Icons.communityFill - } - } - - var iconNameCircle: String { - switch self { - case .all: Icons.federatedFeedCircle - case .local: Icons.localFeedCircle - case .subscribed: Icons.subscribedFeedCircle - case .saved: Icons.savedFeedCircle - case .community: Icons.community - } - } - - /// Icon to use in system settings. This should be removed when the "unified symbol handling" is closed - var settingsIconName: String { - switch self { - case .all: "circle.hexagongrid" - case .local: "house" - case .subscribed: "newspaper" - case .saved: Icons.save - case .community: Icons.community - } - } -} - -extension NewFeedType: AssociatedColor { - var color: Color? { - switch self { - case .all: .blue - case .local: .purple - case .subscribed: .red - case .saved: .green - case .community: .blue - } - } -} diff --git a/Mlem/Enums/Settings/DefaultFeedType.swift b/Mlem/Enums/Settings/DefaultFeedType.swift new file mode 100644 index 000000000..ff1ba0eb8 --- /dev/null +++ b/Mlem/Enums/Settings/DefaultFeedType.swift @@ -0,0 +1,34 @@ +// +// DefaultFeedType.swift +// Mlem +// +// Created by Eric Andrews on 2024-01-22. +// + +import Foundation + +enum DefaultFeedType: String, SettingsOptions, CaseIterable { + case all, local, subscribed, saved + + var label: String { rawValue.capitalized } + + var settingsIconName: String { + switch self { + case .all: Icons.federatedFeed + case .local: Icons.localFeed + case .subscribed: Icons.subscribedFeed + case .saved: Icons.savedFeed + } + } + + var toFeedType: FeedType { + switch self { + case .all: .all + case .local: .local + case .subscribed: .subscribed + case .saved: .saved + } + } + + var id: Self { self } +} diff --git a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift index 82574d125..e55f8b0c1 100644 --- a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift +++ b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift @@ -9,11 +9,11 @@ import Foundation import SwiftUI private struct FeedTypeEnvironmentKey: EnvironmentKey { - static let defaultValue: FeedType? = nil + static let defaultValue: APIListingType? = nil } extension EnvironmentValues { - var feedType: FeedType? { + var feedType: APIListingType? { get { self[FeedTypeEnvironmentKey.self] } set { self[FeedTypeEnvironmentKey.self] = newValue } } diff --git a/Mlem/MlemApp.swift b/Mlem/MlemApp.swift index 921b71e03..95d62c874 100644 --- a/Mlem/MlemApp.swift +++ b/Mlem/MlemApp.swift @@ -77,7 +77,7 @@ struct MlemApp: App { // Subscribed Feed let subscribedIcon = UIApplicationShortcutIcon(systemImageName: Icons.subscribedFeed) let subscribedFeedItem = UIApplicationShortcutItem( - type: FeedType.subscribed.rawValue, + type: FeedType.subscribed.toShortcutString, localizedTitle: "Subscribed", localizedSubtitle: nil, icon: subscribedIcon, @@ -87,7 +87,7 @@ struct MlemApp: App { // Local Feed let localIcon = UIApplicationShortcutIcon(systemImageName: Icons.localFeed) let localFeedItem = UIApplicationShortcutItem( - type: FeedType.local.rawValue, + type: FeedType.local.toShortcutString, localizedTitle: "Local", localizedSubtitle: nil, icon: localIcon, @@ -97,17 +97,28 @@ struct MlemApp: App { // All Feed let allIcon = UIApplicationShortcutIcon(systemImageName: Icons.federatedFeed) let allFeedItem = UIApplicationShortcutItem( - type: FeedType.all.rawValue, + type: FeedType.all.toShortcutString, localizedTitle: "All", localizedSubtitle: nil, icon: allIcon, userInfo: nil ) + + // Saved Feed + let savedIcon = UIApplicationShortcutIcon(systemImageName: Icons.savedFeed) + let savedFeedItem = UIApplicationShortcutItem( + type: FeedType.saved.toShortcutString, + localizedTitle: "Saved", + localizedSubtitle: nil, + icon: savedIcon, + userInfo: nil + ) UIApplication.shared.shortcutItems = [ - subscribedFeedItem, + allFeedItem, localFeedItem, - allFeedItem + subscribedFeedItem, + savedFeedItem ] } diff --git a/Mlem/Models/Navigation Contexts/Community Link.swift b/Mlem/Models/Navigation Contexts/Community Link.swift index d3d52f7d6..acd3469d3 100644 --- a/Mlem/Models/Navigation Contexts/Community Link.swift +++ b/Mlem/Models/Navigation Contexts/Community Link.swift @@ -21,5 +21,5 @@ struct CommunityLinkWithContext: Equatable, Identifiable, Hashable { var id: Int { hashValue } let community: CommunityModel? - let feedType: FeedType + let feedType: APIListingType } diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 669952f05..a6bbafb12 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -50,7 +50,7 @@ class StandardPostTracker: StandardTracker { // TODO: ERIC keyword filters could be more elegant var filteredKeywords: [String] - var feedType: NewFeedType + var feedType: FeedType private(set) var postSortType: PostSortType private var filters: [NewPostFilterReason: Int] @@ -61,7 +61,7 @@ class StandardPostTracker: StandardTracker { maxConcurrentRequestCount: 40 ) - init(internetSpeed: InternetSpeed, sortType: PostSortType, showReadPosts: Bool, feedType: NewFeedType) { + init(internetSpeed: InternetSpeed, sortType: PostSortType, showReadPosts: Bool, feedType: FeedType) { @Dependency(\.persistenceRepository) var persistenceRepository self.feedType = feedType @@ -97,7 +97,7 @@ class StandardPostTracker: StandardTracker { page: page, cursor: cursor, sort: postSortType, - type: feedType, + type: feedType.toApiListingType, limit: internetSpeed.pageSize ) @@ -130,7 +130,7 @@ class StandardPostTracker: StandardTracker { page: page, cursor: nil, sort: postSortType, - type: feedType, + type: feedType.toApiListingType, limit: internetSpeed.pageSize ) } @@ -155,7 +155,7 @@ class StandardPostTracker: StandardTracker { } @MainActor - func changeFeedType(to newFeedType: NewFeedType) async { + func changeFeedType(to newFeedType: FeedType) async { // don't do anything if feed type not changed guard feedType != newFeedType else { return diff --git a/Mlem/Repositories/CommunityRepository.swift b/Mlem/Repositories/CommunityRepository.swift index fa3aef757..d203c4444 100644 --- a/Mlem/Repositories/CommunityRepository.swift +++ b/Mlem/Repositories/CommunityRepository.swift @@ -35,7 +35,7 @@ struct CommunityRepository { var communities = [APICommunityView]() repeat { - let response = try await client.loadCommunityList(sort: nil, page: page, limit: limit, type: FeedType.subscribed.rawValue) + let response = try await client.loadCommunityList(sort: nil, page: page, limit: limit, type: APIListingType.subscribed.rawValue) communities.append(contentsOf: response.communities) hasMorePages = response.communities.count >= limit page += 1 @@ -78,7 +78,7 @@ struct CommunityRepository { } func loadDetails(for id: Int) async throws -> CommunityModel { - CommunityModel(from: try await details(apiClient, id)) + try await CommunityModel(from: details(apiClient, id)) } @discardableResult diff --git a/Mlem/Repositories/PostRepository.swift b/Mlem/Repositories/PostRepository.swift index e2a6f809e..6fbe0214c 100644 --- a/Mlem/Repositories/PostRepository.swift +++ b/Mlem/Repositories/PostRepository.swift @@ -11,40 +11,13 @@ import Foundation class PostRepository { @Dependency(\.apiClient) private var apiClient - // TODO: ERIC delete this // swiftlint:disable function_parameter_count func loadPage( communityId: Int?, page: Int, cursor: String?, sort: PostSortType?, - type: FeedType, - limit: Int, - savedOnly: Bool? = nil, - communityName: String? = nil - ) async throws -> (posts: [PostModel], cursor: String?) { - let response = try await apiClient.loadPosts( - communityId: communityId, - page: page, - cursor: cursor, - sort: sort, - type: type, - limit: limit, - savedOnly: savedOnly, - communityName: communityName - ) - - let posts = response.posts.map { PostModel(from: $0) } - return (posts, response.nextPage) - } - - // swiftlint:disable function_parameter_count - func loadPage( - communityId: Int?, - page: Int, - cursor: String?, - sort: PostSortType?, - type: NewFeedType, + type: APIListingType, limit: Int, savedOnly: Bool? = nil, communityName: String? = nil diff --git a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift index b09f078b7..634246976 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView+Logic.swift @@ -65,7 +65,6 @@ extension PostComposerView { hapticManager.play(haptic: .success, priority: .high) - // TODO: ERIC test this if let postTracker = editModel.postTracker { Task { await postTracker.prependItem(PostModel(from: response.postView)) diff --git a/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift b/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift index 6449cd87d..261e90c41 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift @@ -19,141 +19,141 @@ struct FavoriteStarButtonStyle: ButtonStyle { } } -struct CommuntiyFeedRowView: View { - @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker - @Dependency(\.hapticManager) var hapticManager - @Dependency(\.notifier) var notifier - - let community: APICommunity - let subscribed: Bool - let communitySubscriptionChanged: (APICommunity, Bool) -> Void - let navigationContext: NavigationContext - - var body: some View { - NavigationLink(value: pathValue) { - HStack { - // NavigationLink with invisible array - communityNameLabel - - Spacer() - Button("Favorite Community") { - hapticManager.play(haptic: .success, priority: .high) - - toggleFavorite() - - }.buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) - .accessibilityHidden(true) - } - }.swipeActions { - if subscribed { - Button("Unsubscribe") { - Task(priority: .userInitiated) { - await subscribe(communityId: community.id, shouldSubscribe: false) - } - }.tint(.red) // Destructive role seems to remove from list so just make it red - } else { - Button("Subscribe") { - Task(priority: .userInitiated) { - await subscribe(communityId: community.id, shouldSubscribe: true) - } - }.tint(.blue) - } - } - .accessibilityAction(named: "Toggle favorite") { - toggleFavorite() - } - .accessibilityElement(children: .combine) - .accessibilityLabel(communityLabel) - } - - private var pathValue: AnyHashable { - if navigationContext == .sidebar { - return CommunityLinkWithContext(community: CommunityModel(from: community), feedType: .subscribed) - } else { - // Do not use enum route path in sidebar: It doesn't work, and I have no idea why =/ [2023.09] - // return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) - return AppRoute.community(CommunityModel(from: community)) - } - } - - private var communityNameText: Text { - Text(community.name) - } - - @ViewBuilder - private var communityNameLabel: some View { - if let website = community.actorId.host(percentEncoded: false) { - communityNameText + - Text("@\(website)") - .font(.footnote) - .foregroundColor(.gray.opacity(0.5)) - } else { - communityNameText - } - } - - private var communityLabel: String { - var label = community.name - - if let website = community.actorId.host(percentEncoded: false) { - label += "@\(website)" - } - - if isFavorited() { - label += ", is a favorite" - } - - return label - } - - private func toggleFavorite() { - if isFavorited() { - favoriteCommunitiesTracker.unfavorite(community) - UIAccessibility.post(notification: .announcement, argument: "Unfavorited \(community.name)") - Task { - await notifier.add(.success("Unfavorited \(community.name)")) - } - } else { - favoriteCommunitiesTracker.favorite(community) - UIAccessibility.post(notification: .announcement, argument: "Favorited \(community.name)") - Task { - await notifier.add(.success("Favorited \(community.name)")) - } - } - } - - private func isFavorited() -> Bool { - favoriteCommunitiesTracker.isFavorited(community) - } - - private func subscribe(communityId: Int, shouldSubscribe: Bool) async { - communitySubscriptionChanged(community, shouldSubscribe) - } -} - -struct HomepageFeedRowView: View { - let feedType: FeedType - - init(_ feedType: FeedType) { - self.feedType = feedType - } - - var body: some View { - NavigationLink(value: pathValue) { - HStack { - Image(systemName: feedType.iconNameCircle).resizable() - .frame(width: 36, height: 36).foregroundColor(feedType.color) - VStack(alignment: .leading) { - Text("\(feedType.label) Communities") - Text(feedType.description).font(.caption).foregroundColor(.gray) - } - } - .padding(.bottom, 1) - .accessibilityElement(children: .combine) - } - } - - private var pathValue: AnyHashable { - CommunityLinkWithContext(community: nil, feedType: feedType) - } -} +// struct CommuntiyFeedRowView: View { +// @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker +// @Dependency(\.hapticManager) var hapticManager +// @Dependency(\.notifier) var notifier +// +// let community: APICommunity +// let subscribed: Bool +// let communitySubscriptionChanged: (APICommunity, Bool) -> Void +// let navigationContext: NavigationContext +// +// var body: some View { +// NavigationLink(value: pathValue) { +// HStack { +// // NavigationLink with invisible array +// communityNameLabel +// +// Spacer() +// Button("Favorite Community") { +// hapticManager.play(haptic: .success, priority: .high) +// +// toggleFavorite() +// +// }.buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) +// .accessibilityHidden(true) +// } +// }.swipeActions { +// if subscribed { +// Button("Unsubscribe") { +// Task(priority: .userInitiated) { +// await subscribe(communityId: community.id, shouldSubscribe: false) +// } +// }.tint(.red) // Destructive role seems to remove from list so just make it red +// } else { +// Button("Subscribe") { +// Task(priority: .userInitiated) { +// await subscribe(communityId: community.id, shouldSubscribe: true) +// } +// }.tint(.blue) +// } +// } +// .accessibilityAction(named: "Toggle favorite") { +// toggleFavorite() +// } +// .accessibilityElement(children: .combine) +// .accessibilityLabel(communityLabel) +// } +// +// private var pathValue: AnyHashable { +// if navigationContext == .sidebar { +// return CommunityLinkWithContext(community: CommunityModel(from: community), feedType: .subscribed) +// } else { +// // Do not use enum route path in sidebar: It doesn't work, and I have no idea why =/ [2023.09] +// // return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) +// return AppRoute.community(CommunityModel(from: community)) +// } +// } +// +// private var communityNameText: Text { +// Text(community.name) +// } +// +// @ViewBuilder +// private var communityNameLabel: some View { +// if let website = community.actorId.host(percentEncoded: false) { +// communityNameText + +// Text("@\(website)") +// .font(.footnote) +// .foregroundColor(.gray.opacity(0.5)) +// } else { +// communityNameText +// } +// } +// +// private var communityLabel: String { +// var label = community.name +// +// if let website = community.actorId.host(percentEncoded: false) { +// label += "@\(website)" +// } +// +// if isFavorited() { +// label += ", is a favorite" +// } +// +// return label +// } +// +// private func toggleFavorite() { +// if isFavorited() { +// favoriteCommunitiesTracker.unfavorite(community) +// UIAccessibility.post(notification: .announcement, argument: "Unfavorited \(community.name)") +// Task { +// await notifier.add(.success("Unfavorited \(community.name)")) +// } +// } else { +// favoriteCommunitiesTracker.favorite(community) +// UIAccessibility.post(notification: .announcement, argument: "Favorited \(community.name)") +// Task { +// await notifier.add(.success("Favorited \(community.name)")) +// } +// } +// } +// +// private func isFavorited() -> Bool { +// favoriteCommunitiesTracker.isFavorited(community) +// } +// +// private func subscribe(communityId: Int, shouldSubscribe: Bool) async { +// communitySubscriptionChanged(community, shouldSubscribe) +// } +// } +// +// struct HomepageFeedRowView: View { +// let feedType: FeedType +// +// init(_ feedType: FeedType) { +// self.feedType = feedType +// } +// +// var body: some View { +// NavigationLink(value: pathValue) { +// HStack { +// Image(systemName: feedType.iconNameCircle).resizable() +// .frame(width: 36, height: 36).foregroundColor(feedType.color) +// VStack(alignment: .leading) { +// Text("\(feedType.label) Communities") +// Text(feedType.description).font(.caption).foregroundColor(.gray) +// } +// } +// .padding(.bottom, 1) +// .accessibilityElement(children: .combine) +// } +// } +// +// private var pathValue: AnyHashable { +// CommunityLinkWithContext(community: nil, feedType: feedType) +// } +// } diff --git a/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift b/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift index 66a2353ab..45028c796 100644 --- a/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/FeedRowView.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI struct FeedRowView: View { - let feedType: NewFeedType + let feedType: FeedType var body: some View { HStack { diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift index 2468ac501..162bdcc4a 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView+Logic.swift @@ -10,7 +10,7 @@ import Foundation extension AggregateFeedView { func genFeedSwitchingFunctions() -> [MenuFunction] { var ret: [MenuFunction] = .init() - NewFeedType.allAggregateFeedCases.forEach { type in + FeedType.allAggregateFeedCases.forEach { type in let (imageName, enabled) = type != postTracker.feedType ? (type.iconName, true) : (type.iconNameFill, false) diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift index e6346c1c7..4f3f075f0 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift @@ -25,7 +25,7 @@ struct AggregateFeedView: View { postTracker.items.first?.id } - init(feedType: NewFeedType) { + init(feedType: FeedType) { // need to grab some stuff from app storage to initialize with @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift index de765c9bb..89f60022b 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI struct SavedFeedView: View { - // TODO: ERIC this should be a multi-feed with saved comments as well + // TODO: ERIC this needs its own tracker type var body: some View { AggregateFeedView(feedType: .saved) diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index 2fd2ef711..1640a5170 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -9,11 +9,14 @@ import Foundation import SwiftUI struct FeedsView: View { + @AppStorage("defaultFeed") var defaultFeed: DefaultFeedType = .subscribed + @Environment(\.scenePhase) var scenePhase @EnvironmentObject var appState: AppState - @State private var selectedFeed: NewFeedType? + @State private var selectedFeed: FeedType? + @State var appeared: Bool = false // tracks whether this is the view's first appearance @StateObject private var communityListModel: CommunityListModel = .init() @@ -22,14 +25,19 @@ struct FeedsView: View { var body: some View { content - // .navigationTitle("Communities") .onAppear { + // on first appearance, immediately navigate to defaultFeed + if !appeared { + appeared = true + selectedFeed = defaultFeed.toFeedType + } + Task(priority: .high) { await communityListModel.load() } } .onChange(of: scenePhase) { newPhase in - if newPhase == .active, let shortcutItem = NewFeedType.fromShortcut(shortcut: shortcutItemToProcess?.type) { + if newPhase == .active, let shortcutItem = FeedType.fromShortcutString(shortcut: shortcutItemToProcess?.type) { selectedFeed = shortcutItem } } @@ -41,7 +49,7 @@ struct FeedsView: View { // Note that NavigationLinks in here update selectedFeed and are handled by the detail switch, not the general navigation handler ZStack(alignment: .trailing) { List(selection: $selectedFeed) { - ForEach([NewFeedType.all, NewFeedType.local, NewFeedType.subscribed, NewFeedType.saved]) { feedType in + ForEach([FeedType.all, FeedType.local, FeedType.subscribed, FeedType.saved]) { feedType in NavigationLink(value: feedType) { FeedRowView(feedType: feedType) } @@ -51,7 +59,7 @@ struct FeedsView: View { ForEach(communityListModel.visibleSections) { section in Section(header: communitySectionHeaderView(for: section)) { ForEach(communityListModel.communities(for: section)) { community in - NavigationLink(value: NewFeedType.community(.init(from: community, subscribed: true))) { + NavigationLink(value: FeedType.community(.init(from: community, subscribed: true))) { CommunityFeedRowView( community: community, subscribed: communityListModel.isSubscribed(to: community), diff --git a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift index 7b2611f77..30e86066b 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift @@ -16,7 +16,7 @@ struct GeneralSettingsView: View { @AppStorage("shouldBlurNsfw") var shouldBlurNsfw: Bool = true @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast - @AppStorage("defaultFeed") var defaultFeed: FeedType = .subscribed + @AppStorage("defaultFeed") var defaultFeed: DefaultFeedType = .subscribed @AppStorage("hapticLevel") var hapticLevel: HapticPriority = .low @AppStorage("upvoteOnSave") var upvoteOnSave: Bool = false @@ -48,8 +48,6 @@ struct GeneralSettingsView: View { settingName: "Upvote on Save", isTicked: $upvoteOnSave ) - } footer: { - Text("You may need to restart the app for Upvote on Save changes to take effect.") } Section { @@ -93,7 +91,7 @@ struct GeneralSettingsView: View { settingIconSystemName: defaultFeed.settingsIconName, settingName: "Default Feed", currentValue: $defaultFeed, - options: FeedType.allCases + options: DefaultFeedType.allCases ) } footer: { Text("The feed to show by default when you open the app.") From fb902b3b628ee620aae067ea3aa7631f50e91208 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:23:51 -0500 Subject: [PATCH 43/69] fixed subscribed indicator --- Mlem.xcodeproj/project.pbxproj | 4 ---- Mlem/ContentView.swift | 1 - ...vironmentValues+FeedColumnVisibility.swift | 20 ------------------- .../EnvironmentValues+FeedType.swift | 4 ++-- .../Feeds/Feed Types/AggregateFeedView.swift | 1 + 5 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 683b47035..703a27559 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -383,7 +383,6 @@ CD46C1F62B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */; }; CD46C1F82B0D0A8A00065953 /* View+ReselectAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */; }; CD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; }; - CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */; }; CD4BAD3B2B4C6C3200A1E726 /* FeedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */; }; CD4BAD3D2B4C6C8E00A1E726 /* FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD3C2B4C6C8E00A1E726 /* FeedType.swift */; }; CD4BAD432B507F2B00A1E726 /* AggregateFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */; }; @@ -924,7 +923,6 @@ CD46C1F52B0D0A5700065953 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = ""; }; CD46C1F72B0D0A8A00065953 /* View+ReselectAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ReselectAction.swift"; sourceTree = ""; }; CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = ""; }; - CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeedColumnVisibility.swift"; sourceTree = ""; }; CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedRowView.swift; sourceTree = ""; }; CD4BAD3C2B4C6C8E00A1E726 /* FeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedType.swift; sourceTree = ""; }; CD4BAD422B507F2B00A1E726 /* AggregateFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregateFeedView.swift; sourceTree = ""; }; @@ -2249,7 +2247,6 @@ CDDCF6462A663849003DA3AC /* EnvironmentValues+TabSelectionHashValue.swift */, CD9A03C52B34D20500C16276 /* EnvironmentValues+Navigation.swift */, CD9A03C72B389F7000C16276 /* EnvironmentValues+FeedType.swift */, - CD4BAD362B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift */, ); path = EnvironmentValues; sourceTree = ""; @@ -3416,7 +3413,6 @@ 03C905CE2B3C8DC400B9082F /* UserView+Logic.swift in Sources */, CDEC95122B5B318B004BA288 /* CommunityFeedView.swift in Sources */, 6372185B2A3A2AAD008C4816 /* APICommunityView.swift in Sources */, - CD4BAD372B4B98BA00A1E726 /* EnvironmentValues+FeedColumnVisibility.swift in Sources */, 030E86442AC6F6D5000283A6 /* SearchBar+NavigationView.swift in Sources */, 637218552A3A2AAD008C4816 /* APITagline.swift in Sources */, 6322A5CB27F77A4D00135D4F /* Loading View.swift in Sources */, diff --git a/Mlem/ContentView.swift b/Mlem/ContentView.swift index 942cb660a..44426dff5 100644 --- a/Mlem/ContentView.swift +++ b/Mlem/ContentView.swift @@ -53,7 +53,6 @@ struct ContentView: View { var body: some View { FancyTabBar(selection: $tabSelection, navigationSelection: $tabNavigation, dragUpGestureCallback: showAccountSwitcherDragCallback) { Group { - // FeedRoot() FeedsView() .fancyTabItem(tag: TabSelection.feeds) { FancyTabBarLabel( diff --git a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift deleted file mode 100644 index 9d32c46fa..000000000 --- a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedColumnVisibility.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// EnvironmentValues+FeedColumnVisibility.swift -// Mlem -// -// Created by Eric Andrews on 2024-01-07. -// - -import Foundation -import SwiftUI - -private struct FeedColumnVisibility: EnvironmentKey { - static let defaultValue: NavigationSplitViewVisibility = .automatic -} - -extension EnvironmentValues { - var feedColumnVisibility: NavigationSplitViewVisibility { - get { self[FeedColumnVisibility.self] } - set { self[FeedColumnVisibility.self] = newValue } - } -} diff --git a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift index e55f8b0c1..82574d125 100644 --- a/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift +++ b/Mlem/Extensions/EnvironmentValues/EnvironmentValues+FeedType.swift @@ -9,11 +9,11 @@ import Foundation import SwiftUI private struct FeedTypeEnvironmentKey: EnvironmentKey { - static let defaultValue: APIListingType? = nil + static let defaultValue: FeedType? = nil } extension EnvironmentValues { - var feedType: APIListingType? { + var feedType: FeedType? { get { self[FeedTypeEnvironmentKey.self] } set { self[FeedTypeEnvironmentKey.self] = newValue } } diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift index 4f3f075f0..b1257a0f2 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift @@ -59,6 +59,7 @@ struct AggregateFeedView: View { var body: some View { content + .environment(\.feedType, postTracker.feedType) .environmentObject(postTracker) .refreshable { await Task { From b4899c0ec2762389a27a0e08feabe991216f93fe Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:25:36 -0500 Subject: [PATCH 44/69] renamed NewPostFilter to PostFilter --- .../Trackers/Feeds/StandardPostTracker.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index a6bbafb12..13881bf7c 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -9,8 +9,8 @@ import Dependencies import Foundation import Nuke -/// Enumeration of reasons a post could be filtered -enum NewPostFilterReason: Hashable { +/// Enumeration of criteria on which to filter a post +enum PostFilter: Hashable { /// Post is filtered because it was read case read @@ -52,7 +52,7 @@ class StandardPostTracker: StandardTracker { var feedType: FeedType private(set) var postSortType: PostSortType - private var filters: [NewPostFilterReason: Int] + private var filters: [PostFilter: Int] // prefetching private let prefetcher = ImagePrefetcher( @@ -177,14 +177,14 @@ class StandardPostTracker: StandardTracker { /// Applies a filter to all items currently in the tracker, but does **NOT** add the filter to the tracker! /// Use in situations where filtering is handled server-side but should be retroactively applied to the current set of posts (e.g., filtering posts from a blocked user or community) /// - Parameter filter: filter to apply - func applyFilter(_ filter: NewPostFilterReason) async { + func applyFilter(_ filter: PostFilter) async { await setItems(items.filter { shouldFilterPost($0, filters: [filter]) == nil }) } /// Adds a filter to the tracker, removing all current posts that do not pass the filter and filtering out all future posts that do not pass the filter. /// Use in situations where filtering is handled client-side (e.g., filtering read posts or keywords) /// - Parameter newFilter: NewPostFilterReason describing the filter to apply - func addFilter(_ newFilter: NewPostFilterReason) async { + func addFilter(_ newFilter: PostFilter) async { guard !filters.keys.contains(newFilter) else { assertionFailure("Cannot apply new filter (already present in filters!)") return @@ -202,7 +202,7 @@ class StandardPostTracker: StandardTracker { } } - func removeFilter(_ filterToRemove: NewPostFilterReason) async { + func removeFilter(_ filterToRemove: PostFilter) async { guard filters.keys.contains(filterToRemove) else { assertionFailure("Cannot remove filter (not present in filters!)") return @@ -216,7 +216,7 @@ class StandardPostTracker: StandardTracker { } } - func getFilteredCount(for filter: NewPostFilterReason) -> Int { + func getFilteredCount(for filter: PostFilter) -> Int { filters[filter, default: 0] } @@ -239,7 +239,7 @@ class StandardPostTracker: StandardTracker { /// Given a post, determines whether it should be filtered /// - Returns: the first reason according to which the post should be filtered, if applicable, or nil if the post should not be filtered - private func shouldFilterPost(_ postModel: PostModel, filters: [NewPostFilterReason]) -> NewPostFilterReason? { + private func shouldFilterPost(_ postModel: PostModel, filters: [PostFilter]) -> PostFilter? { for filter in filters { switch filter { case .read: From b682147f0bb1dffbd72119cd7b425c94eb6ee9c6 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:32:05 -0500 Subject: [PATCH 45/69] removed dead code --- Mlem.xcodeproj/project.pbxproj | 8 +- .../Shared/Composer/PostComposerView.swift | 2 - .../Shared/Posts/ExpandedPostLogic.swift | 4 +- .../CommunityListRowViews.swift | 159 ------------------ .../FavoriteStarButtonStyle.swift | 20 +++ 5 files changed, 26 insertions(+), 167 deletions(-) delete mode 100644 Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift create mode 100644 Mlem/Views/Tabs/Feeds/Community List/FavoriteStarButtonStyle.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 703a27559..1329fb471 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -275,7 +275,7 @@ 6D80037B2A46458800363206 /* Lazy Load Expanded Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D80037A2A46458800363206 /* Lazy Load Expanded Post.swift */; }; 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D8F08FE2A4029AE003EB4FD /* CommunityListSection.swift */; }; 6D91D4552A415994006B8F9A /* CommunityListSidebarEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */; }; - 6D91D4582A4159D8006B8F9A /* CommunityListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */; }; + 6D91D4582A4159D8006B8F9A /* FavoriteStarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D91D4572A4159D8006B8F9A /* FavoriteStarButtonStyle.swift */; }; 6DA61F812A55B83F001EA633 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA61F802A55B83F001EA633 /* SearchView.swift */; }; 6DA61F872A5720EA001EA633 /* RecentSearchesTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA61F862A5720EA001EA633 /* RecentSearchesTracker.swift */; }; 6DA61F892A575DF1001EA633 /* URL+WithIconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA61F882A575DF1001EA633 /* URL+WithIconSize.swift */; }; @@ -818,7 +818,7 @@ 6D80037A2A46458800363206 /* Lazy Load Expanded Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Lazy Load Expanded Post.swift"; sourceTree = ""; }; 6D8F08FE2A4029AE003EB4FD /* CommunityListSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunityListSection.swift; sourceTree = ""; }; 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListSidebarEntry.swift; sourceTree = ""; }; - 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListRowViews.swift; sourceTree = ""; }; + 6D91D4572A4159D8006B8F9A /* FavoriteStarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteStarButtonStyle.swift; sourceTree = ""; }; 6DA61F802A55B83F001EA633 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 6DA61F862A5720EA001EA633 /* RecentSearchesTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSearchesTracker.swift; sourceTree = ""; }; 6DA61F882A575DF1001EA633 /* URL+WithIconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+WithIconSize.swift"; sourceTree = ""; }; @@ -2465,7 +2465,7 @@ isa = PBXGroup; children = ( 6D91D4542A415994006B8F9A /* CommunityListSidebarEntry.swift */, - 6D91D4572A4159D8006B8F9A /* CommunityListRowViews.swift */, + 6D91D4572A4159D8006B8F9A /* FavoriteStarButtonStyle.swift */, 505240E62A88D36D00EA4558 /* SectionIndexTitles.swift */, ); path = "Community List"; @@ -3478,7 +3478,7 @@ CDA217E62A63016A00BDA173 /* ReportMessage.swift in Sources */, CD9DD8832A622A6C0044EA8E /* ReportCommentReply.swift in Sources */, CD3FBCE12A4A836000B2063F /* AllItemsFeedView.swift in Sources */, - 6D91D4582A4159D8006B8F9A /* CommunityListRowViews.swift in Sources */, + 6D91D4582A4159D8006B8F9A /* FavoriteStarButtonStyle.swift in Sources */, 63F0C7B92A0533C700A18C5D /* Add Account View.swift in Sources */, 63E5D3922A13CF2300EC1FBD /* Favorite Community Tracker.swift in Sources */, B1B78D642A51D53900F72485 /* AppDelegate.swift in Sources */, diff --git a/Mlem/Views/Shared/Composer/PostComposerView.swift b/Mlem/Views/Shared/Composer/PostComposerView.swift index 63bd03c7b..fe5fea8b2 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView.swift @@ -32,7 +32,6 @@ struct PostComposerView: View { @Environment(\.dismiss) var dismiss - // let postTracker: PostTracker let editModel: PostEditorModel @AppStorage("promptUser.permission.privacy.allowImageUploads") var askedForPermissionToUploadImages: Bool = false @@ -56,7 +55,6 @@ struct PostComposerView: View { @FocusState private var focusedField: Field? init(editModel: PostEditorModel) { - // self.postTracker = editModel.postTracker self.editModel = editModel self._postTitle = State(initialValue: editModel.editPost?.post.name ?? "") diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index 17d156daf..76fff2b54 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -188,8 +188,8 @@ extension ExpandedPost { do { // Making this request marks unread comments as read. - // post = try await PostModel(from: postRepository.loadPost(postId: post.postId)) - // postTracker.update(with: post) + let newPost = try await PostModel(from: postRepository.loadPost(postId: post.postId)) + post.reinit(from: newPost) let comments = try await commentRepository.comments(for: post.post.id) let sorted = sortComments(comments, by: commentSortingType) diff --git a/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift b/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift deleted file mode 100644 index 261e90c41..000000000 --- a/Mlem/Views/Tabs/Feeds/Community List/CommunityListRowViews.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// CommunityListRowViews.swift -// Mlem -// -// Created by Jake Shirley on 6/19/23. -// - -import Dependencies -import SwiftUI - -struct FavoriteStarButtonStyle: ButtonStyle { - let isFavorited: Bool - - func makeBody(configuration: Configuration) -> some View { - Image(systemName: isFavorited ? Icons.favoriteFill : Icons.favorite) - .foregroundColor(.blue) - .opacity(isFavorited ? 1.0 : 0.2) - .accessibilityRepresentation { configuration.label } - } -} - -// struct CommuntiyFeedRowView: View { -// @Dependency(\.favoriteCommunitiesTracker) var favoriteCommunitiesTracker -// @Dependency(\.hapticManager) var hapticManager -// @Dependency(\.notifier) var notifier -// -// let community: APICommunity -// let subscribed: Bool -// let communitySubscriptionChanged: (APICommunity, Bool) -> Void -// let navigationContext: NavigationContext -// -// var body: some View { -// NavigationLink(value: pathValue) { -// HStack { -// // NavigationLink with invisible array -// communityNameLabel -// -// Spacer() -// Button("Favorite Community") { -// hapticManager.play(haptic: .success, priority: .high) -// -// toggleFavorite() -// -// }.buttonStyle(FavoriteStarButtonStyle(isFavorited: isFavorited())) -// .accessibilityHidden(true) -// } -// }.swipeActions { -// if subscribed { -// Button("Unsubscribe") { -// Task(priority: .userInitiated) { -// await subscribe(communityId: community.id, shouldSubscribe: false) -// } -// }.tint(.red) // Destructive role seems to remove from list so just make it red -// } else { -// Button("Subscribe") { -// Task(priority: .userInitiated) { -// await subscribe(communityId: community.id, shouldSubscribe: true) -// } -// }.tint(.blue) -// } -// } -// .accessibilityAction(named: "Toggle favorite") { -// toggleFavorite() -// } -// .accessibilityElement(children: .combine) -// .accessibilityLabel(communityLabel) -// } -// -// private var pathValue: AnyHashable { -// if navigationContext == .sidebar { -// return CommunityLinkWithContext(community: CommunityModel(from: community), feedType: .subscribed) -// } else { -// // Do not use enum route path in sidebar: It doesn't work, and I have no idea why =/ [2023.09] -// // return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) -// return AppRoute.community(CommunityModel(from: community)) -// } -// } -// -// private var communityNameText: Text { -// Text(community.name) -// } -// -// @ViewBuilder -// private var communityNameLabel: some View { -// if let website = community.actorId.host(percentEncoded: false) { -// communityNameText + -// Text("@\(website)") -// .font(.footnote) -// .foregroundColor(.gray.opacity(0.5)) -// } else { -// communityNameText -// } -// } -// -// private var communityLabel: String { -// var label = community.name -// -// if let website = community.actorId.host(percentEncoded: false) { -// label += "@\(website)" -// } -// -// if isFavorited() { -// label += ", is a favorite" -// } -// -// return label -// } -// -// private func toggleFavorite() { -// if isFavorited() { -// favoriteCommunitiesTracker.unfavorite(community) -// UIAccessibility.post(notification: .announcement, argument: "Unfavorited \(community.name)") -// Task { -// await notifier.add(.success("Unfavorited \(community.name)")) -// } -// } else { -// favoriteCommunitiesTracker.favorite(community) -// UIAccessibility.post(notification: .announcement, argument: "Favorited \(community.name)") -// Task { -// await notifier.add(.success("Favorited \(community.name)")) -// } -// } -// } -// -// private func isFavorited() -> Bool { -// favoriteCommunitiesTracker.isFavorited(community) -// } -// -// private func subscribe(communityId: Int, shouldSubscribe: Bool) async { -// communitySubscriptionChanged(community, shouldSubscribe) -// } -// } -// -// struct HomepageFeedRowView: View { -// let feedType: FeedType -// -// init(_ feedType: FeedType) { -// self.feedType = feedType -// } -// -// var body: some View { -// NavigationLink(value: pathValue) { -// HStack { -// Image(systemName: feedType.iconNameCircle).resizable() -// .frame(width: 36, height: 36).foregroundColor(feedType.color) -// VStack(alignment: .leading) { -// Text("\(feedType.label) Communities") -// Text(feedType.description).font(.caption).foregroundColor(.gray) -// } -// } -// .padding(.bottom, 1) -// .accessibilityElement(children: .combine) -// } -// } -// -// private var pathValue: AnyHashable { -// CommunityLinkWithContext(community: nil, feedType: feedType) -// } -// } diff --git a/Mlem/Views/Tabs/Feeds/Community List/FavoriteStarButtonStyle.swift b/Mlem/Views/Tabs/Feeds/Community List/FavoriteStarButtonStyle.swift new file mode 100644 index 000000000..45c5c1f25 --- /dev/null +++ b/Mlem/Views/Tabs/Feeds/Community List/FavoriteStarButtonStyle.swift @@ -0,0 +1,20 @@ +// +// FavoriteStarButtonStyle.swift +// Mlem +// +// Created by Jake Shirley on 6/19/23. +// + +import Dependencies +import SwiftUI + +struct FavoriteStarButtonStyle: ButtonStyle { + let isFavorited: Bool + + func makeBody(configuration: Configuration) -> some View { + Image(systemName: isFavorited ? Icons.favoriteFill : Icons.favorite) + .foregroundColor(.blue) + .opacity(isFavorited ? 1.0 : 0.2) + .accessibilityRepresentation { configuration.label } + } +} From d8e801146eaedaa3022553dde422e5831e5d860a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:41:26 -0500 Subject: [PATCH 46/69] made swiftlint mean again --- Mlem.xcodeproj/project.pbxproj | 2 +- Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift | 3 --- Mlem/Models/Trackers/Feeds/StandardPostTracker.swift | 6 +++++- Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 4 ++++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 1329fb471..f843b3b0d 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -2988,7 +2988,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint lint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint lint --strict\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index fab2eacb5..961f401a5 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -22,7 +22,6 @@ struct HandleLemmyLinksDisplay: ViewModifier { @AppStorage("upvoteOnSave") var upvoteOnSave = false - // swiftlint:disable function_body_length // swiftlint:disable:next cyclomatic_complexity func body(content: Content) -> some View { content @@ -65,8 +64,6 @@ struct HandleLemmyLinksDisplay: ViewModifier { } } } - - // swiftlint:enable function_body_length @ViewBuilder // swiftlint:disable:next cyclomatic_complexity diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 13881bf7c..c8bff2359 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -169,7 +169,11 @@ class StandardPostTracker: StandardTracker { } } - @available(*, deprecated, message: "Compatibility function for UserView. Should be removed and UserView refactored to use new multi-trackers.") + @available( + *, + deprecated, + message: "Compatibility function for UserView. Should be removed and UserView refactored to use new multi-trackers." + ) func reset(with newPosts: [PostModel]) async { await setItems(newPosts) } diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index bd173471b..4ba46f1e8 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -9,6 +9,8 @@ import Dependencies import Foundation import SwiftUI +// swiftlint:disable type_body_length + /// View for a single community struct CommunityFeedView: View { enum Tab: String, Identifiable, CaseIterable { @@ -333,3 +335,5 @@ struct CommunityFeedView: View { } } } + +// swiftlint:enable type_body_length From 43a770d0eb8be4b1f78b97b8bd3db805d3a323a1 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 16:46:37 -0500 Subject: [PATCH 47/69] lint --- .../Content/Community/CommunityModel+MenuFunctions.swift | 4 ++-- Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 3 +-- Mlem/Views/Tabs/Search/Results/CommunityResultView.swift | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift index 08a09b94b..a6f924a76 100644 --- a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift +++ b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift @@ -86,9 +86,9 @@ extension CommunityModel { } func menuFunctions( - _ callback: @escaping (_ item: Self) -> Void = { _ in }, editorTracker: EditorTracker? = nil, - postTracker: StandardPostTracker? = nil + postTracker: StandardPostTracker? = nil, + _ callback: @escaping (_ item: Self) -> Void = { _ in } ) -> [MenuFunction] { var functions: [MenuFunction] = .init() if let editorTracker { diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 4ba46f1e8..505acd04f 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -119,10 +119,9 @@ struct CommunityFeedView: View { ToolbarItemGroup(placement: .secondaryAction) { ForEach( communityModel.menuFunctions( - { communityModel = $0 }, editorTracker: editorTracker, postTracker: postTracker - ) + ) { communityModel = $0 } ) { menuFunction in MenuButton(menuFunction: menuFunction, confirmDestructive: confirmDestructive) } diff --git a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift index 6891f59e3..460b99b07 100644 --- a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift @@ -5,8 +5,8 @@ // Created by Sjmarf on 18/09/2023. // -import SwiftUI import Dependencies +import SwiftUI struct CommunityResultView: View { @Dependency(\.apiClient) private var apiClient @@ -139,8 +139,8 @@ struct CommunityResultView: View { .contextMenu { ForEach( community.menuFunctions( - trackerCallback, - editorTracker: editorTracker + editorTracker: editorTracker, + trackerCallback ) ) { item in MenuButton(menuFunction: item, confirmDestructive: confirmDestructive) From 189706b33c3b0f74481c357720ee8b3520731260 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 21:56:36 -0500 Subject: [PATCH 48/69] fixed switching post tabs reloading feed --- .../Feeds/Components/PostFeedView+Logic.swift | 20 ++++------- .../Tabs/Feeds/Components/PostFeedView.swift | 34 +++++++++++++------ .../Feeds/Feed Types/CommunityFeedView.swift | 1 + 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index 86c93081a..63875622e 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -9,19 +9,13 @@ import Dependencies import SwiftUI extension PostFeedView { - func setDefaultSortMode() { - @Dependency(\.siteInformation) var siteInformationn - - @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot - @AppStorage("fallbackDefaultPostSorting") var fallbackDefaultPostSorting: PostSortType = .hot - - if let siteVersion = siteInformation.version, siteVersion < defaultPostSorting.minimumVersion { - postSortType = fallbackDefaultPostSorting - } else { - postSortType = defaultPostSorting - } - - if siteInformation.version != nil { + func setDefaultSortMode() async { + if let siteVersion = siteInformation.version, !siteVersionResolved { + let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting + + // manually change the tracker sort type here so that view is not redrawn by `onChange(of: postSortType)` + await postTracker.changeSortType(to: newPostSort) + postSortType = newPostSort siteVersionResolved = true } } diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 487b1991e..8b9baf87e 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -26,9 +26,21 @@ struct PostFeedView: View { @Binding var postSortType: PostSortType let showCommunity: Bool - @State var siteVersionResolved: Bool = false + @State var siteVersionResolved: Bool + @State var shouldPerformInitialLoad: Bool // used to display appropriate loading indicator when version resolved on initial appear + @State var errorDetails: ErrorDetails? + init(postSortType: Binding, showCommunity: Bool) { + @Dependency(\.siteInformation) var siteInformation + + let siteVersionResolved = siteInformation.version != nil + self._siteVersionResolved = .init(wrappedValue: siteVersionResolved) + self._shouldPerformInitialLoad = .init(wrappedValue: siteVersionResolved) + self._postSortType = postSortType + self.showCommunity = showCommunity + } + var body: some View { content .onChange(of: showReadPosts) { newValue in @@ -38,17 +50,17 @@ struct PostFeedView: View { Task { await postTracker.addFilter(.read) } } } - .task(id: siteInformation.version) { - // when site version changes, check if it's resolved; if so, update sort type and siteVersionResolved - if let siteVersion = siteInformation.version, !siteVersionResolved { - let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting - - // manually change the tracker sort type here so that view is not redrawn by `onChange(of: postSortType)` - await postTracker.changeSortType(to: newPostSort, forceRefresh: true) - postSortType = newPostSort - siteVersionResolved = true + .task { + if shouldPerformInitialLoad { + if postTracker.items.isEmpty { + await postTracker.loadMoreItems() + } + shouldPerformInitialLoad = false } } + .task(id: siteInformation.version) { + await setDefaultSortMode() + } .onChange(of: postSortType) { newValue in Task { await postTracker.changeSortType(to: newValue) } } @@ -107,7 +119,7 @@ struct PostFeedView: View { private func noPostsView() -> some View { VStack { // don't show posts until site information loads to avoid jarring redraw - if postTracker.loadingState == .loading || !siteVersionResolved { + if postTracker.loadingState == .loading || !siteVersionResolved || shouldPerformInitialLoad { LoadingView(whatIsLoading: .posts) .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 505acd04f..7ccfeba5b 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -97,6 +97,7 @@ struct CommunityFeedView: View { .refreshable { await Task { do { + print("DEBUG refreshing from refreshable") _ = try await postTracker.refresh(clearBeforeRefresh: false) } catch { errorHandler.handle(error) From e7f699ea18b3c7571023dd6d0cee747b24bdb504 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 22:08:41 -0500 Subject: [PATCH 49/69] fixed community list not being fancy tab scroll compatible --- Mlem/Views/Tabs/Feeds/FeedsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index 1640a5170..dd7f7097e 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -75,6 +75,7 @@ struct FeedsView: View { .scrollIndicators(.hidden) .navigationTitle("Communities") .listStyle(PlainListStyle()) + .fancyTabScrollCompatible() SectionIndexTitles(proxy: scrollProxy, communitySections: communityListModel.allSections()) } From 9374e35c3585aecc910bfd0bdfbb8a3f30ad4438 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Mon, 22 Jan 2024 22:16:29 -0500 Subject: [PATCH 50/69] file ordering --- Mlem.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index f843b3b0d..9e0b32a0e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -2434,8 +2434,8 @@ children = ( 03EF1D0B2B434CB10056175C /* CommunityStatsView.swift */, CD4BAD3A2B4C6C3200A1E726 /* FeedRowView.swift */, - CDBCBA1F2B537A4B0070F60D /* PostFeedView.swift */, CDBCBA232B54A5F40070F60D /* NoPostsView.swift */, + CDBCBA1F2B537A4B0070F60D /* PostFeedView.swift */, CDCA28D32B58AF53009D9F54 /* PostFeedView+MenuFunctions.swift */, CDEC95182B5D950D004BA288 /* PostFeedView+Logic.swift */, ); From 099fe0bbe92c50fcc06b1dfe0a5d89e502525408 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Tue, 23 Jan 2024 17:45:06 -0500 Subject: [PATCH 51/69] fixed crash on opening with saved as default --- Mlem/API/Models/Site/APIMyUserInfo.swift | 6 ++++ .../Trackers/SiteInformationTracker.swift | 2 +- .../Tabs/Feeds/Feed Types/SavedFeedView.swift | 36 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Mlem/API/Models/Site/APIMyUserInfo.swift b/Mlem/API/Models/Site/APIMyUserInfo.swift index 163c5ba5e..a6cd7e6da 100644 --- a/Mlem/API/Models/Site/APIMyUserInfo.swift +++ b/Mlem/API/Models/Site/APIMyUserInfo.swift @@ -13,3 +13,9 @@ struct APIMyUserInfo: Decodable { var localUserView: APILocalUserView var discussionLanguages: [Int] } + +extension APIMyUserInfo: Equatable { + static func == (lhs: APIMyUserInfo, rhs: APIMyUserInfo) -> Bool { + lhs.localUserView.person.id == rhs.localUserView.person.id + } +} diff --git a/Mlem/Models/Trackers/SiteInformationTracker.swift b/Mlem/Models/Trackers/SiteInformationTracker.swift index 0fd58be25..04024e052 100644 --- a/Mlem/Models/Trackers/SiteInformationTracker.swift +++ b/Mlem/Models/Trackers/SiteInformationTracker.swift @@ -23,7 +23,6 @@ class SiteInformationTracker: ObservableObject { version = account.siteVersion Task { do { - let response = try await apiClient.loadSiteInformation() enableDownvotes = response.siteView.localSite.enableDownvotes version = SiteVersion(response.version) @@ -35,6 +34,7 @@ class SiteInformationTracker: ObservableObject { } myUserInfo = response.myUser allLanguages = response.allLanguages + print("DEBUG version loaded") } catch { errorHandler.handle(error) } diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift index 89f60022b..83d2aa742 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/SavedFeedView.swift @@ -5,13 +5,45 @@ // Created by Eric Andrews on 2024-01-21. // +import Dependencies import Foundation import SwiftUI struct SavedFeedView: View { - // TODO: ERIC this needs its own tracker type + // TODO: ERIC this whole view needs its own PR--needs its own tracker to handle loading user content, needs a different type of feed to handle mixed posts and comments, and needs a good way of determining the current user ID + + @Dependency(\.siteInformation) var siteInformation + @Dependency(\.errorHandler) var errorHandler + + // ugly little hack to deal with the fact that dependencies don't propagate state changes nicely but we need to listen for siteInformation.myUserInfo to resolve + @State var siteInformationLoaded: Bool + + init() { + @Dependency(\.siteInformation) var siteInformation + + _siteInformationLoaded = .init(wrappedValue: siteInformation.myUserInfo != nil) + } var body: some View { - AggregateFeedView(feedType: .saved) + // note to reviewers: this is super ugly but exists just to get the app in a stable running state pending the aforementioned PR to make this view nice + if !siteInformationLoaded { + LoadingView(whatIsLoading: .posts) + .task { + for _ in 0 ..< 5 { + if siteInformation.myUserInfo != nil { + siteInformationLoaded = true + break + } + + do { + try await Task.sleep(nanoseconds: 1_000_000_000) + } catch { + errorHandler.handle(error) + } + } + } + } else { + AggregateFeedView(feedType: .saved) + } } } From bf0b8e244463a08f86a8a8ef2caef3035ef9905c Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Tue, 23 Jan 2024 17:47:46 -0500 Subject: [PATCH 52/69] removed dead code --- Mlem/API/Models/Site/APIMyUserInfo.swift | 6 ------ Mlem/Models/Trackers/SiteInformationTracker.swift | 1 - 2 files changed, 7 deletions(-) diff --git a/Mlem/API/Models/Site/APIMyUserInfo.swift b/Mlem/API/Models/Site/APIMyUserInfo.swift index a6cd7e6da..163c5ba5e 100644 --- a/Mlem/API/Models/Site/APIMyUserInfo.swift +++ b/Mlem/API/Models/Site/APIMyUserInfo.swift @@ -13,9 +13,3 @@ struct APIMyUserInfo: Decodable { var localUserView: APILocalUserView var discussionLanguages: [Int] } - -extension APIMyUserInfo: Equatable { - static func == (lhs: APIMyUserInfo, rhs: APIMyUserInfo) -> Bool { - lhs.localUserView.person.id == rhs.localUserView.person.id - } -} diff --git a/Mlem/Models/Trackers/SiteInformationTracker.swift b/Mlem/Models/Trackers/SiteInformationTracker.swift index 04024e052..ffd0c81a3 100644 --- a/Mlem/Models/Trackers/SiteInformationTracker.swift +++ b/Mlem/Models/Trackers/SiteInformationTracker.swift @@ -34,7 +34,6 @@ class SiteInformationTracker: ObservableObject { } myUserInfo = response.myUser allLanguages = response.allLanguages - print("DEBUG version loaded") } catch { errorHandler.handle(error) } From 115ed062c6aae84aa84a02610bc20d068627b06b Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Tue, 23 Jan 2024 18:42:52 -0500 Subject: [PATCH 53/69] changed how initial sort type is handled --- .../Feeds/Components/PostFeedView+Logic.swift | 5 +-- .../Tabs/Feeds/Components/PostFeedView.swift | 44 ++++++++++--------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index 63875622e..6b28a748d 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -10,13 +10,12 @@ import SwiftUI extension PostFeedView { func setDefaultSortMode() async { - if let siteVersion = siteInformation.version, !siteVersionResolved { + if let siteVersion = siteInformation.version { let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting - // manually change the tracker sort type here so that view is not redrawn by `onChange(of: postSortType)` + // manually change the tracker sort type here so that view is not redrawn by `task(id: internalPostSortType)` await postTracker.changeSortType(to: newPostSort) postSortType = newPostSort - siteVersionResolved = true } } } diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 8b9baf87e..33ca07213 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -23,20 +23,25 @@ struct PostFeedView: View { @EnvironmentObject var postTracker: StandardPostTracker @EnvironmentObject var appState: AppState - @Binding var postSortType: PostSortType + // used to actually drive post loading; when nil, indicates that the site version is unresolved and it is not safe to load posts + @State var versionSafePostSort: PostSortType? + @Binding var postSortType: PostSortType { + didSet { + versionSafePostSort = postSortType + } + } + let showCommunity: Bool - - @State var siteVersionResolved: Bool - @State var shouldPerformInitialLoad: Bool // used to display appropriate loading indicator when version resolved on initial appear - + @State var errorDetails: ErrorDetails? init(postSortType: Binding, showCommunity: Bool) { @Dependency(\.siteInformation) var siteInformation - let siteVersionResolved = siteInformation.version != nil - self._siteVersionResolved = .init(wrappedValue: siteVersionResolved) - self._shouldPerformInitialLoad = .init(wrappedValue: siteVersionResolved) + if let siteVersion = siteInformation.version, postSortType.wrappedValue.minimumVersion <= siteVersion { + self._versionSafePostSort = .init(wrappedValue: postSortType.wrappedValue) + } + self._postSortType = postSortType self.showCommunity = showCommunity } @@ -50,22 +55,19 @@ struct PostFeedView: View { Task { await postTracker.addFilter(.read) } } } - .task { - if shouldPerformInitialLoad { - if postTracker.items.isEmpty { - await postTracker.loadMoreItems() - } - shouldPerformInitialLoad = false - } - } .task(id: siteInformation.version) { await setDefaultSortMode() } - .onChange(of: postSortType) { newValue in - Task { await postTracker.changeSortType(to: newValue) } + .task(id: versionSafePostSort) { + if let versionSafePostSort { + await postTracker.changeSortType( + to: versionSafePostSort, + forceRefresh: postTracker.items.isEmpty + ) + } } .toolbar { - if siteVersionResolved { + if versionSafePostSort != nil { if postTracker.feedType != .saved { ToolbarItem(placement: .primaryAction) { sortMenu } } @@ -87,7 +89,7 @@ struct PostFeedView: View { var content: some View { LazyVStack(spacing: 0) { - if postTracker.items.isEmpty || !siteVersionResolved { + if postTracker.items.isEmpty || versionSafePostSort == nil { noPostsView() } else { ForEach(postTracker.items, id: \.uid) { feedPost(for: $0) } @@ -119,7 +121,7 @@ struct PostFeedView: View { private func noPostsView() -> some View { VStack { // don't show posts until site information loads to avoid jarring redraw - if postTracker.loadingState == .loading || !siteVersionResolved || shouldPerformInitialLoad { + if postTracker.loadingState == .loading || versionSafePostSort == nil { LoadingView(whatIsLoading: .posts) .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) From 5b551b8d7616b1f18abfb8589e47c73d4bfa35c1 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Tue, 23 Jan 2024 18:59:51 -0500 Subject: [PATCH 54/69] masked bad noPostsView behavior --- Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 33ca07213..855650f2f 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -30,6 +30,9 @@ struct PostFeedView: View { versionSafePostSort = postSortType } } + + // If versionSafePostSort is defined at init, the post tracker won't detect that and start loading until a fraction of a second after the view draws; if the tracker is also empty, this leads to noPostsView flashing for a fraction of a second. This masks that behavior. + @State var suppressNoPostsView: Bool = true let showCommunity: Bool @@ -48,6 +51,7 @@ struct PostFeedView: View { var body: some View { content + .animation(.easeOut(duration: 0.2), value: postTracker.items.isEmpty) .onChange(of: showReadPosts) { newValue in if newValue { Task { await postTracker.removeFilter(.read) } @@ -59,6 +63,8 @@ struct PostFeedView: View { await setDefaultSortMode() } .task(id: versionSafePostSort) { + defer { suppressNoPostsView = false } + if let versionSafePostSort { await postTracker.changeSortType( to: versionSafePostSort, @@ -121,7 +127,7 @@ struct PostFeedView: View { private func noPostsView() -> some View { VStack { // don't show posts until site information loads to avoid jarring redraw - if postTracker.loadingState == .loading || versionSafePostSort == nil { + if postTracker.loadingState == .loading || versionSafePostSort == nil || suppressNoPostsView { LoadingView(whatIsLoading: .posts) .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) From 60826ffbcf6d4b4aeb8f4823ccef861345fd310f Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 14:53:49 -0500 Subject: [PATCH 55/69] added 0.17 compatibility decoder back into ListingType --- Mlem/API/Models/ListingType.swift | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Mlem/API/Models/ListingType.swift b/Mlem/API/Models/ListingType.swift index fbd70f967..dfa6e9d11 100644 --- a/Mlem/API/Models/ListingType.swift +++ b/Mlem/API/Models/ListingType.swift @@ -12,4 +12,42 @@ enum APIListingType: String, Codable { case local = "Local" case subscribed = "Subscribed" case moderatorView = "ModeratorView" + + // Pre 0.18.0 it appears that they used integers instead of strings here. We can remove this intialiser once we drop support for old versions. To fully support both systems, we'd also need to *encode* back into the correct integer or string format. I'd rather not go through the effort for instance versions that most people don't use any more, so I've disabled the option to edit account settings on instances running <0.18.0 + // - sjmarf + + // TODO: 0.17 deprecation remove this initialiser + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let stringValue = try? container.decode(String.self) { + guard let value = APIListingType(rawValue: stringValue) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Value not one of \"All\", \"Local\" or \"Subscribed\"." + ) + } + self = value + } else if let intValue = try? container.decode(Int.self) { + guard 0 ... 2 ~= intValue else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Must be an integer in range 0...2." + ) + } + switch intValue { + case 0: + self = .all + case 1: + self = .local + default: + self = .subscribed + } + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid value" + ) + } + } } From 2d3229da510fb0cd932024129e8f53d5ce56767c Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 15:11:01 -0500 Subject: [PATCH 56/69] neatened up shortcut handling --- Mlem/MlemApp.swift | 56 +++++++++------------------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/Mlem/MlemApp.swift b/Mlem/MlemApp.swift index 95d62c874..a177ea32f 100644 --- a/Mlem/MlemApp.swift +++ b/Mlem/MlemApp.swift @@ -73,53 +73,17 @@ struct MlemApp: App { private func setupAppShortcuts() { guard accountsTracker.savedAccounts.first != nil else { return } - - // Subscribed Feed - let subscribedIcon = UIApplicationShortcutIcon(systemImageName: Icons.subscribedFeed) - let subscribedFeedItem = UIApplicationShortcutItem( - type: FeedType.subscribed.toShortcutString, - localizedTitle: "Subscribed", - localizedSubtitle: nil, - icon: subscribedIcon, - userInfo: nil - ) - - // Local Feed - let localIcon = UIApplicationShortcutIcon(systemImageName: Icons.localFeed) - let localFeedItem = UIApplicationShortcutItem( - type: FeedType.local.toShortcutString, - localizedTitle: "Local", - localizedSubtitle: nil, - icon: localIcon, - userInfo: nil - ) - - // All Feed - let allIcon = UIApplicationShortcutIcon(systemImageName: Icons.federatedFeed) - let allFeedItem = UIApplicationShortcutItem( - type: FeedType.all.toShortcutString, - localizedTitle: "All", - localizedSubtitle: nil, - icon: allIcon, - userInfo: nil - ) - // Saved Feed - let savedIcon = UIApplicationShortcutIcon(systemImageName: Icons.savedFeed) - let savedFeedItem = UIApplicationShortcutItem( - type: FeedType.saved.toShortcutString, - localizedTitle: "Saved", - localizedSubtitle: nil, - icon: savedIcon, - userInfo: nil - ) - - UIApplication.shared.shortcutItems = [ - allFeedItem, - localFeedItem, - subscribedFeedItem, - savedFeedItem - ] + UIApplication.shared.shortcutItems = FeedType.allAggregateFeedCases.map { feedType in + let icon = UIApplicationShortcutIcon(systemImageName: feedType.iconName) + return UIApplicationShortcutItem( + type: feedType.toShortcutString, + localizedTitle: feedType.label, + localizedSubtitle: nil, + icon: icon, + userInfo: nil + ) + } } /// A variable describing the initial flow the application should run after start-up From 6494ae1a92da88f8febfce48626e1c2667ff8ef2 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 15:20:55 -0500 Subject: [PATCH 57/69] re-enabled lemmy link resolution on feeds --- Mlem/Views/Tabs/Feeds/FeedsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index dd7f7097e..cdc57b397 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -41,6 +41,7 @@ struct FeedsView: View { selectedFeed = shortcutItem } } + .handleLemmyLinkResolution(navigationPath: .constant(feedTabNavigation)) } var content: some View { From d481347b16d0d0a522e110136cd4f3efe5ec43b2 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 15:26:42 -0500 Subject: [PATCH 58/69] fixed (yet another) post sort issue --- Mlem/Models/Content/Post Model.swift | 2 +- Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index f46c1f7f6..6c2aac56c 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -21,7 +21,7 @@ class PostModel: ContentIdentifiable, ObservableObject { var community: CommunityModel @Published var votes: VotesModel var commentCount: Int - var unreadCommentCount: Int + @Published var unreadCommentCount: Int @Published var saved: Bool @Published var read: Bool @Published var deleted: Bool diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index 6b28a748d..ec13cf2bf 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -10,7 +10,8 @@ import SwiftUI extension PostFeedView { func setDefaultSortMode() async { - if let siteVersion = siteInformation.version { + print("DEBUG setting default sort mode") + if let siteVersion = siteInformation.version, versionSafePostSort == nil { let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting // manually change the tracker sort type here so that view is not redrawn by `task(id: internalPostSortType)` From a9addd2a1442442a9bfd6b97de770fb853707492 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 16:01:11 -0500 Subject: [PATCH 59/69] fixed unread count and read status not properly propagating --- Mlem/Models/Trackers/Feeds/StandardPostTracker.swift | 1 - Mlem/Views/Shared/Components/Thumbnail Image View.swift | 2 +- Mlem/Views/Shared/Posts/Expanded Post.swift | 6 ++++-- Mlem/Views/Shared/Posts/ExpandedPostLogic.swift | 3 ++- Mlem/Views/Shared/Posts/Feed Post.swift | 5 ----- Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift | 2 +- Mlem/Views/Shared/Posts/Post Sizes/Headline Post.swift | 2 +- Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift | 2 +- Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift | 1 - Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 1 - 10 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index c8bff2359..b2f995a6e 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -142,7 +142,6 @@ class StandardPostTracker: StandardTracker { func changeSortType(to newSortType: PostSortType, forceRefresh: Bool = false) async { // don't do anything if sort type not changed guard postSortType != newSortType || forceRefresh else { - print("DEBUG sort type unchanged and forceRefresh false, will not reload feed") return } diff --git a/Mlem/Views/Shared/Components/Thumbnail Image View.swift b/Mlem/Views/Shared/Components/Thumbnail Image View.swift index a99232325..b1a88f763 100644 --- a/Mlem/Views/Shared/Components/Thumbnail Image View.swift +++ b/Mlem/Views/Shared/Components/Thumbnail Image View.swift @@ -16,7 +16,7 @@ struct ThumbnailImageView: View { @Dependency(\.postRepository) var postRepository @Environment(\.openURL) private var openURL - let post: PostModel + @ObservedObject var post: PostModel var showNsfwFilter: Bool { (post.post.nsfw || post.community.nsfw) && shouldBlurNsfw } diff --git a/Mlem/Views/Shared/Posts/Expanded Post.swift b/Mlem/Views/Shared/Posts/Expanded Post.swift index 994cca1f4..ecf2e7bfb 100644 --- a/Mlem/Views/Shared/Posts/Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Expanded Post.swift @@ -81,8 +81,10 @@ struct ExpandedPost: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { toolbarMenu } } - .task { await loadComments() } - .task { await post.markRead(true) } + .task { + await loadComments() + await post.markRead(true) + } .refreshable { await refreshComments() } .onChange(of: commentSortingType) { newSortingType in withAnimation(.easeIn(duration: 0.4)) { diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index 76fff2b54..b80f49532 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -187,8 +187,9 @@ extension ExpandedPost { isLoading = true do { - // Making this request marks unread comments as read. + // Making this request should mark unread comments as read, but doesn't appear to so we do it manually let newPost = try await PostModel(from: postRepository.loadPost(postId: post.postId)) + newPost.unreadCommentCount = 0 post.reinit(from: newPost) let comments = try await commentRepository.comments(for: post.post.id) diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index f20f642db..f3a42fbc4 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -50,11 +50,6 @@ struct FeedPost: View { @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @Environment(\.horizontalSizeClass) var horizontalSizeClass - @State var dirtyVote: ScoringOperation = .resetVote - @State var dirtyScore: Int = 0 - @State var dirtySaved: Bool = false - @State var dirty: Bool = false - // MARK: Parameters @ObservedObject var postModel: PostModel diff --git a/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift index 74a9eeb66..3a32aee4e 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Compact Post.swift @@ -30,7 +30,7 @@ struct CompactPost: View { private let spacing: CGFloat = 10 // constant for readability, ease of modification // arguments - let post: PostModel + @ObservedObject var post: PostModel let community: CommunityModel? let showCommunity: Bool // true to show community name, false to show username let menuFunctions: [MenuFunction] diff --git a/Mlem/Views/Shared/Posts/Post Sizes/Headline Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Headline Post.swift index d3ac1ebd8..e3647f5e3 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Headline Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Headline Post.swift @@ -20,7 +20,7 @@ struct HeadlinePost: View { private let spacing: CGFloat = 10 // constant for readability, ease of modification // arguments - let post: PostModel + @ObservedObject var post: PostModel var body: some View { VStack(alignment: .leading, spacing: AppConstants.postAndCommentSpacing) { diff --git a/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift index 9f9b824a0..8420b4f00 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift @@ -48,7 +48,7 @@ struct LargePost: View { @AppStorage("limitImageHeightInFeed") var limitImageHeightInFeed: Bool = true // parameters - let post: PostModel + @ObservedObject var post: PostModel @Binding var layoutMode: LayoutMode private var isExpanded: Bool { diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index ec13cf2bf..4b8320442 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -10,7 +10,6 @@ import SwiftUI extension PostFeedView { func setDefaultSortMode() async { - print("DEBUG setting default sort mode") if let siteVersion = siteInformation.version, versionSafePostSort == nil { let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 7ccfeba5b..505acd04f 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -97,7 +97,6 @@ struct CommunityFeedView: View { .refreshable { await Task { do { - print("DEBUG refreshing from refreshable") _ = try await postTracker.refresh(clearBeforeRefresh: false) } catch { errorHandler.handle(error) From 60358b4b9b085842b40318f54a81e3e6b794ff65 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 16:55:36 -0500 Subject: [PATCH 60/69] fixed load flicker on community --- Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift | 1 + Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index 4b8320442..d911f4305 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -11,6 +11,7 @@ import SwiftUI extension PostFeedView { func setDefaultSortMode() async { if let siteVersion = siteInformation.version, versionSafePostSort == nil { + print("DEBUG changing sort mode") let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting // manually change the tracker sort type here so that view is not redrawn by `task(id: internalPostSortType)` diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 505acd04f..30e363694 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -80,10 +80,6 @@ struct CommunityFeedView: View { var body: some View { content .onAppear { - if postTracker.items.isEmpty { - Task { await postTracker.loadMoreItems() } - } - if communityModel.moderators == nil { Task(priority: .userInitiated) { do { From 374a9c2b18b98eb0395a07baff1e0e451ac8471e Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Wed, 24 Jan 2024 16:57:03 -0500 Subject: [PATCH 61/69] Update Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift Co-authored-by: Sjmarf <78750526+Sjmarf@users.noreply.github.com> --- Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift index b1257a0f2..8d4c504fc 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/AggregateFeedView.swift @@ -138,7 +138,7 @@ struct AggregateFeedView: View { .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 5) - + .padding(.bottom, 3) Divider() } } From 6fac55296edd7b93cf13307682989817f2b8464a Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 15:21:56 -0500 Subject: [PATCH 62/69] added padding on top of noPostsView --- Mlem.xcodeproj/project.pbxproj | 2 + Mlem/Info.plist | 2 - .../Community List/SectionIndexTitles.swift | 95 +++++++++++-------- .../Tabs/Feeds/Components/NoPostsView.swift | 36 +++---- .../Tabs/Feeds/Components/PostFeedView.swift | 1 - 5 files changed, 76 insertions(+), 60 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 8294fd375..ab67c4751 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -3662,6 +3662,7 @@ INFOPLIST_FILE = Mlem/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Mlem; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -3703,6 +3704,7 @@ INFOPLIST_FILE = Mlem/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Mlem; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Mlem/Info.plist b/Mlem/Info.plist index 51709c328..a4a5a42b1 100644 --- a/Mlem/Info.plist +++ b/Mlem/Info.plist @@ -19,8 +19,6 @@ ITSAppUsesNonExemptEncryption - NSPhotoLibraryAddUsageDescription - NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift b/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift index 61d31d111..ec59ce464 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/SectionIndexTitles.swift @@ -9,7 +9,6 @@ import Dependencies import SwiftUI -// Original article here: https://www.fivestars.blog/code/section-title-index-swiftui.html struct SectionIndexTitles: View { @Dependency(\.hapticManager) var hapticManager @@ -24,59 +23,75 @@ struct SectionIndexTitles: View { var body: some View { VStack { ForEach(communitySections) { communitySection in - HStack { - if let icon = communitySection.sidebarEntry.sidebarIcon { - SectionIndexImage(image: icon) - } else if let label = communitySection.sidebarEntry.sidebarLabel { - SectionIndexText(label: label) - } else { - EmptyView() - } - } - .background(dragObserver(viewId: communitySection.viewId)) + sectionTitle(for: communitySection) + .frame(width: 12, height: 6) } } - .padding(2) - .padding(.top, 4) + .overlay { + GeometryReader { geo in + // Color.clear doesn't register gestures (presumably because it never gets drawn), so we fake it + Color.black + .opacity(0.00000000001) + .gesture( + DragGesture(minimumDistance: 0, coordinateSpace: .local) + .updating($dragLocation) { value, _, _ in + // ignore if out of bounds--actually add a tiny bit of padding to the left side to make it feel right + guard value.location.x > -20.0, value.location.y >= 0.0, value.location.y <= geo.size.height else { + return + } + + // compute which section is currently dragged + // height of one section is communitySections.count / geo.size.height + // drag is thus (value.location.y / (communitySections.count / geo.size.height )) sections up + // then do some algebra to make it prettier and round down to int + let sectionIndex = Int((value.location.y * Double(communitySections.count)) / geo.size.height) + + guard sectionIndex < communitySections.count else { + assertionFailure("Invalid section index! The math must be wrong.") + return + } + + let sectionLabel = communitySections[sectionIndex].viewId + + if sectionLabel != lastSelectedLabel { + DispatchQueue.main.async { + lastSelectedLabel = sectionLabel + proxy.scrollTo(sectionLabel, anchor: .center) + + // Play nice tappy taps + hapticManager.play(haptic: .rigidInfo, priority: .low) + } + } + } + ) + } + } + .padding(.vertical, 6) .background { Capsule() .foregroundStyle(.ultraThinMaterial) } - .gesture( - DragGesture(minimumDistance: 0, coordinateSpace: .global) - .updating($dragLocation) { value, state, _ in - state = value.location - } - ) - } - - func dragObserver(viewId: String) -> some View { - GeometryReader { geometry in - dragObserver(geometry: geometry, viewId: viewId) - } } +} - func dragObserver(geometry: GeometryProxy, viewId: String) -> some View { - if geometry.frame(in: .global).contains(dragLocation) { - if viewId != lastSelectedLabel { - DispatchQueue.main.async { - lastSelectedLabel = viewId - proxy.scrollTo(viewId, anchor: .center) - - // Play nice tappy taps - hapticManager.play(haptic: .rigidInfo, priority: .low) - } - } - } - return Rectangle().fill(Color.clear) +// Sidebar Label Views +@ViewBuilder +func sectionTitle(for communitySection: CommunityListSection) -> some View { + if let icon = communitySection.sidebarEntry.sidebarIcon { + SectionIndexImage(image: icon) + } else if let label = communitySection.sidebarEntry.sidebarLabel { + SectionIndexText(label: label) + } else { + EmptyView() } } -// Sidebar Label Views struct SectionIndexText: View { let label: String var body: some View { - Text(label).font(.system(size: 11)).fontWeight(.semibold) + Text(label) + .font(.system(size: 11)) + .fontWeight(.semibold) } } diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index 8e741bc03..84650f3e8 100644 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -17,28 +17,32 @@ struct NoPostsView: View { var body: some View { VStack { if loadingState != .loading { - VStack(alignment: .center, spacing: AppConstants.postAndCommentSpacing) { + VStack(alignment: .center, spacing: 0) { let unreadItems = postTracker.getFilteredCount(for: .read) Image(systemName: Icons.noPosts) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35) - .padding(.bottom, 12) - .frame(width: unreadItems == 0 ? 35 : 50) - .padding(.bottom, unreadItems == 0 ? 8 : 12) - Text(title) - - if unreadItems != 0 { - Text( - "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." - ) - .foregroundStyle(.tertiary) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, 20) + .padding(.vertical, 35) + .padding(.top, 10) // offsets the illusion of whitespace created by lowercase letters below + + VStack(spacing: AppConstants.postAndCommentSpacing) { + Text(title) + + if unreadItems != 0 { + Text( + "\(unreadItems) read post\(unreadItems == 1 ? " has" : "s have") been hidden." + ) + .foregroundStyle(.tertiary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, 20) + } + + buttons + .padding(.top) } - buttons } .foregroundStyle(.secondary) } @@ -74,7 +78,5 @@ struct NoPostsView: View { } .foregroundStyle(.secondary) .buttonStyle(.bordered) - .padding(.top) - .padding(.horizontal, 20) } } diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 855650f2f..cf732352b 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -137,7 +137,6 @@ struct PostFeedView: View { } else { NoPostsView(loadingState: postTracker.loadingState, postSortType: $postSortType, showReadPosts: $showReadPosts) .transition(.scale(scale: 0.9).combined(with: .opacity)) - .padding(.top, 25) } } .animation(.easeOut(duration: 0.1), value: postTracker.loadingState) From abac03bdcbf05181dcb80aaa888febf0231279ce Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 15:28:14 -0500 Subject: [PATCH 63/69] removed stale tests --- .../Tabs/Feeds/Components/NoPostsView.swift | 2 +- MlemTests/Navigation/RoutableTests.swift | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index 84650f3e8..df395617c 100644 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -18,7 +18,7 @@ struct NoPostsView: View { VStack { if loadingState != .loading { VStack(alignment: .center, spacing: 0) { - let unreadItems = postTracker.getFilteredCount(for: .read) + let unreadItems = 0 // postTracker.getFilteredCount(for: .read) Image(systemName: Icons.noPosts) .resizable() diff --git a/MlemTests/Navigation/RoutableTests.swift b/MlemTests/Navigation/RoutableTests.swift index 7b7224043..ca505592c 100644 --- a/MlemTests/Navigation/RoutableTests.swift +++ b/MlemTests/Navigation/RoutableTests.swift @@ -9,7 +9,6 @@ import XCTest final class RoutableTests: XCTestCase { - private enum MockRoute: Routable { case routeA case routeB(Int) @@ -51,22 +50,4 @@ final class RoutableTests: XCTestCase { let data = "Mock Unsupported Value" XCTAssertThrowsError(try MockRoute.routeC(.makeRoute(data))) } - - // MARK: - AppRoutes - - /// Passing in raw data value should return a valid route. - /// Assert `(Data) –> Route`. - func testNavigationRouteHandlesDataValue() throws { - let value = CommunityLinkWithContext(community: nil, feedType: .all) - let route = try AppRoute.makeRoute(value) - XCTAssert(route == .communityLinkWithContext(value)) - } - - /// Passing in a route enum with an associated value should return the passed in value. - func testNavigationRouteHandlesNonNestedAssociatedValueEnumCase() throws { - let data = CommunityLinkWithContext(community: nil, feedType: .all) - let value = AppRoute.communityLinkWithContext(data) - let route = try AppRoute.makeRoute(value) - XCTAssert(route == value) - } } From 66ba3aac749801d16ecca84e27f0109b1dfaed47 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 15:29:09 -0500 Subject: [PATCH 64/69] removed testing code --- Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index df395617c..84650f3e8 100644 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -18,7 +18,7 @@ struct NoPostsView: View { VStack { if loadingState != .loading { VStack(alignment: .center, spacing: 0) { - let unreadItems = 0 // postTracker.getFilteredCount(for: .read) + let unreadItems = postTracker.getFilteredCount(for: .read) Image(systemName: Icons.noPosts) .resizable() From 4e56fc6b997ac2ef4a745dd385f38e79fdd35439 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 15:54:33 -0500 Subject: [PATCH 65/69] fixed noPostsView not properly reloading posts --- Mlem/Models/Trackers/Feeds/StandardPostTracker.swift | 2 ++ Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift | 6 ++++-- .../Tabs/Feeds/Components/PostFeedView+Logic.swift | 1 - Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift | 10 ++++++++-- Mlem/Views/Tabs/Feeds/FeedsView.swift | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index b2f995a6e..88adcb691 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -145,6 +145,8 @@ class StandardPostTracker: StandardTracker { return } + print("DEBUG changing sort type in post tracker") + postSortType = newSortType do { try await refresh(clearBeforeRefresh: true) diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index 84650f3e8..9483398e0 100644 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -11,7 +11,9 @@ struct NoPostsView: View { @EnvironmentObject var postTracker: StandardPostTracker let loadingState: LoadingState - @Binding var postSortType: PostSortType + let postSortType: PostSortType + // this isn't the most elegant but passing a nested binding doesn't seem to propagate changes correctly [Eric 2024.01.25] + let switchToHot: () -> Void @Binding var showReadPosts: Bool var body: some View { @@ -61,7 +63,7 @@ struct NoPostsView: View { VStack { if postSortType != .hot { Button { - postSortType = .hot + switchToHot() } label: { Label("Switch to Hot", systemImage: Icons.hotSort) } diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift index d911f4305..4b8320442 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView+Logic.swift @@ -11,7 +11,6 @@ import SwiftUI extension PostFeedView { func setDefaultSortMode() async { if let siteVersion = siteInformation.version, versionSafePostSort == nil { - print("DEBUG changing sort mode") let newPostSort = siteVersion < defaultPostSorting.minimumVersion ? fallbackDefaultPostSorting : defaultPostSorting // manually change the tracker sort type here so that view is not redrawn by `task(id: internalPostSortType)` diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index cf732352b..733ccdd57 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -27,6 +27,7 @@ struct PostFeedView: View { @State var versionSafePostSort: PostSortType? @Binding var postSortType: PostSortType { didSet { + print("DEBUG detected post sort type change") versionSafePostSort = postSortType } } @@ -135,8 +136,13 @@ struct PostFeedView: View { ErrorView(errorDetails) .frame(maxWidth: .infinity) } else { - NoPostsView(loadingState: postTracker.loadingState, postSortType: $postSortType, showReadPosts: $showReadPosts) - .transition(.scale(scale: 0.9).combined(with: .opacity)) + NoPostsView( + loadingState: postTracker.loadingState, + postSortType: postSortType, + switchToHot: { postSortType = .hot }, + showReadPosts: $showReadPosts + ) + .transition(.scale(scale: 0.9).combined(with: .opacity)) } } .animation(.easeOut(duration: 0.1), value: postTracker.loadingState) diff --git a/Mlem/Views/Tabs/Feeds/FeedsView.swift b/Mlem/Views/Tabs/Feeds/FeedsView.swift index cdc57b397..55da4a1d0 100644 --- a/Mlem/Views/Tabs/Feeds/FeedsView.swift +++ b/Mlem/Views/Tabs/Feeds/FeedsView.swift @@ -74,7 +74,7 @@ struct FeedsView: View { .padding(.trailing, 10) } .scrollIndicators(.hidden) - .navigationTitle("Communities") + .navigationTitle("Feeds") .listStyle(PlainListStyle()) .fancyTabScrollCompatible() From 6d440d1c2ab38744f0d7c2de5019a2daf11ae2cd Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 16:00:13 -0500 Subject: [PATCH 66/69] smoothed some animations and removed test prints --- Mlem/Models/Trackers/Feeds/StandardPostTracker.swift | 2 -- Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift | 2 +- Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift | 12 +++++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift index 88adcb691..b2f995a6e 100644 --- a/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift +++ b/Mlem/Models/Trackers/Feeds/StandardPostTracker.swift @@ -145,8 +145,6 @@ class StandardPostTracker: StandardTracker { return } - print("DEBUG changing sort type in post tracker") - postSortType = newSortType do { try await refresh(clearBeforeRefresh: true) diff --git a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift index 9483398e0..e5449b12f 100644 --- a/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/NoPostsView.swift @@ -12,9 +12,9 @@ struct NoPostsView: View { let loadingState: LoadingState let postSortType: PostSortType + @Binding var showReadPosts: Bool // this isn't the most elegant but passing a nested binding doesn't seem to propagate changes correctly [Eric 2024.01.25] let switchToHot: () -> Void - @Binding var showReadPosts: Bool var body: some View { VStack { diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 733ccdd57..584d77f97 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -27,7 +27,6 @@ struct PostFeedView: View { @State var versionSafePostSort: PostSortType? @Binding var postSortType: PostSortType { didSet { - print("DEBUG detected post sort type change") versionSafePostSort = postSortType } } @@ -136,16 +135,15 @@ struct PostFeedView: View { ErrorView(errorDetails) .frame(maxWidth: .infinity) } else { - NoPostsView( - loadingState: postTracker.loadingState, - postSortType: postSortType, - switchToHot: { postSortType = .hot }, - showReadPosts: $showReadPosts - ) + NoPostsView(loadingState: postTracker.loadingState, postSortType: postSortType, showReadPosts: $showReadPosts) { + suppressNoPostsView = true + postSortType = .hot + } .transition(.scale(scale: 0.9).combined(with: .opacity)) } } .animation(.easeOut(duration: 0.1), value: postTracker.loadingState) + .animation(.easeOut(duration: 0.1), value: suppressNoPostsView) } @ViewBuilder From ae3d5938a54eefffa2f3c2fde23f12a2f1ba4dac Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 16:18:03 -0500 Subject: [PATCH 67/69] fixed moderator flair not appearing --- Mlem/Views/Shared/Links/User/UserLinkView.swift | 1 + Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift | 6 ++++-- Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Mlem/Views/Shared/Links/User/UserLinkView.swift b/Mlem/Views/Shared/Links/User/UserLinkView.swift index 7a22c22c5..d8367a193 100644 --- a/Mlem/Views/Shared/Links/User/UserLinkView.swift +++ b/Mlem/Views/Shared/Links/User/UserLinkView.swift @@ -52,6 +52,7 @@ struct UserLinkView: View { self.postContext = postContext self.commentContext = commentContext self.communityContext = communityContext + print(communityContext?.moderators) } var body: some View { diff --git a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift index 584d77f97..6c05dd232 100644 --- a/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Components/PostFeedView.swift @@ -35,10 +35,11 @@ struct PostFeedView: View { @State var suppressNoPostsView: Bool = true let showCommunity: Bool + let communityContext: CommunityModel? @State var errorDetails: ErrorDetails? - init(postSortType: Binding, showCommunity: Bool) { + init(postSortType: Binding, showCommunity: Bool, communityContext: CommunityModel? = nil) { @Dependency(\.siteInformation) var siteInformation if let siteVersion = siteInformation.version, postSortType.wrappedValue.minimumVersion <= siteVersion { @@ -47,6 +48,7 @@ struct PostFeedView: View { self._postSortType = postSortType self.showCommunity = showCommunity + self.communityContext = communityContext } var body: some View { @@ -111,7 +113,7 @@ struct PostFeedView: View { NavigationLink(.postLinkWithContext(.init(post: post, community: nil, postTracker: postTracker))) { FeedPost( post: post, - community: post.community, + community: communityContext, showPostCreator: shouldShowPostCreator, showCommunity: showCommunity ) diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 30e363694..31369c019 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -84,6 +84,8 @@ struct CommunityFeedView: View { Task(priority: .userInitiated) { do { communityModel = try await communityRepository.loadDetails(for: communityModel.communityId) + print("DEBUG loaded details") + print("DEBUG moderators: \(communityModel.moderators)") } catch { errorHandler.handle(error) } @@ -162,7 +164,7 @@ struct CommunityFeedView: View { } func posts() -> some View { - PostFeedView(postSortType: $postSortType, showCommunity: false) + PostFeedView(postSortType: $postSortType, showCommunity: false, communityContext: communityModel) .environmentObject(postTracker) } From 625a580671abb9103b5db37c0b5530a21b5dadca Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 16:18:23 -0500 Subject: [PATCH 68/69] removed test prints --- Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift index 31369c019..f1f8b7964 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Types/CommunityFeedView.swift @@ -84,8 +84,6 @@ struct CommunityFeedView: View { Task(priority: .userInitiated) { do { communityModel = try await communityRepository.loadDetails(for: communityModel.communityId) - print("DEBUG loaded details") - print("DEBUG moderators: \(communityModel.moderators)") } catch { errorHandler.handle(error) } From 350012ea475151a76f6a6e2d5576257ddb8952ec Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Thu, 25 Jan 2024 16:19:04 -0500 Subject: [PATCH 69/69] removed more test prints --- Mlem/Views/Shared/Links/User/UserLinkView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mlem/Views/Shared/Links/User/UserLinkView.swift b/Mlem/Views/Shared/Links/User/UserLinkView.swift index d8367a193..7a22c22c5 100644 --- a/Mlem/Views/Shared/Links/User/UserLinkView.swift +++ b/Mlem/Views/Shared/Links/User/UserLinkView.swift @@ -52,7 +52,6 @@ struct UserLinkView: View { self.postContext = postContext self.commentContext = commentContext self.communityContext = communityContext - print(communityContext?.moderators) } var body: some View {