New account settings + create / edit server side filters

This commit is contained in:
Thomas Ricouard 2023-01-25 21:18:34 +01:00
parent d0f16c84f7
commit 5cd9ddd945
26 changed files with 681 additions and 41 deletions

View file

@ -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 */,

View file

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

View file

@ -3,6 +3,7 @@ import AppAccount
import DesignSystem
import Env
import SwiftUI
import Models
struct SideBarView<Content: View>: View {
@EnvironmentObject private var appAccounts: AppAccountsManager

View 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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,12 +3,14 @@ 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 {

View 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
}
}

View file

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

View file

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

View file

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

View file

@ -9,6 +9,14 @@ public class CurrentInstance: ObservableObject {
private var client: Client?
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() {}

View file

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

View 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
}
}

View file

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

View 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"
}
}
}

View file

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

View file

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

View file

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