Tag groups (#1506)

* Implemented tag groups

* Cleanup

---------

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Alejandro Martínez 2023-07-19 07:44:35 +02:00 committed by GitHub
parent c4c705aa10
commit 5951bcec38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 553 additions and 37 deletions

View file

@ -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 = "<group>"; };
E9DF420429830FEC0003AAD2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F355EEDA297A8BD500E362C0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTagGroupView.swift; sourceTree = "<group>"; };
FAD203CF2A66D8A80030A7FD /* Symbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Symbols.swift; sourceTree = "<group>"; };
FF8259FB298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
FF8259FC298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -401,6 +405,8 @@
children = (
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */,
FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */,
FAD203CF2A66D8A80030A7FD /* Symbols.swift */,
);
path = Timeline;
sourceTree = "<group>";
@ -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 */,

View file

@ -82,6 +82,9 @@ extension View {
case .addRemoteLocalTimeline:
AddRemoteTimelineView()
.withEnvironments()
case .addTagGroup:
AddTagGroupView()
.withEnvironments()
case let .statusEditHistory(status):
StatusEditHistoryView(statusId: status)
.withEnvironments()

View file

@ -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")
}
@ -262,6 +265,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 {
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in

View file

@ -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()
}
}

View file

@ -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 []
}()

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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:

View file

@ -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

View file

@ -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: " ")
}
}

View file

@ -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,7 +14,7 @@ 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)"
}
}
@ -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
}
}
}

View file

@ -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):

View file

@ -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,41 +182,65 @@ 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() }
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -41,6 +41,13 @@ class TimelineViewModel: ObservableObject {
@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()
private let cache: TimelineCache = .shared