mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 17:51:01 +00:00
New account settings + create / edit server side filters
This commit is contained in:
parent
d0f16c84f7
commit
5cd9ddd945
26 changed files with 681 additions and 41 deletions
|
@ -47,6 +47,7 @@
|
|||
9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */; };
|
||||
9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */; };
|
||||
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
|
||||
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
|
||||
9F8CA5972979B61100481E8E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E9B576C529743F4C00BCE646 /* Localizable.strings */; };
|
||||
9F8CA5982979B63D00481E8E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E9B576C529743F4C00BCE646 /* Localizable.strings */; };
|
||||
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9FAD85822971BF7200496AB1 /* Secret.plist */; };
|
||||
|
@ -156,6 +157,7 @@
|
|||
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = "<group>"; };
|
||||
9F7D939B2980F5C100EE6B7A /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
9F7D939C2980F5C200EE6B7A /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
|
||||
9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = "<group>"; };
|
||||
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -379,6 +381,7 @@
|
|||
9F2A540629699698009B2D7C /* SupportAppView.swift */,
|
||||
9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */,
|
||||
C9B22676297F6C2E001F9EFE /* ContentSettingsView.swift */,
|
||||
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -591,6 +594,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */,
|
||||
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */,
|
||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */,
|
||||
C9B22677297F6C2E001F9EFE /* ContentSettingsView.swift in Sources */,
|
||||
9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */,
|
||||
|
|
|
@ -17,6 +17,8 @@ extension View {
|
|||
AccountDetailView(accountId: id)
|
||||
case let .accountDetailWithAccount(account):
|
||||
AccountDetailView(account: account)
|
||||
case let .accountSettingsWithAccount(account, appAccount):
|
||||
AccountSettingsView(account: account, appAccount: appAccount)
|
||||
case let .statusDetail(id):
|
||||
StatusDetailView(statusId: id)
|
||||
case let .conversationDetail(conversation):
|
||||
|
|
|
@ -3,6 +3,7 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
import Models
|
||||
|
||||
struct SideBarView<Content: View>: View {
|
||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||
|
|
81
IceCubesApp/App/Tabs/Settings/AccountSettingView.swift
Normal file
81
IceCubesApp/App/Tabs/Settings/AccountSettingView.swift
Normal file
|
@ -0,0 +1,81 @@
|
|||
import SwiftUI
|
||||
import Account
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import AppAccount
|
||||
|
||||
struct AccountSettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
|
||||
@State private var isEditingAccount: Bool = false
|
||||
@State private var isEditingFilters: Bool = false
|
||||
|
||||
let account: Account
|
||||
let appAccount: AppAccount
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(value: RouterDestinations.accountDetailWithAccount(account: account)) {
|
||||
Label("See Profile", systemImage: "person.crop.circle")
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
Section {
|
||||
Label("Edit profile", systemImage: "pencil")
|
||||
.onTapGesture {
|
||||
isEditingAccount = true
|
||||
}
|
||||
if currentInstance.isFiltersSupported {
|
||||
Label("Edit Filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
.onTapGesture {
|
||||
isEditingFilters = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
if let token = appAccount.oauthToken {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: [.init(server: appAccount.server,
|
||||
token: token,
|
||||
accountName: appAccount.accountName)])
|
||||
appAccountsManager.delete(account: appAccount)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Logout account")
|
||||
}
|
||||
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
AvatarView(url: account.avatar, size: .embed)
|
||||
Text(account.safeDisplayName)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(account.safeDisplayName)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
}
|
||||
}
|
|
@ -343,3 +343,13 @@
|
|||
"status.visibility.follower" = "Follower";
|
||||
"status.visibility.public" = "Öffentlich";
|
||||
"status.visibility.unlisted" = "Ungelistet";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -346,3 +346,13 @@
|
|||
"status.visibility.follower" = "Followers";
|
||||
"status.visibility.public" = "Everyone";
|
||||
"status.visibility.unlisted" = "Unlisted";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -346,3 +346,13 @@
|
|||
"status.visibility.follower" = "Sólo seguidores";
|
||||
"status.visibility.public" = "Todo el mundo";
|
||||
"status.visibility.unlisted" = "Sin listar";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -343,3 +343,13 @@
|
|||
"status.visibility.follower" = "Solo ai follower";
|
||||
"status.visibility.public" = "A tutti";
|
||||
"status.visibility.unlisted" = "Ai non appartenenti alle liste";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -338,3 +338,14 @@
|
|||
"settings.content.default-visibility" = "投稿の可視化";
|
||||
"settings.content.reading" = "Reading";
|
||||
"settings.content.posting" = "Posting";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -343,3 +343,13 @@
|
|||
"status.visibility.follower" = "Alleen volgers";
|
||||
"status.visibility.public" = "Openbaar";
|
||||
"status.visibility.unlisted" = "Minder openbaar";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -329,3 +329,13 @@
|
|||
"status.visibility.follower" = "Takipçiler";
|
||||
"status.visibility.public" = "Herkes";
|
||||
"status.visibility.unlisted" = "Liste dışı";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -344,3 +344,13 @@
|
|||
"status.visibility.follower" = "粉丝";
|
||||
"status.visibility.public" = "所有人";
|
||||
"status.visibility.unlisted" = "不公开";
|
||||
|
||||
// MARK: Filters
|
||||
"filter.new" = "New Filter";
|
||||
"filter.filters" = "Filters";
|
||||
"filter.edit.title" = "Filter title";
|
||||
"filter.edit.keywords" = "Filter Keywords";
|
||||
"filter.edit.keywords.add" = "Add a new keyword";
|
||||
"filter.edit.contexts" = "Filter Contexts";
|
||||
"filter.edit.action" = "Filter Action";
|
||||
"account.action.edit-filters" = "Edit Filters";
|
||||
|
|
|
@ -13,6 +13,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
@EnvironmentObject private var watcher: StreamWatcher
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var curretnInstance: CurrentInstance
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var client: Client
|
||||
|
@ -25,6 +26,7 @@ public struct AccountDetailView: View {
|
|||
@State private var isCreateListAlertPresented: Bool = false
|
||||
@State private var createListTitle: String = ""
|
||||
@State private var isEditingAccount: Bool = false
|
||||
@State private var isEditingFilters: Bool = false
|
||||
|
||||
/// When coming from a URL like a mention tap in a status.
|
||||
public init(accountId: String) {
|
||||
|
@ -121,6 +123,9 @@ public struct AccountDetailView: View {
|
|||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
@ -507,6 +512,14 @@ public struct AccountDetailView: View {
|
|||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
}
|
||||
|
||||
if curretnInstance.isFiltersSupported {
|
||||
Button {
|
||||
isEditingFilters = true
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,15 @@ import Models
|
|||
import Network
|
||||
import SwiftUI
|
||||
|
||||
struct EditAccountView: View {
|
||||
public struct EditAccountView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@StateObject private var viewModel = EditAccountViewModel()
|
||||
|
||||
public init() { }
|
||||
|
||||
public var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
|
|
216
Packages/Account/Sources/Account/Filters/EditFilterView.swift
Normal file
216
Packages/Account/Sources/Account/Filters/EditFilterView.swift
Normal file
|
@ -0,0 +1,216 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Network
|
||||
|
||||
struct EditFilterView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var account: CurrentAccount
|
||||
@EnvironmentObject private var client: Client
|
||||
|
||||
@State private var isSavingFilter: Bool = false
|
||||
@State private var filter: ServerFilter?
|
||||
@State private var title: String
|
||||
@State private var keywords: [ServerFilter.Keyword]
|
||||
@State private var newKeyword: String = ""
|
||||
@State private var contexts: [ServerFilter.Context]
|
||||
@State private var filterAction: ServerFilter.Action
|
||||
|
||||
@FocusState private var isTitleFocused: Bool
|
||||
|
||||
private var data: ServerFilterData {
|
||||
.init(title: title,
|
||||
context: contexts,
|
||||
filterAction: filterAction,
|
||||
expireIn: nil)
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
!title.isEmpty
|
||||
}
|
||||
|
||||
init(filter: ServerFilter?) {
|
||||
_filter = .init(initialValue: filter)
|
||||
_title = .init(initialValue: filter?.title ?? "")
|
||||
_keywords = .init(initialValue: filter?.keywords ?? [])
|
||||
_contexts = .init(initialValue: filter?.context ?? [.home])
|
||||
_filterAction = .init(initialValue: filter?.filterAction ?? .warn)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
titleSection
|
||||
if filter != nil {
|
||||
keywordsSection
|
||||
contextsSection
|
||||
filterActionView
|
||||
}
|
||||
}
|
||||
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.onAppear {
|
||||
if filter == nil {
|
||||
isTitleFocused = true
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
saveButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var titleSection: some View {
|
||||
Section("filter.edit.title") {
|
||||
TextField("filter.edit.title", text: $title)
|
||||
.focused($isTitleFocused)
|
||||
.onSubmit {
|
||||
Task {
|
||||
await saveFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
}
|
||||
|
||||
private var keywordsSection: some View {
|
||||
Section("filter.edit.keywords") {
|
||||
ForEach(keywords) { keyword in
|
||||
HStack {
|
||||
Text(keyword.keyword)
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
await deleteKeyword(keyword: keyword)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
let keyword = keywords[index]
|
||||
Task {
|
||||
await deleteKeyword(keyword: keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
TextField("filter.edit.keywords.add", text: $newKeyword, axis: .horizontal)
|
||||
.onSubmit {
|
||||
Task {
|
||||
await addKeyword(name: newKeyword)
|
||||
newKeyword = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
private var contextsSection: some View {
|
||||
Section("filter.edit.contexts") {
|
||||
ForEach(ServerFilter.Context.allCases, id: \.self) { context in
|
||||
Toggle(isOn: .init(get: {
|
||||
contexts.contains(where: { $0 == context })
|
||||
}, set: { _ in
|
||||
if let index = contexts.firstIndex(of: context) {
|
||||
contexts.remove(at: index)
|
||||
} else {
|
||||
contexts.append(context)
|
||||
}
|
||||
Task {
|
||||
await saveFilter()
|
||||
}
|
||||
})) {
|
||||
Label(context.name, systemImage: context.iconName)
|
||||
}
|
||||
.disabled(isSavingFilter)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
private var filterActionView: some View {
|
||||
Section("filter.edit.action") {
|
||||
Picker(selection: $filterAction) {
|
||||
ForEach(ServerFilter.Action.allCases, id: \.self) { filter in
|
||||
Text(filter.label)
|
||||
.id(filter)
|
||||
}
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.onChange(of: filterAction) { _ in
|
||||
Task {
|
||||
await saveFilter()
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
private var saveButton: some View {
|
||||
Button {
|
||||
Task {
|
||||
await saveFilter()
|
||||
dismiss()
|
||||
}
|
||||
} label: {
|
||||
if isSavingFilter {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("action.done")
|
||||
}
|
||||
}
|
||||
.disabled(!canSave)
|
||||
}
|
||||
|
||||
private func saveFilter() async {
|
||||
do {
|
||||
isSavingFilter = true
|
||||
if let filter {
|
||||
self.filter = try await client.put(endpoint: ServerFilters.editFilter(id: filter.id, json: data),
|
||||
forceVersion: .v2)
|
||||
} else {
|
||||
let newFilter: ServerFilter = try await client.post(endpoint: ServerFilters.createFilter(json: data),
|
||||
forceVersion: .v2)
|
||||
self.filter = newFilter
|
||||
}
|
||||
} catch {}
|
||||
isSavingFilter = false
|
||||
}
|
||||
|
||||
private func addKeyword(name: String) async {
|
||||
guard let filterId = filter?.id else { return }
|
||||
isSavingFilter = true
|
||||
do {
|
||||
let keyword: ServerFilter.Keyword = try await
|
||||
client.post(endpoint: ServerFilters.addKeyword(filter: filterId,
|
||||
keyword: name,
|
||||
wholeWord: true),
|
||||
forceVersion: .v2)
|
||||
self.keywords.append(keyword)
|
||||
} catch { }
|
||||
isSavingFilter = false
|
||||
}
|
||||
|
||||
private func deleteKeyword(keyword: ServerFilter.Keyword) async {
|
||||
isSavingFilter = true
|
||||
do {
|
||||
let response = try await client.delete(endpoint: ServerFilters.removeKeyword(id: keyword.id),
|
||||
forceVersion: .v2)
|
||||
if response?.statusCode == 200 {
|
||||
keywords.removeAll(where: { $0.id == keyword.id })
|
||||
}
|
||||
} catch { }
|
||||
isSavingFilter = false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import Network
|
||||
import DesignSystem
|
||||
import Models
|
||||
|
||||
public struct FiltersListView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var account: CurrentAccount
|
||||
@EnvironmentObject private var client: Client
|
||||
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var filters: [ServerFilter] = []
|
||||
|
||||
public init() { }
|
||||
|
||||
public var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
if isLoading && filters.isEmpty {
|
||||
ProgressView()
|
||||
} else {
|
||||
ForEach(filters) { filter in
|
||||
NavigationLink(destination: EditFilterView(filter: filter)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(filter.title)
|
||||
.font(.scaledSubheadline)
|
||||
Text("\(filter.context.map{ $0.name }.joined(separator: ", "))")
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexes in
|
||||
deleteFilter(indexes: indexes)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Section {
|
||||
NavigationLink(destination: EditFilterView(filter: nil)) {
|
||||
Label("filter.new", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.toolbar {
|
||||
toolbarContent
|
||||
}
|
||||
.navigationTitle("filter.filters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.task {
|
||||
do {
|
||||
isLoading = true
|
||||
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
|
||||
isLoading = false
|
||||
} catch {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteFilter(indexes: IndexSet) {
|
||||
if let index = indexes.first {
|
||||
Task {
|
||||
do {
|
||||
let response = try await client.delete(endpoint: ServerFilters.filter(id: filters[index].id),
|
||||
forceVersion: .v2)
|
||||
if response?.statusCode == 200 {
|
||||
filters.remove(at: index)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,7 @@ import Models
|
|||
import Network
|
||||
import SwiftUI
|
||||
|
||||
public struct AppAccount: Codable, Identifiable {
|
||||
public let server: String
|
||||
public var accountName: String?
|
||||
public let oauthToken: OauthToken?
|
||||
|
||||
public var id: String {
|
||||
key
|
||||
}
|
||||
|
||||
extension AppAccount {
|
||||
private static var keychain: KeychainSwift {
|
||||
let keychain = KeychainSwift()
|
||||
#if !DEBUG
|
||||
|
@ -21,23 +13,6 @@ public struct AppAccount: Codable, Identifiable {
|
|||
return keychain
|
||||
}
|
||||
|
||||
public var key: String {
|
||||
if let oauthToken {
|
||||
return "\(server):\(oauthToken.createdAt)"
|
||||
} else {
|
||||
return "\(server):anonymous:\(Date().timeIntervalSince1970)"
|
||||
}
|
||||
}
|
||||
|
||||
public init(server: String,
|
||||
accountName: String?,
|
||||
oauthToken: OauthToken? = nil)
|
||||
{
|
||||
self.server = server
|
||||
self.accountName = accountName
|
||||
self.oauthToken = oauthToken
|
||||
}
|
||||
|
||||
public func save() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(self)
|
||||
|
|
|
@ -64,7 +64,7 @@ public struct AppAccountView: View {
|
|||
if appAccounts.currentAccount.id == viewModel.appAccount.id,
|
||||
let account = viewModel.account
|
||||
{
|
||||
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||
routerPath.navigate(to: .accountSettingsWithAccount(account: account, appAccount: viewModel.appAccount))
|
||||
} else {
|
||||
appAccounts.currentAccount = viewModel.appAccount
|
||||
}
|
||||
|
|
|
@ -10,6 +10,14 @@ public class CurrentInstance: ObservableObject {
|
|||
|
||||
public static let shared = CurrentInstance()
|
||||
|
||||
public var isFiltersSupported: Bool {
|
||||
instance?.version.hasPrefix("4") == true
|
||||
}
|
||||
|
||||
public var isEditSupported: Bool {
|
||||
instance?.version.hasPrefix("4") == true
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
public func setClient(client: Client) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import SwiftUI
|
|||
public enum RouterDestinations: Hashable {
|
||||
case accountDetail(id: String)
|
||||
case accountDetailWithAccount(account: Account)
|
||||
case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
|
||||
case statusDetail(id: String)
|
||||
case conversationDetail(conversation: Conversation)
|
||||
case remoteStatusDetail(url: URL)
|
||||
|
|
28
Packages/Models/Sources/Models/AppAccount.swift
Normal file
28
Packages/Models/Sources/Models/AppAccount.swift
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct AppAccount: Codable, Identifiable, Hashable {
|
||||
public let server: String
|
||||
public var accountName: String?
|
||||
public let oauthToken: OauthToken?
|
||||
|
||||
public var key: String {
|
||||
if let oauthToken {
|
||||
return "\(server):\(oauthToken.createdAt)"
|
||||
} else {
|
||||
return "\(server):anonymous:\(Date().timeIntervalSince1970)"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: String {
|
||||
key
|
||||
}
|
||||
|
||||
public init(server: String,
|
||||
accountName: String?,
|
||||
oauthToken: OauthToken? = nil) {
|
||||
self.server = server
|
||||
self.accountName = accountName
|
||||
self.oauthToken = oauthToken
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public struct OauthToken: Codable {
|
||||
public struct OauthToken: Codable, Hashable {
|
||||
public let accessToken: String
|
||||
public let tokenType: String
|
||||
public let scope: String
|
||||
|
|
67
Packages/Models/Sources/Models/ServerFilter.swift
Normal file
67
Packages/Models/Sources/Models/ServerFilter.swift
Normal file
|
@ -0,0 +1,67 @@
|
|||
import Foundation
|
||||
|
||||
public struct ServerFilter: Codable, Identifiable, Hashable {
|
||||
public struct Keyword: Codable, Identifiable, Hashable {
|
||||
public let id: String
|
||||
public let keyword: String
|
||||
public let wholeWord: Bool
|
||||
}
|
||||
|
||||
public enum Context: String, Codable, CaseIterable {
|
||||
case home, notifications, `public`, thread, account
|
||||
}
|
||||
|
||||
public enum Action: String, Codable, CaseIterable {
|
||||
case warn, hide
|
||||
}
|
||||
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let keywords: [Keyword]
|
||||
public let filterAction: Action
|
||||
public let context: [Context]
|
||||
public let expireIn: Int?
|
||||
}
|
||||
|
||||
extension ServerFilter.Context {
|
||||
public var iconName: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "rectangle.on.rectangle"
|
||||
case .notifications:
|
||||
return "bell"
|
||||
case .public:
|
||||
return "globe.americas"
|
||||
case .thread:
|
||||
return "bubble.left.and.bubble.right"
|
||||
case .account:
|
||||
return "person.crop.circle"
|
||||
}
|
||||
}
|
||||
|
||||
public var name: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "Home and lists"
|
||||
case .notifications:
|
||||
return "Notifications"
|
||||
case .public:
|
||||
return "Public timelines"
|
||||
case .thread:
|
||||
return "Conversations"
|
||||
case .account:
|
||||
return "Profiles"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ServerFilter.Action {
|
||||
public var label: String {
|
||||
switch self {
|
||||
case .warn:
|
||||
return "Hide with a warning"
|
||||
case .hide:
|
||||
return "Hide completely"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,12 +107,12 @@ public class Client: ObservableObject, Equatable {
|
|||
return (try decoder.decode(Entity.self, from: data), linkHandler)
|
||||
}
|
||||
|
||||
public func post<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||
try await makeEntityRequest(endpoint: endpoint, method: "POST")
|
||||
public func post<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity {
|
||||
try await makeEntityRequest(endpoint: endpoint, method: "POST", forceVersion: forceVersion)
|
||||
}
|
||||
|
||||
public func post(endpoint: Endpoint) async throws -> HTTPURLResponse? {
|
||||
let url = makeURL(endpoint: endpoint)
|
||||
public func post(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? {
|
||||
let url = makeURL(endpoint: endpoint, forceVersion: forceVersion)
|
||||
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST")
|
||||
let (_, httpResponse) = try await urlSession.data(for: request)
|
||||
return httpResponse as? HTTPURLResponse
|
||||
|
@ -125,12 +125,12 @@ public class Client: ObservableObject, Equatable {
|
|||
return httpResponse as? HTTPURLResponse
|
||||
}
|
||||
|
||||
public func put<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
||||
public func put<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity {
|
||||
try await makeEntityRequest(endpoint: endpoint, method: "PUT", forceVersion: forceVersion)
|
||||
}
|
||||
|
||||
public func delete(endpoint: Endpoint) async throws -> HTTPURLResponse? {
|
||||
let url = makeURL(endpoint: endpoint)
|
||||
public func delete(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> HTTPURLResponse? {
|
||||
let url = makeURL(endpoint: endpoint, forceVersion: forceVersion)
|
||||
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE")
|
||||
let (_, httpResponse) = try await urlSession.data(for: request)
|
||||
return httpResponse as? HTTPURLResponse
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import Foundation
|
||||
import Models
|
||||
|
||||
public enum ServerFilters: Endpoint {
|
||||
case filters
|
||||
case createFilter(json: ServerFilterData)
|
||||
case editFilter(id: String, json: ServerFilterData)
|
||||
case addKeyword(filter: String, keyword: String, wholeWord: Bool)
|
||||
case removeKeyword(id: String)
|
||||
case filter(id: String)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case .filters:
|
||||
return "filters"
|
||||
case .createFilter:
|
||||
return "filters"
|
||||
case let .filter(id):
|
||||
return "filters/\(id)"
|
||||
case let .editFilter(id, _):
|
||||
return "filters/\(id)"
|
||||
case let .addKeyword(id, _, _):
|
||||
return "filters/\(id)/keywords"
|
||||
case let .removeKeyword(id):
|
||||
return "filters/keywords/\(id)"
|
||||
}
|
||||
}
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .addKeyword(_, keyword, wholeWord):
|
||||
return [.init(name: "keyword", value: keyword),
|
||||
.init(name: "whole_word", value: wholeWord ? "true": "false")]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var jsonValue: Encodable? {
|
||||
switch self {
|
||||
case let .createFilter(json):
|
||||
return json
|
||||
case let .editFilter(_, json):
|
||||
return json
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ServerFilterData: Encodable {
|
||||
public let title: String
|
||||
public let context: [ServerFilter.Context]
|
||||
public let filterAction: ServerFilter.Action
|
||||
public let expireIn: Int?
|
||||
|
||||
public init(title: String,
|
||||
context: [ServerFilter.Context],
|
||||
filterAction: ServerFilter.Action,
|
||||
expireIn: Int?) {
|
||||
self.title = title
|
||||
self.context = context
|
||||
self.filterAction = filterAction
|
||||
self.expireIn = expireIn
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import SwiftUI
|
|||
struct StatusRowContextMenu: View {
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var account: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
@ -101,10 +102,12 @@ struct StatusRowContextMenu: View {
|
|||
} label: {
|
||||
Label(viewModel.isPinned ? "status.action.unpin" : "status.action.pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin")
|
||||
}
|
||||
Button {
|
||||
routerPath.presentedSheet = .editStatusEditor(status: viewModel.status)
|
||||
} label: {
|
||||
Label("status.action.edit", systemImage: "pencil")
|
||||
if currentInstance.isEditSupported {
|
||||
Button {
|
||||
routerPath.presentedSheet = .editStatusEditor(status: viewModel.status)
|
||||
} label: {
|
||||
Label("status.action.edit", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
Button(role: .destructive) { Task { await viewModel.delete() } } label: {
|
||||
Label("status.action.delete", systemImage: "trash")
|
||||
|
|
Loading…
Reference in a new issue