diff --git a/IceCubesApp/App/AppRouter.swift b/IceCubesApp/App/AppRouter.swift index 0d93b766..bdc72bb5 100644 --- a/IceCubesApp/App/AppRouter.swift +++ b/IceCubesApp/App/AppRouter.swift @@ -83,8 +83,8 @@ extension View { AddRemoteTimelineView() .withEnvironments() case .addTagGroup: - AddTagGroupView() - .withEnvironments() + AddTagGroupView() + .withEnvironments() case let .statusEditHistory(status): StatusEditHistoryView(statusId: status) .withEnvironments() diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 3173d8e0..d3eb90a5 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -97,9 +97,9 @@ struct SideBarView: View { private var tabsView: some View { ForEach(tabs) { tab in Button { - //ensure keyboard is always dismissed when selecting a tab + // ensure keyboard is always dismissed when selecting a tab hideKeyboard() - + if tab == selectedTab { popToRootTab = .other DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { @@ -176,8 +176,8 @@ private struct SideBarIcon: View { } extension View { - func hideKeyboard() { - let resign = #selector(UIResponder.resignFirstResponder) - UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil) - } + func hideKeyboard() { + let resign = #selector(UIResponder.resignFirstResponder) + UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil) + } } diff --git a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift index 4e0c72f2..489784f3 100644 --- a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift @@ -27,7 +27,7 @@ struct ContentSettingsView: View { Text("settings.content.media.show.alt") } }.listRowBackground(theme.primaryBackgroundColor) - + Section("settings.content.sharing") { Picker("settings.content.sharing.share-button-behavior", selection: $userPreferences.shareButtonBehavior) { ForEach(PreferredShareButtonBehavior.allCases, id: \.rawValue) { option in diff --git a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift index c7e8be8f..fd817676 100644 --- a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift @@ -13,7 +13,7 @@ class DisplaySettingsLocalValues: ObservableObject { @Published var labelColor = Theme.shared.labelColor @Published var lineSpacing = Theme.shared.lineSpacing @Published var fontSizeScale = Theme.shared.fontSizeScale - + private let debouncesDelay: DispatchQueue.SchedulerTimeType.Stride = .seconds(0.5) private var subscriptions = Set() diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 497a2b03..3f1a1230 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -139,9 +139,9 @@ struct SettingsTabs: View { NavigationLink(destination: remoteLocalTimelinesView) { Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right") } - NavigationLink(destination: tagGroupsView) { - Label("timeline.filter.tag-groups", systemImage: "number") - } + NavigationLink(destination: tagGroupsView) { + Label("timeline.filter.tag-groups", systemImage: "number") + } NavigationLink(destination: ContentSettingsView()) { Label("settings.general.content", systemImage: "rectangle.stack") } @@ -264,36 +264,36 @@ struct SettingsTabs: View { } } } - - private var tagGroupsView: some View { - Form { - ForEach(preferences.tagGroups, id: \.self) { group in - Text(group.title) - } - .onDelete { indexes in - if let index = indexes.first { - _ = preferences.tagGroups.remove(at: index) - } - } - .onMove { source, destination in - preferences.tagGroups.move(fromOffsets: source, toOffset: destination) - } - .listRowBackground(theme.primaryBackgroundColor) - - Button { - routerPath.presentedSheet = .addTagGroup - } label: { - Label("timeline.filter.add-tag-groups", systemImage: "plus") - } - .listRowBackground(theme.primaryBackgroundColor) + + private var tagGroupsView: some View { + Form { + ForEach(preferences.tagGroups, id: \.self) { group in + Text(group.title) } - .navigationTitle("timeline.filter.tag-groups") - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) - .toolbar { - EditButton() + .onDelete { indexes in + if let index = indexes.first { + _ = preferences.tagGroups.remove(at: index) + } } + .onMove { source, destination in + preferences.tagGroups.move(fromOffsets: source, toOffset: destination) + } + .listRowBackground(theme.primaryBackgroundColor) + + Button { + routerPath.presentedSheet = .addTagGroup + } label: { + Label("timeline.filter.add-tag-groups", systemImage: "plus") + } + .listRowBackground(theme.primaryBackgroundColor) } + .navigationTitle("timeline.filter.tag-groups") + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + .toolbar { + EditButton() + } + } private var remoteLocalTimelinesView: some View { Form { @@ -317,7 +317,7 @@ struct SettingsTabs: View { .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) .toolbar { - EditButton() + EditButton() } } diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index a3489830..22f08b57 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -38,7 +38,7 @@ struct TranslationSettingsView: View { .listRowBackground(theme.primaryBackgroundColor) } } - + Section { Toggle(isOn: preferences.$autoDetectPostLanguage) { Text("settings.translation.auto-detect-post-language") @@ -46,7 +46,6 @@ struct TranslationSettingsView: View { } footer: { Text("settings.translation.auto-detect-post-language-footer") } - } .navigationTitle("settings.translation.navigation-title") .scrollContentBackground(.hidden) diff --git a/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift b/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift index bce5ef21..5ec7d585 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift @@ -8,190 +8,188 @@ import Shimmer import SwiftUI struct AddTagGroupView: View { - @Environment(\.dismiss) private var dismiss - - @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var theme: Theme - - @State private var title: String = "" - @State private var sfSymbolName: String = "" - @State private var tags: [String] = [] - @State private var newTag: String = "" - - private var canSave: Bool { - !title.isEmpty && - // At least have 2 tags, one main and one additional. - tags.count >= 2 - } - - @FocusState private var focusedField: Focus? - - enum Focus { - case title - case symbol - case new - } - - var body: some View { - NavigationStack { - Form { - metadataSection - keywordsSection - } - .formStyle(.grouped) - .navigationTitle("timeline.filter.add-tag-groups") - .navigationBarTitleDisplayMode(.inline) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) - .scrollDismissesKeyboard(.immediately) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("action.cancel", action: { dismiss() }) - } - ToolbarItem(placement: .navigationBarTrailing) { - Button("action.save", action: { save() }) - .disabled(!canSave) - } - } - .onAppear { - focusedField = .title - } - .overlay(alignment: .bottom) { - symbolsSuggestionView - } + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject private var preferences: UserPreferences + @EnvironmentObject private var theme: Theme + + @State private var title: String = "" + @State private var sfSymbolName: String = "" + @State private var tags: [String] = [] + @State private var newTag: String = "" + + private var canSave: Bool { + !title.isEmpty && + // At least have 2 tags, one main and one additional. + tags.count >= 2 + } + + @FocusState private var focusedField: Focus? + + enum Focus { + case title + case symbol + case new + } + + var body: some View { + NavigationStack { + Form { + metadataSection + keywordsSection + } + .formStyle(.grouped) + .navigationTitle("timeline.filter.add-tag-groups") + .navigationBarTitleDisplayMode(.inline) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + .scrollDismissesKeyboard(.immediately) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("action.cancel", action: { dismiss() }) } - } - - @ViewBuilder - private var metadataSection: some View { - Section { - TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal) - .focused($focusedField, equals: Focus.title) - .onSubmit { - focusedField = Focus.symbol - } - - HStack { - TextField("add-tag-groups.edit.icon.field", text: $sfSymbolName, axis: .horizontal) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .focused($focusedField, equals: Focus.symbol) - .onSubmit { - focusedField = Focus.new - } - .onChange(of: sfSymbolName) { name in - popupTagsPresented = true - } - - Image(systemName: sfSymbolName) - } + ToolbarItem(placement: .navigationBarTrailing) { + Button("action.save", action: { save() }) + .disabled(!canSave) } + } + .onAppear { + focusedField = .title + } + .overlay(alignment: .bottom) { + symbolsSuggestionView + } } - - @State private var popupTagsPresented = false - - private var keywordsSection: some View { - Section("add-tag-groups.edit.tags") { - ForEach(tags, id: \.self) { tag in - HStack { - Text(tag) - Spacer() - Button { - deleteTag(tag) - } label: { - Image(systemName: "trash") - .tint(.red) - } - } - } - .onDelete { indexes in - if let index = indexes.first { - let tag = tags[index] - deleteTag(tag) - } - } - HStack { - TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .onSubmit { - addNewTag() - } - .focused($focusedField, equals: Focus.new) - Spacer() - if !newTag.isEmpty { - Button { - addNewTag() - } label: { - Image(systemName: "checkmark.circle.fill") - .tint(.green) - } - } - } + } + + @ViewBuilder + private var metadataSection: some View { + Section { + TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal) + .focused($focusedField, equals: Focus.title) + .onSubmit { + focusedField = Focus.symbol } - .listRowBackground(theme.primaryBackgroundColor) + + HStack { + TextField("add-tag-groups.edit.icon.field", text: $sfSymbolName, axis: .horizontal) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .focused($focusedField, equals: Focus.symbol) + .onSubmit { + focusedField = Focus.new + } + .onChange(of: sfSymbolName) { _ in + popupTagsPresented = true + } + + Image(systemName: sfSymbolName) + } } - - private func addNewTag() { - addTag(newTag.trimmingCharacters(in: .whitespaces)) - newTag = "" - focusedField = Focus.new - } - - private func addTag(_ tag: String) { - guard !tag.isEmpty else { return } - tags.append(tag) - } - - private func deleteTag(_ tag: String) { - tags.removeAll(where: { $0 == tag }) - } - - private func save() { - var toSave = tags - let main = toSave.removeFirst() - preferences.tagGroups.append(.init( - title: title.trimmingCharacters(in: .whitespaces), - sfSymbolName: sfSymbolName, - main: main, - additional: toSave - )) - - dismiss() - } - - @ViewBuilder - private var symbolsSuggestionView: some View { - if focusedField == .symbol && !sfSymbolName.isEmpty { - let filteredMatches = allSymbols - .filter { $0.contains(sfSymbolName) } - if !filteredMatches.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - ForEach(filteredMatches, id: \.self) { symbolName in - Button { - sfSymbolName = symbolName - } label: { - Image(systemName: symbolName) - } - } - } - .padding(.horizontal, .layoutPadding) - } - .frame(height: 40) - .background(.ultraThinMaterial) - } - } else { - EmptyView() + } + + @State private var popupTagsPresented = false + + private var keywordsSection: some View { + Section("add-tag-groups.edit.tags") { + ForEach(tags, id: \.self) { tag in + HStack { + Text(tag) + Spacer() + Button { + deleteTag(tag) + } label: { + Image(systemName: "trash") + .tint(.red) + } } + } + .onDelete { indexes in + if let index = indexes.first { + let tag = tags[index] + deleteTag(tag) + } + } + HStack { + TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onSubmit { + addNewTag() + } + .focused($focusedField, equals: Focus.new) + Spacer() + if !newTag.isEmpty { + Button { + addNewTag() + } label: { + Image(systemName: "checkmark.circle.fill") + .tint(.green) + } + } + } } + .listRowBackground(theme.primaryBackgroundColor) + } + + private func addNewTag() { + addTag(newTag.trimmingCharacters(in: .whitespaces)) + newTag = "" + focusedField = Focus.new + } + + private func addTag(_ tag: String) { + guard !tag.isEmpty else { return } + tags.append(tag) + } + + private func deleteTag(_ tag: String) { + tags.removeAll(where: { $0 == tag }) + } + + private func save() { + var toSave = tags + let main = toSave.removeFirst() + preferences.tagGroups.append(.init( + title: title.trimmingCharacters(in: .whitespaces), + sfSymbolName: sfSymbolName, + main: main, + additional: toSave + )) + + dismiss() + } + + @ViewBuilder + private var symbolsSuggestionView: some View { + if focusedField == .symbol && !sfSymbolName.isEmpty { + let filteredMatches = allSymbols + .filter { $0.contains(sfSymbolName) } + if !filteredMatches.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + ForEach(filteredMatches, id: \.self) { symbolName in + Button { + sfSymbolName = symbolName + } label: { + Image(systemName: symbolName) + } + } + } + .padding(.horizontal, .layoutPadding) + } + .frame(height: 40) + .background(.ultraThinMaterial) + } + } else { + EmptyView() + } + } } - - struct AddTagGroupView_Previews: PreviewProvider { - static var previews: some View { - AddTagGroupView() - .withEnvironments() - } + static var previews: some View { + AddTagGroupView() + .withEnvironments() + } } diff --git a/IceCubesApp/App/Tabs/Timeline/Symbols.swift b/IceCubesApp/App/Tabs/Timeline/Symbols.swift index c96e779c..6b331391 100644 --- a/IceCubesApp/App/Tabs/Timeline/Symbols.swift +++ b/IceCubesApp/App/Tabs/Timeline/Symbols.swift @@ -7,13 +7,12 @@ import Foundation - let allSymbols: [String] = { - if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"), - let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"), - let plist = NSDictionary(contentsOfFile: resourcePath) { - - return plist.allKeys as? [String] ?? [] - } - return [] + if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"), + let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"), + let plist = NSDictionary(contentsOfFile: resourcePath) + { + return plist.allKeys as? [String] ?? [] + } + return [] }() diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index f3dcad52..43733899 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -151,25 +151,25 @@ struct TimelineTab: View { Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right") } } - - Menu("timeline.filter.tag-groups") { - ForEach(preferences.tagGroups, id: \.self) { group in - Button { - timeline = .tagGroup(group) - } label: { - VStack { - let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName - Label(group.title, systemImage: icon) - } - } - } - - Button { - routerPath.presentedSheet = .addTagGroup - } label: { - Label("timeline.filter.add-tag-groups", systemImage: "plus") + + Menu("timeline.filter.tag-groups") { + ForEach(preferences.tagGroups, id: \.self) { group in + Button { + timeline = .tagGroup(group) + } label: { + VStack { + let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName + Label(group.title, systemImage: icon) } + } } + + Button { + routerPath.presentedSheet = .addTagGroup + } label: { + Label("timeline.filter.add-tag-groups", systemImage: "plus") + } + } } private var addAccountButton: some View { diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 60ae3a8a..70654506 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -316,7 +316,7 @@ public struct AccountDetailView: View { Image(systemName: "arrowshape.turn.up.left") } } - + Menu { AccountDetailContextMenu(viewModel: viewModel) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index dd44463a..f9e4e440 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -99,7 +99,7 @@ public struct AppAccountsSelectorView: View { return theme.secondaryBackgroundColor } } - + private var accountsView: some View { NavigationStack { List { diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift index 909fbe48..1712db8e 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift @@ -201,26 +201,25 @@ struct ConversationsListRow: View { // Add in each detected link in the content ForEach(lastStatus.content.links) { link in switch link.type { - case .url: - if UIApplication.shared.canOpenURL(link.url) { - Button("accessibility.tabs.timeline.content-link-\(link.title)") { - HapticManager.shared.fireHaptic(of: .notification(.success)) - _ = routerPath.handle(url: link.url) - } - } - case .hashtag: - Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { - HapticManager.shared.fireHaptic(of: .notification(.success)) - _ = routerPath.handle(url: link.url) - } - case .mention: - Button("\(link.title)") { + case .url: + if UIApplication.shared.canOpenURL(link.url) { + Button("accessibility.tabs.timeline.content-link-\(link.title)") { HapticManager.shared.fireHaptic(of: .notification(.success)) _ = routerPath.handle(url: link.url) } + } + case .hashtag: + Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { + HapticManager.shared.fireHaptic(of: .notification(.success)) + _ = routerPath.handle(url: link.url) + } + case .mention: + Button("\(link.title)") { + HapticManager.shared.fireHaptic(of: .notification(.success)) + _ = routerPath.handle(url: link.url) + } } } } - } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift index 4ee1442e..17d5cd20 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift @@ -15,7 +15,7 @@ public extension View { .accessibilityInputLabels([ LocalizedStringKey("accessibility.tabs.timeline.new-post.label"), LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel1"), - LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2") + LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2"), ]) } } @@ -42,7 +42,7 @@ public struct StatusEditorToolbarItem: ToolbarContent { .accessibilityInputLabels([ LocalizedStringKey("accessibility.tabs.timeline.new-post.label"), LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel1"), - LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2") + LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2"), ]) } } diff --git a/Packages/Env/Sources/Env/PreferredShareButtonBehavior.swift b/Packages/Env/Sources/Env/PreferredShareButtonBehavior.swift index 1ea50340..244c29ea 100644 --- a/Packages/Env/Sources/Env/PreferredShareButtonBehavior.swift +++ b/Packages/Env/Sources/Env/PreferredShareButtonBehavior.swift @@ -4,7 +4,7 @@ import SwiftUI public enum PreferredShareButtonBehavior: Int, CaseIterable, Codable { case linkOnly case linkAndText - + public var title: LocalizedStringKey { switch self { case .linkOnly: return "settings.content.sharing.share-behavior.link-only" diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 3c9f51eb..00abb6b9 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -33,7 +33,7 @@ public enum SheetDestination: Identifiable { case listAddAccount(account: Account) case addAccount case addRemoteLocalTimeline -case addTagGroup + case addTagGroup case statusEditHistory(status: String) case settings case accountPushNotficationsSettings @@ -52,7 +52,7 @@ case addTagGroup case .addAccount: return "addAccount" case .addTagGroup: - return "addTagGroup" + return "addTagGroup" case .addRemoteLocalTimeline: return "addRemoteLocalTimeline" case .statusEditHistory: diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index f87d305e..77247e27 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -12,7 +12,7 @@ public class StreamWatcher: ObservableObject { private let decoder = JSONDecoder() private let encoder = JSONEncoder() - + private var retryDelay: Int = 10 public enum Stream: String { @@ -24,7 +24,6 @@ public class StreamWatcher: ObservableObject { @Published public var events: [any StreamEvent] = [] @Published public var unreadNotificationsCount: Int = 0 @Published public var latestEvent: (any StreamEvent)? - public init() { decoder.keyDecodingStrategy = .convertFromSnakeCase @@ -66,7 +65,8 @@ public class StreamWatcher: ObservableObject { private func sendMessage(message: StreamMessage) { if let encodedMessage = try? encoder.encode(message), - let stringMessage = String(data: encodedMessage, encoding: .utf8) { + let stringMessage = String(data: encodedMessage, encoding: .utf8) + { task?.send(.string(stringMessage), completionHandler: { _ in }) } } diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index b7eca2c8..79c12e08 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -12,7 +12,7 @@ public class UserPreferences: ObservableObject { private var client: Client? @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] - @AppStorage("tag_groups") public var tagGroups: [TagGroup] = [] + @AppStorage("tag_groups") public var tagGroups: [TagGroup] = [] @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("draft_posts") public var draftsPosts: [String] = [] @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true @@ -55,7 +55,7 @@ public class UserPreferences: ObservableObject { @AppStorage("requested_review") public var requestedReview = false @AppStorage("collapse-long-posts") public var collapseLongPosts = true - + @AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkAndText public enum SwipeActionsIconStyle: String, CaseIterable { diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index 4ec73bc9..f6a9154a 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -90,7 +90,7 @@ public struct ExploreView: View { } } } - + private var quickAccessView: some View { ScrollView(.horizontal) { HStack { diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index 3eb066aa..4df4a9b9 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -7,7 +7,7 @@ import SwiftUI class ExploreViewModel: ObservableObject { enum SearchScope: String, CaseIterable { case all, people, hashtags, posts - + var localizedString: LocalizedStringKey { switch self { case .all: @@ -21,7 +21,7 @@ class ExploreViewModel: ObservableObject { } } } - + var client: Client? { didSet { if oldValue != client { diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index 972227aa..be59d634 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -11,7 +11,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { public var asMarkdown: String = "" public var asRawText: String = "" public var statusesURLs = [URL]() - private(set) public var links = [Link]() + public private(set) var links = [Link]() public var asSafeMarkdownAttributedString: AttributedString = .init() private var main_regex: NSRegularExpression? @@ -156,7 +156,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { asMarkdown += ")" if let url = URL(string: href) { - let displayString = asMarkdown[start.. [URLQueryItem]? { return nil } - + public var jsonValue: Encodable? { switch self { case let .media(_, json): @@ -25,7 +25,6 @@ public enum Media: Endpoint { return nil } } - } public struct MediaDescriptionData: Encodable, Sendable { @@ -35,4 +34,3 @@ public struct MediaDescriptionData: Encodable, Sendable { self.description = description } } - diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index d2023a78..1e2b8388 100644 --- a/Packages/Network/Sources/Network/Endpoint/Timelines.swift +++ b/Packages/Network/Sources/Network/Endpoint/Timelines.swift @@ -4,7 +4,7 @@ public enum Timelines: Endpoint { case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool) case home(sinceId: String?, maxId: String?, minId: String?) case list(listId: String, sinceId: String?, maxId: String?, minId: String?) - case hashtag(tag: String, additional: [String]?, maxId: String?) + case hashtag(tag: String, additional: [String]?, maxId: String?) public func path() -> String { switch self { @@ -18,7 +18,7 @@ public enum Timelines: Endpoint { return "timelines/tag/\(tag)" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .pub(sinceId, maxId, minId, local): @@ -30,10 +30,10 @@ public enum Timelines: Endpoint { case let .list(_, sinceId, maxId, mindId): return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) case let .hashtag(_, additional, maxId): - var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? [] - params.append(contentsOf: (additional ?? []) - .map { URLQueryItem(name: "any[]", value: $0) }) - return params + var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? [] + params.append(contentsOf: (additional ?? []) + .map { URLQueryItem(name: "any[]", value: $0) }) + return params } } } diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift index cd9d0ab8..e566cb66 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift @@ -11,9 +11,9 @@ public struct StatusDetailView: View { @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var client: Client @EnvironmentObject private var routerPath: RouterPath - + @StateObject private var viewModel: StatusDetailViewModel - + @State private var isLoaded: Bool = false @State private var statusHeight: CGFloat = 0 diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorCameraPickerView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorCameraPickerView.swift index 6f89b0ce..c6b19e36 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorCameraPickerView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorCameraPickerView.swift @@ -1,35 +1,33 @@ -import UIKit import SwiftUI +import UIKit struct StatusEditorCameraPickerView: UIViewControllerRepresentable { @Binding var selectedImage: UIImage? @Environment(\.presentationMode) var isPresented - + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let picker: StatusEditorCameraPickerView - + init(picker: StatusEditorCameraPickerView) { self.picker = picker } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { guard let selectedImage = info[.originalImage] as? UIImage else { return } - self.picker.selectedImage = selectedImage - self.picker.isPresented.wrappedValue.dismiss() + picker.selectedImage = selectedImage + picker.isPresented.wrappedValue.dismiss() } } - + func makeUIViewController(context: Context) -> UIImagePickerController { let imagePicker = UIImagePickerController() imagePicker.sourceType = .camera imagePicker.delegate = context.coordinator return imagePicker } - - func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { - - } - + + func updateUIViewController(_: UIImagePickerController, context _: Context) {} + func makeCoordinator() -> Coordinator { Coordinator(picker: self) } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift index d8f76ef1..363b8c2d 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift @@ -15,7 +15,7 @@ struct StatusEditorMediaEditView: View { @FocusState private var isFieldFocused: Bool @State private var isUpdating: Bool = false - + @State private var didAppear: Bool = false var body: some View { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift index ff469c05..f10b3574 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift @@ -71,7 +71,8 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { if self == .jpeg || self == .png || self == .tiff || self == .image || self == .uiimage || self == .adobeRawImage { if let image = result as? UIImage, let compressedData = try? await compressor.compressImageForUpload(image), - let compressedImage = UIImage(data: compressedData) { + let compressedImage = UIImage(data: compressedData) + { return compressedImage } else if let imageURL = result as? URL, let compressedData = await compressor.compressImageFrom(url: imageURL), diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 6f3a3310..5814a4c6 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -368,7 +368,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject { .compactMap { NSItemProvider(contentsOf: $0) } processItemsProvider(items: items) } - + func processCameraPhoto(image: UIImage) { mediasImages.append(.init(image: image, movieTransferable: nil, diff --git a/Packages/Status/Sources/Status/List/ReblogCache.swift b/Packages/Status/Sources/Status/List/ReblogCache.swift index 58056f9d..5ef3b527 100644 --- a/Packages/Status/Sources/Status/List/ReblogCache.swift +++ b/Packages/Status/Sources/Status/List/ReblogCache.swift @@ -75,7 +75,7 @@ public class ReblogCache { if let reblog = status.reblog { if let cached = statusCache.value(forKey: reblog.id) { // this is already cached - if cached.postId != status.id && cached.seen { + if cached.postId != status.id, cached.seen { // This was posted by someone other than the person we have in the cache // and we have seen the items at some point, so we might want to suppress it diff --git a/Packages/Status/Sources/Status/Poll/StatusPollView.swift b/Packages/Status/Sources/Status/Poll/StatusPollView.swift index 09092f5e..0bb13973 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollView.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollView.swift @@ -117,7 +117,6 @@ public struct StatusPollView: View { } .accessibilityElement(children: .contain) .accessibilityLabel(viewModel.poll.expired ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label") - } func combinedAccessibilityLabel(for option: Poll.Option, index: Int) -> Text { @@ -126,7 +125,6 @@ public struct StatusPollView: View { Text(", ") + Text(option.title) + Text(showPercentage ? ", \(percentForOption(option: option))%" : "") - } private var footerView: some View { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 1bcd3f31..f7bcd508 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -124,7 +124,7 @@ public struct StatusRowView: View { trailing: .layoutPadding)) .accessibilityElement(children: viewModel.isFocused ? .contain : .combine) .accessibilityLabel(viewModel.isFocused == false && accessibilityEnabled - ? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) + ? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) .accessibilityHidden(viewModel.filter?.filter.filterAction == .hide) .accessibilityAction { viewModel.navigateToDetail() @@ -214,23 +214,23 @@ public struct StatusRowView: View { // Add in each detected link in the content ForEach(viewModel.finalStatus.content.links) { link in switch link.type { - case .url: - if UIApplication.shared.canOpenURL(link.url) { - Button("accessibility.tabs.timeline.content-link-\(link.title)") { - HapticManager.shared.fireHaptic(of: .notification(.success)) - _ = viewModel.routerPath.handle(url: link.url) - } - } - case .hashtag: - Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { - HapticManager.shared.fireHaptic(of: .notification(.success)) - _ = viewModel.routerPath.handle(url: link.url) - } - case .mention: - Button("\(link.title)") { + case .url: + if UIApplication.shared.canOpenURL(link.url) { + Button("accessibility.tabs.timeline.content-link-\(link.title)") { HapticManager.shared.fireHaptic(of: .notification(.success)) _ = viewModel.routerPath.handle(url: link.url) } + } + case .hashtag: + Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { + HapticManager.shared.fireHaptic(of: .notification(.success)) + _ = viewModel.routerPath.handle(url: link.url) + } + case .mention: + Button("\(link.title)") { + HapticManager.shared.fireHaptic(of: .notification(.success)) + _ = viewModel.routerPath.handle(url: link.url) + } } } } @@ -298,28 +298,27 @@ private struct CombinedAccessibilityLabel { func finalLabel() -> Text { if let filter { switch filter.filterAction { - case .warn: - return Text("status.filter.filtered-by-\(filter.title)") - case .hide: - return Text("") + case .warn: + return Text("status.filter.filtered-by-\(filter.title)") + case .hide: + return Text("") } } else { return userNamePreamble() + - Text(hasSpoiler - ? viewModel.finalStatus.spoilerText.asRawText - : viewModel.finalStatus.content.asRawText - ) + - Text(hasSpoiler - ? "status.editor.spoiler" - : "" - ) + Text(", ") + - pollText() + - imageAltText() + - Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") + - Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") + - Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") + - Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)") - + Text(hasSpoiler + ? viewModel.finalStatus.spoilerText.asRawText + : viewModel.finalStatus.content.asRawText + ) + + Text(hasSpoiler + ? "status.editor.spoiler" + : "" + ) + Text(", ") + + pollText() + + imageAltText() + + Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") + + Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") + + Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") + + Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)") } } @@ -377,12 +376,12 @@ private struct CombinedAccessibilityLabel { : 0 text = text + - Text(selected ? "accessibility.status.poll.selected.label" : "") + - Text(", ") + - Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + - Text(", ") + - Text(option.title) + - Text(showPercentage ? ", \(percentage)%. " : ". ") + Text(selected ? "accessibility.status.poll.selected.label" : "") + + Text(", ") + + Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + + Text(", ") + + Text(option.title) + + Text(showPercentage ? ", \(percentage)%. " : ". ") } } return Text("") diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 72c24bff..fc1f52da 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -33,13 +33,14 @@ public class StatusRowViewModel: ObservableObject { @Published var isLoadingRemoteContent: Bool = false @Published var localStatusId: String? @Published var localStatus: Status? - + // The relationship our user has to the author of this post, if available @Published var authorRelationship: Relationship? { didSet { // if we are newly blocking or muting the author, force collapse post so it goes away if let relationship = authorRelationship, - relationship.blocking || relationship.muting { + relationship.blocking || relationship.muting + { lineLimit = 0 } } diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift index 5effef04..1c64962a 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift @@ -124,8 +124,7 @@ struct StatusRowActionsView: View { { switch userPreferences.shareButtonBehavior { case .linkOnly: - ShareLink(item: url) - { + ShareLink(item: url) { action.image(dataController: statusDataController) } .buttonStyle(.statusAction()) diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift index 04f6f7f8..d5785a2c 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift @@ -187,8 +187,7 @@ struct StatusRowContextMenu: View { } label: { Label("status.action.message", systemImage: "tray.full") } - - + if viewModel.authorRelationship?.blocking == true { Button { Task { @@ -216,7 +215,7 @@ struct StatusRowContextMenu: View { Label("account.action.block", systemImage: "person.crop.circle.badge.xmark") } } - + if viewModel.authorRelationship?.muting == true { Button { Task { @@ -248,7 +247,6 @@ struct StatusRowContextMenu: View { Label("account.action.mute", systemImage: "speaker.slash") } } - } } Section { diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index d06f500d..06fe5a47 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -31,8 +31,8 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable { public enum TimelineFilter: Hashable, Equatable { case home, local, federated, trending - case hashtag(tag: String, accountId: String?) - case tagGroup(TagGroup) + case hashtag(tag: String, accountId: String?) + case tagGroup(TagGroup) case list(list: Models.List) case remoteLocal(server: String, filter: RemoteTimelineFilter) case latest @@ -74,7 +74,7 @@ public enum TimelineFilter: Hashable, Equatable { case let .hashtag(tag, _): return "#\(tag)" case let .tagGroup(group): - return group.title + return group.title case let .list(list): return list.title case let .remoteLocal(server, _): @@ -97,7 +97,7 @@ public enum TimelineFilter: Hashable, Equatable { case let .hashtag(tag, _): return "#\(tag)" case let .tagGroup(group): - return LocalizedStringKey(group.title) // ?? not sure since this can't be localized. + return LocalizedStringKey(group.title) // ?? not sure since this can't be localized. case let .list(list): return LocalizedStringKey(list.title) case let .remoteLocal(server, _): @@ -147,10 +147,10 @@ public enum TimelineFilter: Hashable, Equatable { if let accountId { return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil) } else { - return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) + return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId) } case let .tagGroup(group): - return Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId) + return Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId) } } } @@ -162,7 +162,7 @@ extension TimelineFilter: Codable { case federated case trending case hashtag - case tagGroup + case tagGroup case list case remoteLocal case latest @@ -189,8 +189,8 @@ extension TimelineFilter: Codable { accountId: accountId ) case .tagGroup: - let group = try container.decode(TagGroup.self, forKey: .tagGroup) - self = .tagGroup(group) + let group = try container.decode(TagGroup.self, forKey: .tagGroup) + self = .tagGroup(group) case .list: let list = try container.decode( Models.List.self, @@ -233,7 +233,7 @@ extension TimelineFilter: Codable { try nestedContainer.encode(tag) try nestedContainer.encode(accountId) case let .tagGroup(group): - try container.encode(group, forKey: .tagGroup) + try container.encode(group, forKey: .tagGroup) case let .list(list): try container.encode(list, forKey: .list) case let .remoteLocal(server, filter): diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 465e9828..bf2576a3 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -1,11 +1,11 @@ import DesignSystem import Env -import SwiftUIIntrospect import Models import Network import Shimmer import Status import SwiftUI +import SwiftUIIntrospect public struct TimelineView: View { private enum Constants { @@ -39,8 +39,8 @@ public struct TimelineView: View { ScrollViewReader { proxy in ZStack(alignment: .top) { List { - if viewModel.tagGroup != nil { - tagGroupHeaderView + if viewModel.tagGroup != nil { + tagGroupHeaderView } else if viewModel.tag == nil { scrollToTopView } else { @@ -106,19 +106,18 @@ public struct TimelineView: View { } .accessibilityRepresentation { switch timeline { - case let .remoteLocal(_, filter): - if canFilterTimeline { - Menu(filter.localizedTitle()) {} - } else { - Text(filter.localizedTitle()) - } - default: - if canFilterTimeline { - Menu(timeline.localizedTitle()) {} - } else { - Text(timeline.localizedTitle()) - } - + case let .remoteLocal(_, filter): + if canFilterTimeline { + Menu(filter.localizedTitle()) {} + } else { + Text(filter.localizedTitle()) + } + default: + if canFilterTimeline { + Menu(timeline.localizedTitle()) {} + } else { + Text(timeline.localizedTitle()) + } } } .accessibilityAddTraits(.isHeader) @@ -182,64 +181,64 @@ public struct TimelineView: View { @ViewBuilder private var tagHeaderView: some View { if let tag = viewModel.tag { - headerView { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("#\(tag.name)") - .font(.scaledHeadline) - Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)") - .font(.scaledFootnote) - .foregroundColor(.gray) - } - .accessibilityElement(children: .combine) - Spacer() - Button { - Task { - if tag.following { - viewModel.tag = await account.unfollowTag(id: tag.name) - } else { - viewModel.tag = await account.followTag(id: tag.name) - } - } - } label: { - Text(tag.following ? "account.follow.following" : "account.follow.follow") - }.buttonStyle(.bordered) - } - } - } - } - - @ViewBuilder - private var tagGroupHeaderView: some View { - if let group = viewModel.tagGroup { - headerView { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(group.description) - .font(.scaledHeadline) - } - .accessibilityElement(children: .combine) - } + headerView { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("#\(tag.name)") + .font(.scaledHeadline) + Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)") + .font(.scaledFootnote) + .foregroundColor(.gray) } + .accessibilityElement(children: .combine) + Spacer() + Button { + Task { + if tag.following { + viewModel.tag = await account.unfollowTag(id: tag.name) + } else { + viewModel.tag = await account.followTag(id: tag.name) + } + } + } label: { + Text(tag.following ? "account.follow.following" : "account.follow.follow") + }.buttonStyle(.bordered) + } } } - - @ViewBuilder - private func headerView( - @ViewBuilder content: () -> some View - ) -> some View { - VStack(alignment: .leading) { - Spacer() - content() - Spacer() + } + + @ViewBuilder + private var tagGroupHeaderView: some View { + if let group = viewModel.tagGroup { + headerView { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(group.description) + .font(.scaledHeadline) + } + .accessibilityElement(children: .combine) } - .listRowBackground(theme.secondaryBackgroundColor) - .listRowSeparator(.hidden) - .listRowInsets(.init(top: 8, - leading: .layoutPadding, - bottom: 8, - trailing: .layoutPadding)) + } } + } + + @ViewBuilder + private func headerView( + @ViewBuilder content: () -> some View + ) -> some View { + VStack(alignment: .leading) { + Spacer() + content() + Spacer() + } + .listRowBackground(theme.secondaryBackgroundColor) + .listRowSeparator(.hidden) + .listRowInsets(.init(top: 8, + leading: .layoutPadding, + bottom: 8, + trailing: .layoutPadding)) + } private var scrollToTopView: some View { HStack { EmptyView() } diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 6acaaf81..da35bfd6 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -40,13 +40,13 @@ class TimelineViewModel: ObservableObject { private var timelineTask: Task? @Published var tag: Tag? - - var tagGroup: TagGroup? { - if case let .tagGroup(group) = timeline { - return group - } - return nil + + var tagGroup: TagGroup? { + if case let .tagGroup(group) = timeline { + return group } + return nil + } // Internal source of truth for a timeline. private var datasource = TimelineDatasource() @@ -170,7 +170,7 @@ extension TimelineViewModel: StatusesFetcher { } await fetchNewestStatuses() } - + func refreshTimeline() { timelineTask?.cancel() timelineTask = Task { @@ -321,8 +321,9 @@ extension TimelineViewModel: StatusesFetcher { // If none, it'll stop there. // Only do that in the context of the home timeline as other don't worth catching up that much. if timeline == .home, - !Task.isCancelled, - let latest = await datasource.get().first { + !Task.isCancelled, + let latest = await datasource.get().first + { try await fetchNewPagesFrom(latestStatus: latest, client: client) } }