From 5951bcec38b0d3cc2bb4cbd30791e1372ed5b1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mart=C3=ADnez?= Date: Wed, 19 Jul 2023 07:44:35 +0200 Subject: [PATCH] Tag groups (#1506) * Implemented tag groups * Cleanup --------- Co-authored-by: Thomas Ricouard --- IceCubesApp.xcodeproj/project.pbxproj | 8 + IceCubesApp/App/AppRouter.swift | 3 + .../App/Tabs/Settings/SettingsTab.swift | 33 +++ .../App/Tabs/Timeline/AddTagGroupView.swift | 197 ++++++++++++++++++ IceCubesApp/App/Tabs/Timeline/Symbols.swift | 19 ++ .../App/Tabs/Timeline/TimelineTab.swift | 19 ++ .../Localization/be.lproj/Localizable.strings | 8 + .../Localization/ca.lproj/Localizable.strings | 14 ++ .../Localization/de.lproj/Localizable.strings | 8 + .../en-GB.lproj/Localizable.strings | 8 + .../Localization/en.lproj/Localizable.strings | 8 + .../Localization/es.lproj/Localizable.strings | 8 + .../Localization/eu.lproj/Localizable.strings | 8 + .../Localization/fr.lproj/Localizable.strings | 8 + .../Localization/it.lproj/Localizable.strings | 8 + .../Localization/ja.lproj/Localizable.strings | 8 + .../Localization/ko.lproj/Localizable.strings | 8 + .../Localization/nb.lproj/Localizable.strings | 8 + .../Localization/nl.lproj/Localizable.strings | 8 + .../Localization/pl.lproj/Localizable.strings | 8 + .../pt-BR.lproj/Localizable.strings | 8 + .../Localization/tr.lproj/Localizable.strings | 8 + .../Localization/uk.lproj/Localizable.strings | 8 + .../zh-Hans.lproj/Localizable.strings | 8 + .../zh-Hant.lproj/Localizable.strings | 8 + Packages/Env/Sources/Env/Router.swift | 3 + .../Env/Sources/Env/UserPreferences.swift | 1 + Packages/Models/Sources/Models/Tag.swift | 26 +++ .../Sources/Network/Endpoint/Timelines.swift | 13 +- .../Sources/Timeline/TimelineFilter.swift | 17 +- .../Sources/Timeline/TimelineView.swift | 86 +++++--- .../Sources/Timeline/TimelineViewModel.swift | 7 + 32 files changed, 553 insertions(+), 37 deletions(-) create mode 100644 IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift create mode 100644 IceCubesApp/App/Tabs/Timeline/Symbols.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index fa327715..ed5793d6 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ E9DF420129830FEC0003AAD2 /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DF420029830FEC0003AAD2 /* ActionRequestHandler.swift */; }; E9DF420329830FEC0003AAD2 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = E9DF420229830FEC0003AAD2 /* Action.js */; }; E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + FA31A9AB2A66BF7C00D5F662 /* AddTagGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */; }; + FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD203CF2A66D8A80030A7FD /* Symbols.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -272,6 +274,8 @@ E9DF420229830FEC0003AAD2 /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = ""; }; E9DF420429830FEC0003AAD2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F355EEDA297A8BD500E362C0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTagGroupView.swift; sourceTree = ""; }; + FAD203CF2A66D8A80030A7FD /* Symbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Symbols.swift; sourceTree = ""; }; FF8259FB298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; FF8259FC298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; /* End PBXFileReference section */ @@ -401,6 +405,8 @@ children = ( 9F398AB229360A4C00A889F2 /* TimelineTab.swift */, 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */, + FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */, + FAD203CF2A66D8A80030A7FD /* Symbols.swift */, ); path = Timeline; sourceTree = ""; @@ -829,6 +835,8 @@ 9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */, 9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */, 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */, + FA31A9AB2A66BF7C00D5F662 /* AddTagGroupView.swift in Sources */, + FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRouter.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, diff --git a/IceCubesApp/App/AppRouter.swift b/IceCubesApp/App/AppRouter.swift index dcb99135..0d93b766 100644 --- a/IceCubesApp/App/AppRouter.swift +++ b/IceCubesApp/App/AppRouter.swift @@ -82,6 +82,9 @@ extension View { case .addRemoteLocalTimeline: AddRemoteTimelineView() .withEnvironments() + case .addTagGroup: + AddTagGroupView() + .withEnvironments() case let .statusEditHistory(status): StatusEditHistoryView(statusId: status) .withEnvironments() diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 21786182..497a2b03 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -139,6 +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: ContentSettingsView()) { Label("settings.general.content", systemImage: "rectangle.stack") } @@ -261,6 +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) + } + .navigationTitle("timeline.filter.tag-groups") + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + .toolbar { + EditButton() + } + } private var remoteLocalTimelinesView: some View { Form { diff --git a/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift b/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift new file mode 100644 index 00000000..bce5ef21 --- /dev/null +++ b/IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift @@ -0,0 +1,197 @@ +import Combine +import DesignSystem +import Env +import Models +import Network +import NukeUI +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 + } + } + } + + @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) + } + } + } + + @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() + } +} diff --git a/IceCubesApp/App/Tabs/Timeline/Symbols.swift b/IceCubesApp/App/Tabs/Timeline/Symbols.swift new file mode 100644 index 00000000..c96e779c --- /dev/null +++ b/IceCubesApp/App/Tabs/Timeline/Symbols.swift @@ -0,0 +1,19 @@ +// +// Symbols.swift +// IceCubesApp +// +// Created by Alejandro Martinez on 18/7/23. +// + +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 [] +}() diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 53382608..f3dcad52 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -151,6 +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") + } + } } private var addAccountButton: some View { diff --git a/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings index 212fb0b6..33d62d43 100644 --- a/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings @@ -245,6 +245,8 @@ "timeline.filter.lists" = "Спісы"; "timeline.filter.local" = "Мясцовыя шкалы часу"; "timeline.filter.tags" = "Адсочваемыя тэгі"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Дадаць уліковы запіс"; @@ -603,3 +605,9 @@ "tag.suggested.mentions-%lld" = "%lld mentions"; +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; + diff --git a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings index 541b04b6..13e628d9 100644 --- a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings @@ -239,6 +239,8 @@ "timeline.filter.lists" = "Llistes"; "timeline.filter.local" = "Línies de temps locals"; "timeline.filter.tags" = "Etiquetes seguides"; +"timeline.filter.tag-groups" = "Grups d'Etiquetes"; +"timeline.filter.add-tag-groups" = "Afegeix grup"; // MARK: Package: AppAccount "app-account.button.add" = "Afegeix un compte"; @@ -596,3 +598,15 @@ "status.action.report" = "Report Post"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Títol del Grup d'Etiquetes"; +"add-tag-groups.edit.icon.field" = "Icona del Grup d'Etiquetes (SFSymbol)"; +"add-tag-groups.edit.tags" = "Afegeix etiquetes al grup"; +"add-tag-groups.edit.tags.add" = "Etiqueta"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index 4bd9829c..f98660fa 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -242,6 +242,8 @@ "timeline.filter.lists" = "Listen"; "timeline.filter.local" = "Lokale Timelines"; "timeline.filter.tags" = "Gefolgte Hashtags"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Konto hinzufügen"; @@ -583,3 +585,9 @@ "report.title" = "Beitrag melden"; "report.action.send" = "Absenden"; "status.action.report" = "Beitrag melden"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings index 089f37b1..dfd22aa8 100644 --- a/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings @@ -243,6 +243,8 @@ "timeline.filter.lists" = "Lists"; "timeline.filter.local" = "Local Timelines"; "timeline.filter.tags" = "Followed Tags"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Add Account"; @@ -597,3 +599,9 @@ "status.action.report" = "Report Post"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index 3c3d4598..935b5d97 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -241,6 +241,8 @@ "timeline.filter.lists" = "Lists"; "timeline.filter.local" = "Local Timelines"; "timeline.filter.tags" = "Followed Tags"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Add Account"; @@ -599,3 +601,9 @@ "status.action.report" = "Report Post"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index ef0eae8f..77272d4e 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -241,6 +241,8 @@ "timeline.filter.lists" = "Listas"; "timeline.filter.local" = "Cronologías locales"; "timeline.filter.tags" = "Etiquetas que sigues"; +"timeline.filter.tag-groups" = "Grupos de Etiquetas"; +"timeline.filter.add-tag-groups" = "Añadir grupo"; // MARK: Package: AppAccount "app-account.button.add" = "Añadir cuenta"; @@ -598,3 +600,9 @@ "status.action.report" = "Denunciar publicación"; "tag.suggested.mentions-%lld" = "%lld menciones"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Título del Grupo de Etiquetas"; +"add-tag-groups.edit.icon.field" = "Icono del Grupo de Etiquetas (SFSymbol)"; +"add-tag-groups.edit.tags" = "Añade etiquetas al grupo"; +"add-tag-groups.edit.tags.add" = "Etiqueta"; diff --git a/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings index f995abbc..d42c5cab 100644 --- a/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings @@ -242,6 +242,8 @@ "timeline.filter.lists" = "Zerrendak"; "timeline.filter.local" = "Denbora-lerro lokalak"; "timeline.filter.tags" = "Jarraitutako traolak"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Gehitu kontua"; @@ -584,3 +586,9 @@ "report.title" = "Salaketa"; "report.action.send" = "Bidali"; "status.action.report" = "Salatu edukia"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings index 64b8d3d6..f54a2a3d 100644 --- a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "Listes"; "timeline.filter.local" = "Chronologies locales"; "timeline.filter.tags" = "Tags suivis"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Ajouter un compte"; @@ -593,3 +595,9 @@ "status.action.report" = "Signaler la publication"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings index 7454a659..0f7bc116 100644 --- a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings @@ -239,6 +239,8 @@ "timeline.filter.lists" = "Liste"; "timeline.filter.local" = "Timeline locali"; "timeline.filter.tags" = "Tag seguiti"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Aggiungi account"; @@ -597,3 +599,9 @@ "status.action.report" = "Segnala il messaggio"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings index 283534e6..551206ad 100644 --- a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "リスト"; "timeline.filter.local" = "ローカルタイムライン"; "timeline.filter.tags" = "フォローしたタグ"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "アカウントの追加"; @@ -596,3 +598,9 @@ "status.action.report" = "投稿を報告"; "tag.suggested.mentions-%lld" = "返信:%lld"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings index 4d5fd941..76c176c7 100644 --- a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings @@ -241,6 +241,8 @@ "timeline.filter.local" = "원격 로컬 타임라인"; "timeline.filter.tags" = "팔로우한 태그"; "timeline-new-posts %lld" = "%lld개 새 글"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "계정 추가"; @@ -600,3 +602,9 @@ "status.action.report" = "글 신고"; "tag.suggested.mentions-%lld" = "%lld개 글"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings index f5e76854..b85a7a5b 100644 --- a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "Lister"; "timeline.filter.local" = "Lokale tidslinjer"; "timeline.filter.tags" = "Fulgte tagger"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Legg til konto"; @@ -597,3 +599,9 @@ "status.action.report" = "Rapporter innlegg"; "tag.suggested.mentions-%lld" = "%lld omtaler"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index 9db1fc45..1155d3cb 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -237,6 +237,8 @@ "timeline.filter.lists" = "Lijsten"; "timeline.filter.local" = "Lokale tijdlijnen"; "timeline.filter.tags" = "Gevolgde hashtags"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Account toevoegen"; @@ -594,3 +596,9 @@ "status.action.report" = "Meld post"; "tag.suggested.mentions-%lld" = "%lld vermeldingen"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings index 23b511a9..18bf5b0e 100644 --- a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "Listy"; "timeline.filter.local" = "Strumienie lokalne"; "timeline.filter.tags" = "Obserwowane hasztagi"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Dodaj konto"; @@ -587,3 +589,9 @@ "report.action.send" = "Wyślij"; "status.action.report" = "Zgłoś post"; +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; + diff --git a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings index bb432593..a18d7fc6 100644 --- a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "Listas"; "timeline.filter.local" = "Timelines Locais"; "timeline.filter.tags" = "Hashtags Seguidas"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Adicionar Conta"; @@ -597,3 +599,9 @@ "status.action.report" = "Denunciar"; "tag.suggested.mentions-%lld" = "%lld menções"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings index 7620b142..745c8b73 100644 --- a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings @@ -240,6 +240,8 @@ "timeline.filter.lists" = "Listeler"; "timeline.filter.local" = "Yerel Zaman Dilimleri"; "timeline.filter.tags" = "Takip Edilen Etiketler"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Hesap Ekle"; @@ -597,3 +599,9 @@ "status.action.report" = "Report Post"; "tag.suggested.mentions-%lld" = "%lld mentions"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings index 96757b29..864f3243 100644 --- a/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings @@ -241,6 +241,8 @@ "timeline.filter.lists" = "Списки"; "timeline.filter.local" = "Локальна стрічка"; "timeline.filter.tags" = "Хештеґи"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "Додати обліковий запис"; @@ -599,3 +601,9 @@ "tag.suggested.mentions-%lld" = "%lld згадок"; +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; + diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index 46ba0c13..441289e2 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -238,6 +238,8 @@ "timeline.filter.lists" = "列表"; "timeline.filter.local" = "远程时间线"; "timeline.filter.tags" = "关注的标签"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "添加账户"; @@ -597,3 +599,9 @@ "status.action.report" = "举报嘟文"; "tag.suggested.mentions-%lld" = "%lld 个提及"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings index 81fb7e29..72a19941 100644 --- a/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings @@ -242,6 +242,8 @@ "timeline.filter.lists" = "列表"; "timeline.filter.local" = "本站時間軸"; "timeline.filter.tags" = "跟隨標籤"; +"timeline.filter.tag-groups" = "Tag Groups"; +"timeline.filter.add-tag-groups" = "Add tag group"; // MARK: Package: AppAccount "app-account.button.add" = "新增帳號"; @@ -599,3 +601,9 @@ "status.action.report" = "檢舉嘟文"; "tag.suggested.mentions-%lld" = "%lld 提及"; + +// MARK: Tag Groups +"add-tag-groups.edit.title.field" = "Tag Group Title"; +"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)"; +"add-tag-groups.edit.tags" = "Add tags to the group"; +"add-tag-groups.edit.tags.add" = "Tag"; diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index d16a1bda..3c9f51eb 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -33,6 +33,7 @@ public enum SheetDestination: Identifiable { case listAddAccount(account: Account) case addAccount case addRemoteLocalTimeline +case addTagGroup case statusEditHistory(status: String) case settings case accountPushNotficationsSettings @@ -50,6 +51,8 @@ public enum SheetDestination: Identifiable { return "listAddAccount" case .addAccount: return "addAccount" + case .addTagGroup: + return "addTagGroup" case .addRemoteLocalTimeline: return "addRemoteLocalTimeline" case .statusEditHistory: diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 9786ad0a..b7eca2c8 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -12,6 +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("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("draft_posts") public var draftsPosts: [String] = [] @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index 54b2841c..08ce1b81 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -62,3 +62,29 @@ public struct FeaturedTag: Codable, Identifiable { extension Tag: Sendable {} extension Tag.History: Sendable {} extension FeaturedTag: Sendable {} + + +public struct TagGroup: Codable, Equatable, Hashable { + public init(title: String, sfSymbolName: String, main: String, additional: [String]) { + self.title = title + self.sfSymbolName = sfSymbolName + self.main = main + self.additional = additional + } + + public let title: String + public let sfSymbolName: String + public let main: String + public let additional: [String] + + public var tags: [String] { + [main] + additional + } + + public var description: String { + tags + .map { "#\($0)" } + .joined(separator: " ") + } +} + diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index 59c5e467..d2023a78 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, maxId: String?) + case hashtag(tag: String, additional: [String]?, maxId: String?) public func path() -> String { switch self { @@ -14,11 +14,11 @@ public enum Timelines: Endpoint { return "timelines/home" case let .list(listId, _, _, _): return "timelines/list/\(listId)" - case let .hashtag(tag, _): + case let .hashtag(tag, _, _): return "timelines/tag/\(tag)" } } - + public func queryItems() -> [URLQueryItem]? { switch self { case let .pub(sinceId, maxId, minId, local): @@ -29,8 +29,11 @@ public enum Timelines: Endpoint { return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) case let .list(_, sinceId, maxId, mindId): return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId) - case let .hashtag(_, maxId): - return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) + 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 } } } diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index 44dfab41..d06f500d 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -31,7 +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 hashtag(tag: String, accountId: String?) + case tagGroup(TagGroup) case list(list: Models.List) case remoteLocal(server: String, filter: RemoteTimelineFilter) case latest @@ -72,6 +73,8 @@ public enum TimelineFilter: Hashable, Equatable { return "Home" case let .hashtag(tag, _): return "#\(tag)" + case let .tagGroup(group): + return group.title case let .list(list): return list.title case let .remoteLocal(server, _): @@ -93,6 +96,8 @@ public enum TimelineFilter: Hashable, Equatable { return "timeline.home" case let .hashtag(tag, _): return "#\(tag)" + case let .tagGroup(group): + return LocalizedStringKey(group.title) // ?? not sure since this can't be localized. case let .list(list): return LocalizedStringKey(list.title) case let .remoteLocal(server, _): @@ -142,8 +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, 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) } } } @@ -155,6 +162,7 @@ extension TimelineFilter: Codable { case federated case trending case hashtag + case tagGroup case list case remoteLocal case latest @@ -180,6 +188,9 @@ extension TimelineFilter: Codable { tag: tag, accountId: accountId ) + case .tagGroup: + let group = try container.decode(TagGroup.self, forKey: .tagGroup) + self = .tagGroup(group) case .list: let list = try container.decode( Models.List.self, @@ -221,6 +232,8 @@ extension TimelineFilter: Codable { var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag) try nestedContainer.encode(tag) try nestedContainer.encode(accountId) + case let .tagGroup(group): + 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 68f86f8c..465e9828 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -39,7 +39,9 @@ public struct TimelineView: View { ScrollViewReader { proxy in ZStack(alignment: .top) { List { - if viewModel.tag == nil { + if viewModel.tagGroup != nil { + tagGroupHeaderView + } else if viewModel.tag == nil { scrollToTopView } else { tagHeaderView @@ -180,40 +182,64 @@ public struct TimelineView: View { @ViewBuilder private var tagHeaderView: some View { if let tag = viewModel.tag { - VStack(alignment: .leading) { - Spacer() - 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) + 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) } - } label: { - Text(tag.following ? "account.follow.following" : "account.follow.follow") - }.buttonStyle(.bordered) } - Spacer() - } - .listRowBackground(theme.secondaryBackgroundColor) - .listRowSeparator(.hidden) - .listRowInsets(.init(top: 8, - leading: .layoutPadding, - bottom: 8, - trailing: .layoutPadding)) } } + + @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) + } + } + } + } + + @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 5bb52b1d..6acaaf81 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -40,6 +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 + } // Internal source of truth for a timeline. private var datasource = TimelineDatasource()