diff --git a/Localization/app.json b/Localization/app.json index edd92ac78..7f5343a06 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -481,6 +481,9 @@ "lists": { "title": "Lists", "manage_lists": "Manage Lists" + }, + "hashtags": { + "title": "Followed Hashtags" } }, "timeline_pill": { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index b78ec0287..f42934d42 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -127,7 +127,6 @@ extension HashtagTimelineViewModel.State { Task { do { let response = try await viewModel.context.apiService.hashtagTimeline( - domain: viewModel.authContext.mastodonAuthenticationBox.domain, maxID: maxID, hashtag: viewModel.hashtag, authenticationBox: viewModel.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index fd85b426f..18f70e17e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -143,6 +143,9 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media case .list: showLocalTimelineAction.state = .off showFollowingAction.state = .off + case .hashtag: + showLocalTimelineAction.state = .off + showFollowingAction.state = .off } } @@ -190,7 +193,43 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media children: [listsSubmenu] ) - let listsDivider = UIMenu(title: "", options: .displayInline, children: [listsMenu]) + let hashtagsSubmenu = UIDeferredMenuElement.uncached { [weak self] callback in + guard let self else { return callback([]) } + + Task { @MainActor in + let lists = (try? await Mastodon.API.Account.followedTags( + session: .shared, + domain: self.authContext.mastodonAuthenticationBox.domain, + query: .init(limit: nil), + authorization: self.authContext.mastodonAuthenticationBox.userAuthorization + ).singleOutput().value) ?? [] + + let listEntries = lists.map { entry in + return LabeledAction(title: entry.name, image: nil, handler: { [weak self] in + guard let self, let viewModel = self.viewModel else { return } + viewModel.timelineContext = .hashtag(entry.name) + viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.ContextSwitch.self) + timelineSelectorButton.setAttributedTitle( + .init(string: entry.name, attributes: [ + .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + ]), + for: .normal) + timelineSelectorButton.sizeToFit() + timelineSelectorButton.menu = generateTimelineSelectorMenu() + }).menuElement + } + + callback(listEntries) + } + } + + let hashtagsMenu = UIMenu( + title: L10n.Scene.HomeTimeline.TimelineMenu.Hashtags.title, + image: UIImage(systemName: "number"), + children: [hashtagsSubmenu] + ) + + let listsDivider = UIMenu(title: "", options: .displayInline, children: [listsMenu, hashtagsMenu]) return UIMenu(children: [showFollowingAction, showLocalTimelineAction, listsDivider]) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index af732ce00..648a1f1ff 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -138,6 +138,11 @@ extension HomeTimelineViewModel.LoadLatestState { query: .init(sinceID: sinceID), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) + case let .hashtag(tag): + response = try await viewModel.context.apiService.hashtagTimeline( + hashtag: tag, + authenticationBox: viewModel.authContext.mastodonAuthenticationBox + ) } enter(state: Idle.self) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index f0c30177d..12f4a77c3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -80,6 +80,11 @@ extension HomeTimelineViewModel.LoadOldestState { query: .init(local: true, maxID: maxID), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) + case let .hashtag(tag): + response = try await viewModel.context.apiService.hashtagTimeline( + hashtag: tag, + authenticationBox: viewModel.authContext.mastodonAuthenticationBox + ) } let statuses = response.value diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 8876badc5..1c3f81cae 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -180,6 +180,11 @@ extension HomeTimelineViewModel { query: .init(local: true, maxID: status.id), authenticationBox: authContext.mastodonAuthenticationBox ) + case let .hashtag(tag): + response = try? await context.apiService.hashtagTimeline( + hashtag: tag, + authenticationBox: authContext.mastodonAuthenticationBox + ) } // insert missing items diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index e59724c8f..beaa8d56b 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -190,6 +190,11 @@ private extension FeedDataController { query: .init(maxID: maxID), authenticationBox: authContext.mastodonAuthenticationBox ) + case let .hashtag(tag): + response = try await context.apiService.hashtagTimeline( + hashtag: tag, + authenticationBox: authContext.mastodonAuthenticationBox + ) } return response.value.map { .fromStatus(.fromEntity($0), kind: .home) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index 4edd34bf3..2292b4cfd 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -14,7 +14,6 @@ import MastodonSDK extension APIService { public func hashtagTimeline( - domain: String, sinceID: Mastodon.Entity.Status.ID? = nil, maxID: Mastodon.Entity.Status.ID? = nil, limit: Int = onceRequestStatusMaxCount, diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 4e1ab79e7..77085540e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -862,6 +862,10 @@ public enum L10n { public static let following = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Following", fallback: "Following") /// Local public static let localCommunity = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.LocalCommunity", fallback: "Local") + public enum Hashtags { + /// Followed Hashtags + public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Hashtags.Title", fallback: "Followed Hashtags") + } public enum Lists { /// Manage Lists public static let manageLists = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Lists.ManageLists", fallback: "Manage Lists") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 84975d950..f313aac7b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -307,6 +307,7 @@ uploaded to Mastodon."; "Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local"; "Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists"; "Scene.HomeTimeline.TimelineMenu.Lists.ManageLists" = "Manage Lists"; +"Scene.HomeTimeline.TimelineMenu.Hashtags.Title" = "Followed Hashtags"; "Scene.HomeTimeline.TimelinePill.NewPosts" = "New Posts"; "Scene.HomeTimeline.TimelinePill.Offline" = "Offline"; "Scene.HomeTimeline.TimelinePill.PostSent" = "Post Sent"; diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 9fb4a0c8c..33208fcc6 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -15,6 +15,7 @@ public final class MastodonFeed { case home case `public` case list(String) + case hashtag(String) } } diff --git a/WidgetExtension/Variants/Hashtag/HashtagWidget.swift b/WidgetExtension/Variants/Hashtag/HashtagWidget.swift index 0545816fe..534d02412 100644 --- a/WidgetExtension/Variants/Hashtag/HashtagWidget.swift +++ b/WidgetExtension/Variants/Hashtag/HashtagWidget.swift @@ -53,7 +53,7 @@ extension HashtagWidgetProvider { do { let mostRecentStatuses = try await WidgetExtension.appContext .apiService - .hashtagTimeline(domain: authBox.domain, limit: 40, hashtag: desiredHashtag, authenticationBox: authBox) + .hashtagTimeline(limit: 40, hashtag: desiredHashtag, authenticationBox: authBox) .value let filteredStatuses: [Mastodon.Entity.Status]