mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-16 17:55:13 +00:00
Code cleanup / format / lint
This commit is contained in:
parent
8cac9df8c6
commit
b89221a535
29 changed files with 239 additions and 262 deletions
|
@ -5,100 +5,100 @@
|
||||||
// Created by Thomas Durand on 26/01/2023.
|
// Created by Thomas Durand on 26/01/2023.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
||||||
extension NSExtensionContext: @unchecked Sendable { }
|
extension NSExtensionContext: @unchecked Sendable {}
|
||||||
|
|
||||||
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
|
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error {
|
||||||
case inputProviderNotFound
|
case inputProviderNotFound
|
||||||
case loadedItemHasWrongType
|
case loadedItemHasWrongType
|
||||||
case urlNotFound
|
case urlNotFound
|
||||||
case noHost
|
case noHost
|
||||||
case notMastodonInstance
|
case notMastodonInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
func beginRequest(with context: NSExtensionContext) {
|
func beginRequest(with context: NSExtensionContext) {
|
||||||
// Do not call super in an Action extension with no user interface
|
// Do not call super in an Action extension with no user interface
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let url = try await url(from: context)
|
let url = try await url(from: context)
|
||||||
guard await url.isMastodonInstance else {
|
guard await url.isMastodonInstance else {
|
||||||
throw Error.notMastodonInstance
|
throw Error.notMastodonInstance
|
||||||
}
|
|
||||||
await MainActor.run {
|
|
||||||
let deeplink = url.iceCubesAppDeepLink
|
|
||||||
let output = output(wrapping: deeplink)
|
|
||||||
context.completeRequest(returningItems: output)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
context.completeRequest(returningItems: [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await MainActor.run {
|
||||||
|
let deeplink = url.iceCubesAppDeepLink
|
||||||
|
let output = output(wrapping: deeplink)
|
||||||
|
context.completeRequest(returningItems: output)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
context.completeRequest(returningItems: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension URL {
|
extension URL {
|
||||||
var isMastodonInstance: Bool {
|
var isMastodonInstance: Bool {
|
||||||
get async {
|
get async {
|
||||||
do {
|
do {
|
||||||
guard let host = host() else {
|
guard let host = host() else {
|
||||||
throw ActionRequestHandler.Error.noHost
|
throw ActionRequestHandler.Error.noHost
|
||||||
}
|
|
||||||
let _: Instance = try await Client(server: host).get(endpoint: Instances.instance)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let _: Instance = try await Client(server: host).get(endpoint: Instances.instance)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var iceCubesAppDeepLink: URL {
|
var iceCubesAppDeepLink: URL {
|
||||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||||
components.scheme = AppInfo.scheme.trimmingCharacters(in: [":", "/"])
|
components.scheme = AppInfo.scheme.trimmingCharacters(in: [":", "/"])
|
||||||
return components.url!
|
return components.url!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ActionRequestHandler {
|
extension ActionRequestHandler {
|
||||||
/// Will look for an input item that might provide the property list that Javascript sent us
|
/// Will look for an input item that might provide the property list that Javascript sent us
|
||||||
private func url(from context: NSExtensionContext) async throws -> URL {
|
private func url(from context: NSExtensionContext) async throws -> URL {
|
||||||
for item in context.inputItems as! [NSExtensionItem] {
|
for item in context.inputItems as! [NSExtensionItem] {
|
||||||
guard let attachments = item.attachments else {
|
guard let attachments = item.attachments else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for itemProvider in attachments {
|
for itemProvider in attachments {
|
||||||
guard itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
|
guard itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
guard let dictionary = try await itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any] else {
|
|
||||||
throw Error.loadedItemHasWrongType
|
|
||||||
}
|
|
||||||
let input = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:]
|
|
||||||
guard let absoluteStringUrl = input["url"] as? String, let url = URL(string: absoluteStringUrl) else {
|
|
||||||
throw Error.urlNotFound
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw Error.inputProviderNotFound
|
guard let dictionary = try await itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any] else {
|
||||||
|
throw Error.loadedItemHasWrongType
|
||||||
|
}
|
||||||
|
let input = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:]
|
||||||
|
guard let absoluteStringUrl = input["url"] as? String, let url = URL(string: absoluteStringUrl) else {
|
||||||
|
throw Error.urlNotFound
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw Error.inputProviderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrap the output to the expected object so we send back results to JS
|
/// Wrap the output to the expected object so we send back results to JS
|
||||||
private func output(wrapping deeplink: URL) -> [NSExtensionItem] {
|
private func output(wrapping deeplink: URL) -> [NSExtensionItem] {
|
||||||
let results = ["deeplink": deeplink.absoluteString]
|
let results = ["deeplink": deeplink.absoluteString]
|
||||||
let dictionary = [NSExtensionJavaScriptFinalizeArgumentKey: results]
|
let dictionary = [NSExtensionJavaScriptFinalizeArgumentKey: results]
|
||||||
let provider = NSItemProvider(item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
|
let provider = NSItemProvider(item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
|
||||||
let item = NSExtensionItem()
|
let item = NSExtensionItem()
|
||||||
item.attachments = [provider]
|
item.attachments = [provider]
|
||||||
return [item]
|
return [item]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ import Account
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct SideBarView<Content: View>: View {
|
struct SideBarView<Content: View>: View {
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import SwiftUI
|
|
||||||
import Account
|
import Account
|
||||||
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import AppAccount
|
import SwiftUI
|
||||||
|
|
||||||
struct AccountSettingsView: View {
|
struct AccountSettingsView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||||
|
|
||||||
@State private var isEditingAccount: Bool = false
|
@State private var isEditingAccount: Bool = false
|
||||||
@State private var isEditingFilters: Bool = false
|
@State private var isEditingFilters: Bool = false
|
||||||
|
|
||||||
let account: Account
|
let account: Account
|
||||||
let appAccount: AppAccount
|
let appAccount: AppAccount
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -54,7 +54,6 @@ struct AccountSettingsView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Text("account.action.logout")
|
Text("account.action.logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,9 @@ import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
struct ContentSettingsView: View {
|
struct ContentSettingsView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@EnvironmentObject private var userPreferences: UserPreferences
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -19,7 +18,7 @@ struct ContentSettingsView: View {
|
||||||
Text("settings.content.use-instance-settings")
|
Text("settings.content.use-instance-settings")
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.content.main-toggle.description")
|
Text("settings.content.main-toggle.description")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.onChange(of: userPreferences.useInstanceContentSettings) { newVal in
|
.onChange(of: userPreferences.useInstanceContentSettings) { newVal in
|
||||||
|
@ -29,15 +28,14 @@ struct ContentSettingsView: View {
|
||||||
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
||||||
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("settings.content.reading") {
|
Section("settings.content.reading") {
|
||||||
Toggle(isOn: $userPreferences.appAutoExpandSpoilers) {
|
Toggle(isOn: $userPreferences.appAutoExpandSpoilers) {
|
||||||
Text("settings.content.expand-spoilers")
|
Text("settings.content.expand-spoilers")
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
|
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
|
||||||
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
|
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
|
||||||
Text(media.description).tag(media)
|
Text(media.description).tag(media)
|
||||||
|
@ -45,7 +43,7 @@ struct ContentSettingsView: View {
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
}.listRowBackground(theme.primaryBackgroundColor)
|
}.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section("settings.content.posting") {
|
Section("settings.content.posting") {
|
||||||
Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) {
|
Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) {
|
||||||
ForEach(Visibility.allCases, id: \.rawValue) { vis in
|
ForEach(Visibility.allCases, id: \.rawValue) { vis in
|
||||||
|
@ -53,23 +51,16 @@ struct ContentSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
|
|
||||||
Toggle(isOn: $userPreferences.appDefaultPostsSensitive) {
|
Toggle(isOn: $userPreferences.appDefaultPostsSensitive) {
|
||||||
Text("settings.content.default-sensitive")
|
Text("settings.content.default-sensitive")
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.content.navigation-title")
|
.navigationTitle("settings.content.navigation-title")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct PushNotificationsView: View {
|
||||||
Text("settings.push.main-toggle.description")
|
Text("settings.push.main-toggle.description")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
if subscription.isEnabled {
|
if subscription.isEnabled {
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: .init(get: {
|
Toggle(isOn: .init(get: {
|
||||||
|
@ -87,7 +87,7 @@ struct PushNotificationsView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button("settings.push.duplicate.button.fix") {
|
Button("settings.push.duplicate.button.fix") {
|
||||||
Task {
|
Task {
|
||||||
|
@ -101,7 +101,6 @@ struct PushNotificationsView: View {
|
||||||
Text("settings.push.duplicate.footer")
|
Text("settings.push.duplicate.footer")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.push.navigation-title")
|
.navigationTitle("settings.push.navigation-title")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
@ -116,7 +115,7 @@ struct PushNotificationsView: View {
|
||||||
await subscription.updateSubscription()
|
await subscription.updateSubscription()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteSubscription() {
|
private func deleteSubscription() {
|
||||||
Task {
|
Task {
|
||||||
await subscription.deleteSubscription()
|
await subscription.deleteSubscription()
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Timeline
|
||||||
|
|
||||||
struct SettingsTabs: View {
|
struct SettingsTabs: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@ -75,7 +75,8 @@ struct SettingsTabs: View {
|
||||||
if let index = indexSet.first {
|
if let index = indexSet.first {
|
||||||
let account = appAccountsManager.availableAccounts[index]
|
let account = appAccountsManager.availableAccounts[index]
|
||||||
if let token = account.oauthToken,
|
if let token = account.oauthToken,
|
||||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
||||||
|
{
|
||||||
Task {
|
Task {
|
||||||
await sub.deleteSubscription()
|
await sub.deleteSubscription()
|
||||||
appAccountsManager.delete(account: account)
|
appAccountsManager.delete(account: account)
|
||||||
|
|
|
@ -25,7 +25,7 @@ public struct AccountDetailView: View {
|
||||||
@State private var isCurrentUser: Bool = false
|
@State private var isCurrentUser: Bool = false
|
||||||
@State private var isCreateListAlertPresented: Bool = false
|
@State private var isCreateListAlertPresented: Bool = false
|
||||||
@State private var createListTitle: String = ""
|
@State private var createListTitle: String = ""
|
||||||
|
|
||||||
@State private var isEditingAccount: Bool = false
|
@State private var isEditingAccount: Bool = false
|
||||||
@State private var isEditingFilters: Bool = false
|
@State private var isEditingFilters: Bool = false
|
||||||
|
|
||||||
|
@ -513,7 +513,7 @@ public struct AccountDetailView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-info", systemImage: "pencil")
|
Label("account.action.edit-info", systemImage: "pencil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if curretnInstance.isFiltersSupported {
|
if curretnInstance.isFiltersSupported {
|
||||||
Button {
|
Button {
|
||||||
isEditingFilters = true
|
isEditingFilters = true
|
||||||
|
@ -521,7 +521,7 @@ public struct AccountDetailView: View {
|
||||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
routerPath.presentedSheet = .accountPushNotficationsSettings
|
routerPath.presentedSheet = .accountPushNotficationsSettings
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -9,8 +9,8 @@ public struct EditAccountView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@StateObject private var viewModel = EditAccountViewModel()
|
@StateObject private var viewModel = EditAccountViewModel()
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct EditFilterView: View {
|
struct EditFilterView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var account: CurrentAccount
|
@EnvironmentObject private var account: CurrentAccount
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@State private var isSavingFilter: Bool = false
|
@State private var isSavingFilter: Bool = false
|
||||||
@State private var filter: ServerFilter?
|
@State private var filter: ServerFilter?
|
||||||
@State private var title: String
|
@State private var title: String
|
||||||
|
@ -18,20 +18,20 @@ struct EditFilterView: View {
|
||||||
@State private var newKeyword: String = ""
|
@State private var newKeyword: String = ""
|
||||||
@State private var contexts: [ServerFilter.Context]
|
@State private var contexts: [ServerFilter.Context]
|
||||||
@State private var filterAction: ServerFilter.Action
|
@State private var filterAction: ServerFilter.Action
|
||||||
|
|
||||||
@FocusState private var isTitleFocused: Bool
|
@FocusState private var isTitleFocused: Bool
|
||||||
|
|
||||||
private var data: ServerFilterData {
|
private var data: ServerFilterData {
|
||||||
.init(title: title,
|
.init(title: title,
|
||||||
context: contexts,
|
context: contexts,
|
||||||
filterAction: filterAction,
|
filterAction: filterAction,
|
||||||
expireIn: nil)
|
expireIn: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canSave: Bool {
|
private var canSave: Bool {
|
||||||
!title.isEmpty
|
!title.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
init(filter: ServerFilter?) {
|
init(filter: ServerFilter?) {
|
||||||
_filter = .init(initialValue: filter)
|
_filter = .init(initialValue: filter)
|
||||||
_title = .init(initialValue: filter?.title ?? "")
|
_title = .init(initialValue: filter?.title ?? "")
|
||||||
|
@ -39,7 +39,7 @@ struct EditFilterView: View {
|
||||||
_contexts = .init(initialValue: filter?.context ?? [.home])
|
_contexts = .init(initialValue: filter?.context ?? [.home])
|
||||||
_filterAction = .init(initialValue: filter?.filterAction ?? .warn)
|
_filterAction = .init(initialValue: filter?.filterAction ?? .warn)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
titleSection
|
titleSection
|
||||||
|
@ -55,7 +55,7 @@ struct EditFilterView: View {
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
isTitleFocused = true
|
isTitleFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
@ -64,7 +64,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var titleSection: some View {
|
private var titleSection: some View {
|
||||||
Section("filter.edit.title") {
|
Section("filter.edit.title") {
|
||||||
TextField("filter.edit.title", text: $title)
|
TextField("filter.edit.title", text: $title)
|
||||||
|
@ -76,9 +76,8 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var keywordsSection: some View {
|
private var keywordsSection: some View {
|
||||||
Section("filter.edit.keywords") {
|
Section("filter.edit.keywords") {
|
||||||
ForEach(keywords) { keyword in
|
ForEach(keywords) { keyword in
|
||||||
|
@ -113,7 +112,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contextsSection: some View {
|
private var contextsSection: some View {
|
||||||
Section("filter.edit.contexts") {
|
Section("filter.edit.contexts") {
|
||||||
ForEach(ServerFilter.Context.allCases, id: \.self) { context in
|
ForEach(ServerFilter.Context.allCases, id: \.self) { context in
|
||||||
|
@ -136,7 +135,7 @@ struct EditFilterView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filterActionView: some View {
|
private var filterActionView: some View {
|
||||||
Section("filter.edit.action") {
|
Section("filter.edit.action") {
|
||||||
Picker(selection: $filterAction) {
|
Picker(selection: $filterAction) {
|
||||||
|
@ -156,7 +155,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var saveButton: some View {
|
private var saveButton: some View {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
|
@ -172,7 +171,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
.disabled(!canSave)
|
.disabled(!canSave)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveFilter() async {
|
private func saveFilter() async {
|
||||||
do {
|
do {
|
||||||
isSavingFilter = true
|
isSavingFilter = true
|
||||||
|
@ -182,26 +181,26 @@ struct EditFilterView: View {
|
||||||
} else {
|
} else {
|
||||||
let newFilter: ServerFilter = try await client.post(endpoint: ServerFilters.createFilter(json: data),
|
let newFilter: ServerFilter = try await client.post(endpoint: ServerFilters.createFilter(json: data),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
self.filter = newFilter
|
filter = newFilter
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
isSavingFilter = false
|
isSavingFilter = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addKeyword(name: String) async {
|
private func addKeyword(name: String) async {
|
||||||
guard let filterId = filter?.id else { return }
|
guard let filterId = filter?.id else { return }
|
||||||
isSavingFilter = true
|
isSavingFilter = true
|
||||||
do {
|
do {
|
||||||
let keyword: ServerFilter.Keyword = try await
|
let keyword: ServerFilter.Keyword = try await
|
||||||
client.post(endpoint: ServerFilters.addKeyword(filter: filterId,
|
client.post(endpoint: ServerFilters.addKeyword(filter: filterId,
|
||||||
keyword: name,
|
keyword: name,
|
||||||
wholeWord: true),
|
wholeWord: true),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
self.keywords.append(keyword)
|
keywords.append(keyword)
|
||||||
} catch { }
|
} catch {}
|
||||||
isSavingFilter = false
|
isSavingFilter = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteKeyword(keyword: ServerFilter.Keyword) async {
|
private func deleteKeyword(keyword: ServerFilter.Keyword) async {
|
||||||
isSavingFilter = true
|
isSavingFilter = true
|
||||||
do {
|
do {
|
||||||
|
@ -210,7 +209,7 @@ struct EditFilterView: View {
|
||||||
if response?.statusCode == 200 {
|
if response?.statusCode == 200 {
|
||||||
keywords.removeAll(where: { $0.id == keyword.id })
|
keywords.removeAll(where: { $0.id == keyword.id })
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
isSavingFilter = false
|
isSavingFilter = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct FiltersListView: View {
|
public struct FiltersListView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var account: CurrentAccount
|
@EnvironmentObject private var account: CurrentAccount
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@State private var isLoading: Bool = true
|
@State private var isLoading: Bool = true
|
||||||
@State private var filters: [ServerFilter] = []
|
@State private var filters: [ServerFilter] = []
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
@ -31,7 +31,7 @@ public struct FiltersListView: View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(filter.title)
|
Text(filter.title)
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
Text("\(filter.context.map{ $0.name }.joined(separator: ", "))")
|
Text("\(filter.context.map { $0.name }.joined(separator: ", "))")
|
||||||
.font(.scaledBody)
|
.font(.scaledBody)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ public struct FiltersListView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: EditFilterView(filter: nil)) {
|
NavigationLink(destination: EditFilterView(filter: nil)) {
|
||||||
Label("filter.new", systemImage: "plus")
|
Label("filter.new", systemImage: "plus")
|
||||||
|
@ -70,7 +70,7 @@ public struct FiltersListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteFilter(indexes: IndexSet) {
|
private func deleteFilter(indexes: IndexSet) {
|
||||||
if let index = indexes.first {
|
if let index = indexes.first {
|
||||||
Task {
|
Task {
|
||||||
|
@ -84,7 +84,7 @@ public struct FiltersListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbarContent: some ToolbarContent {
|
private var toolbarContent: some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension AppAccount {
|
public extension AppAccount {
|
||||||
private static var keychain: KeychainSwift {
|
private static var keychain: KeychainSwift {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
#if !DEBUG && !targetEnvironment(simulator)
|
#if !DEBUG && !targetEnvironment(simulator)
|
||||||
|
@ -13,17 +13,17 @@ extension AppAccount {
|
||||||
return keychain
|
return keychain
|
||||||
}
|
}
|
||||||
|
|
||||||
public func save() throws {
|
func save() throws {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
let data = try encoder.encode(self)
|
let data = try encoder.encode(self)
|
||||||
Self.keychain.set(data, forKey: key)
|
Self.keychain.set(data, forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete() {
|
func delete() {
|
||||||
Self.keychain.delete(key)
|
Self.keychain.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func retrieveAll() -> [AppAccount] {
|
static func retrieveAll() -> [AppAccount] {
|
||||||
migrateLegacyAccounts()
|
migrateLegacyAccounts()
|
||||||
let keychain = Self.keychain
|
let keychain = Self.keychain
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
|
@ -39,7 +39,7 @@ extension AppAccount {
|
||||||
return accounts
|
return accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func migrateLegacyAccounts() {
|
static func migrateLegacyAccounts() {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
let keys = keychain.allKeys
|
let keys = keychain.allKeys
|
||||||
|
@ -52,7 +52,7 @@ extension AppAccount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func deleteAll() {
|
static func deleteAll() {
|
||||||
let keychain = Self.keychain
|
let keychain = Self.keychain
|
||||||
let keys = keychain.allKeys
|
let keys = keychain.allKeys
|
||||||
for key in keys {
|
for key in keys {
|
||||||
|
|
|
@ -101,7 +101,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
Label("app-account.button.add", systemImage: "person.badge.plus")
|
Label("app-account.button.add", systemImage: "person.badge.plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
Divider()
|
Divider()
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -39,8 +39,8 @@ public struct ConversationDetailView: View {
|
||||||
ForEach(viewModel.messages) { message in
|
ForEach(viewModel.messages) { message in
|
||||||
ConversationMessageView(message: message,
|
ConversationMessageView(message: message,
|
||||||
conversation: viewModel.conversation)
|
conversation: viewModel.conversation)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.id(message.id)
|
.id(message.id)
|
||||||
}
|
}
|
||||||
bottomAnchorView
|
bottomAnchorView
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ public struct ConversationDetailView: View {
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 14)
|
RoundedRectangle(cornerRadius: 14)
|
||||||
.stroke(.gray, lineWidth: 1)
|
.stroke(.gray, lineWidth: 1)
|
||||||
)
|
)
|
||||||
.font(.scaledBody)
|
.font(.scaledBody)
|
||||||
if !viewModel.newMessageText.isEmpty {
|
if !viewModel.newMessageText.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -68,7 +68,7 @@ struct ConversationMessageView: View {
|
||||||
}
|
}
|
||||||
Group {
|
Group {
|
||||||
Text(message.createdAt.shortDateFormatted) +
|
Text(message.createdAt.shortDateFormatted) +
|
||||||
Text(" ")
|
Text(" ")
|
||||||
Text(message.createdAt.asDate, style: .time)
|
Text(message.createdAt.asDate, style: .time)
|
||||||
}
|
}
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
|
|
|
@ -9,11 +9,11 @@ public class CurrentInstance: ObservableObject {
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
public static let shared = CurrentInstance()
|
public static let shared = CurrentInstance()
|
||||||
|
|
||||||
public var isFiltersSupported: Bool {
|
public var isFiltersSupported: Bool {
|
||||||
instance?.version.hasPrefix("4") == true
|
instance?.version.hasPrefix("4") == true
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isEditSupported: Bool {
|
public var isEditSupported: Bool {
|
||||||
instance?.version.hasPrefix("4") == true
|
instance?.version.hasPrefix("4") == true
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class PushNotificationsService: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static let shared = PushNotificationsService()
|
public static let shared = PushNotificationsService()
|
||||||
|
|
||||||
public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = []
|
public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = []
|
||||||
|
|
||||||
@Published public var pushToken: Data?
|
@Published public var pushToken: Data?
|
||||||
|
@ -47,7 +47,7 @@ public class PushNotificationsService: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setAccounts(accounts: [PushAccount]) {
|
public func setAccounts(accounts: [PushAccount]) {
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
|
@ -58,13 +58,13 @@ public class PushNotificationsService: ObservableObject {
|
||||||
subscriptions.append(sub)
|
subscriptions.append(sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateSubscriptions(forceCreate: Bool) async {
|
public func updateSubscriptions(forceCreate: Bool) async {
|
||||||
for subscription in subscriptions {
|
for subscription in subscriptions {
|
||||||
await withTaskGroup(of: Void.self, body: { group in
|
await withTaskGroup(of: Void.self, body: { group in
|
||||||
group.addTask {
|
group.addTask {
|
||||||
await subscription.fetchSubscription()
|
await subscription.fetchSubscription()
|
||||||
if await subscription.subscription != nil && !forceCreate {
|
if await subscription.subscription != nil, !forceCreate {
|
||||||
await subscription.deleteSubscription()
|
await subscription.deleteSubscription()
|
||||||
await subscription.updateSubscription()
|
await subscription.updateSubscription()
|
||||||
} else if forceCreate {
|
} else if forceCreate {
|
||||||
|
@ -136,23 +136,23 @@ public class PushNotificationSubscriptionSettings: ObservableObject {
|
||||||
@Published public var isMentionNotificationEnabled: Bool = true
|
@Published public var isMentionNotificationEnabled: Bool = true
|
||||||
@Published public var isPollNotificationEnabled: Bool = true
|
@Published public var isPollNotificationEnabled: Bool = true
|
||||||
@Published public var isNewPostsNotificationEnabled: Bool = true
|
@Published public var isNewPostsNotificationEnabled: Bool = true
|
||||||
|
|
||||||
public let account: PushAccount
|
public let account: PushAccount
|
||||||
|
|
||||||
private let key: Data
|
private let key: Data
|
||||||
private let authKey: Data
|
private let authKey: Data
|
||||||
|
|
||||||
public var pushToken: Data?
|
public var pushToken: Data?
|
||||||
|
|
||||||
public private(set) var subscription: PushSubscription?
|
public private(set) var subscription: PushSubscription?
|
||||||
|
|
||||||
public init(account: PushAccount, key: Data, authKey: Data, pushToken: Data?) {
|
public init(account: PushAccount, key: Data, authKey: Data, pushToken: Data?) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.key = key
|
self.key = key
|
||||||
self.authKey = authKey
|
self.authKey = authKey
|
||||||
self.pushToken = pushToken
|
self.pushToken = pushToken
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshSubscriptionsUI() {
|
private func refreshSubscriptionsUI() {
|
||||||
if let subscription {
|
if let subscription {
|
||||||
isFollowNotificationEnabled = subscription.alerts.follow
|
isFollowNotificationEnabled = subscription.alerts.follow
|
||||||
|
@ -163,7 +163,7 @@ public class PushNotificationSubscriptionSettings: ObservableObject {
|
||||||
isNewPostsNotificationEnabled = subscription.alerts.status
|
isNewPostsNotificationEnabled = subscription.alerts.status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateSubscription() async {
|
public func updateSubscription() async {
|
||||||
guard let pushToken = pushToken else { return }
|
guard let pushToken = pushToken else { return }
|
||||||
let client = Client(server: account.server, oauthToken: account.token)
|
let client = Client(server: account.server, oauthToken: account.token)
|
||||||
|
@ -176,23 +176,23 @@ public class PushNotificationSubscriptionSettings: ObservableObject {
|
||||||
listenerURL += "?sandbox=true"
|
listenerURL += "?sandbox=true"
|
||||||
#endif
|
#endif
|
||||||
subscription =
|
subscription =
|
||||||
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
||||||
p256dh: key,
|
p256dh: key,
|
||||||
auth: authKey,
|
auth: authKey,
|
||||||
mentions: isMentionNotificationEnabled,
|
mentions: isMentionNotificationEnabled,
|
||||||
status: isNewPostsNotificationEnabled,
|
status: isNewPostsNotificationEnabled,
|
||||||
reblog: isReblogNotificationEnabled,
|
reblog: isReblogNotificationEnabled,
|
||||||
follow: isFollowNotificationEnabled,
|
follow: isFollowNotificationEnabled,
|
||||||
favorite: isFavoriteNotificationEnabled,
|
favorite: isFavoriteNotificationEnabled,
|
||||||
poll: isPollNotificationEnabled))
|
poll: isPollNotificationEnabled))
|
||||||
isEnabled = subscription != nil
|
isEnabled = subscription != nil
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
}
|
}
|
||||||
refreshSubscriptionsUI()
|
refreshSubscriptionsUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteSubscription() async {
|
public func deleteSubscription() async {
|
||||||
let client = Client(server: account.server, oauthToken: account.token)
|
let client = Client(server: account.server, oauthToken: account.token)
|
||||||
do {
|
do {
|
||||||
|
@ -206,7 +206,7 @@ public class PushNotificationSubscriptionSettings: ObservableObject {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchSubscription() async {
|
public func fetchSubscription() async {
|
||||||
let client = Client(server: account.server, oauthToken: account.token)
|
let client = Client(server: account.server, oauthToken: account.token)
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -35,7 +35,7 @@ public enum SheetDestinations: Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
||||||
.mentionStatusEditor, .settings, .accountPushNotficationsSettings:
|
.mentionStatusEditor, .settings, .accountPushNotficationsSettings:
|
||||||
return "statusEditor"
|
return "statusEditor"
|
||||||
case .listEdit:
|
case .listEdit:
|
||||||
return "listEdit"
|
return "listEdit"
|
||||||
|
@ -67,8 +67,8 @@ public class RouterPath: ObservableObject {
|
||||||
|
|
||||||
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
|
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
|
||||||
if url.pathComponents.count == 3 && url.pathComponents[1] == "tags" &&
|
if url.pathComponents.count == 3 && url.pathComponents[1] == "tags" &&
|
||||||
url.host() == status.account.url?.host(),
|
url.host() == status.account.url?.host(),
|
||||||
let tag = url.pathComponents.last
|
let tag = url.pathComponents.last
|
||||||
{
|
{
|
||||||
// OK this test looks weird but it's
|
// OK this test looks weird but it's
|
||||||
// A 3 component path i.e. ["/", "tags", "tagname"]
|
// A 3 component path i.e. ["/", "tags", "tagname"]
|
||||||
|
@ -110,7 +110,8 @@ public class RouterPath: ObservableObject {
|
||||||
} else if let client = client,
|
} else if let client = client,
|
||||||
client.isAuth,
|
client.isAuth,
|
||||||
client.hasConnection(with: url),
|
client.hasConnection(with: url),
|
||||||
let id = Int(url.lastPathComponent) {
|
let id = Int(url.lastPathComponent)
|
||||||
|
{
|
||||||
if url.absoluteString.contains(client.server) {
|
if url.absoluteString.contains(client.server) {
|
||||||
navigate(to: .statusDetail(id: String(id)))
|
navigate(to: .statusDetail(id: String(id)))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,59 +19,44 @@ public class UserPreferences: ObservableObject {
|
||||||
|
|
||||||
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
|
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
|
||||||
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = true
|
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = true
|
||||||
|
|
||||||
@AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
|
@AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
|
||||||
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
|
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
|
||||||
@AppStorage("app_auto_expand_media") public var appAutoExpandMedia:ServerPreferences.AutoExpandMedia = .hideSensitive
|
@AppStorage("app_auto_expand_media") public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia = .hideSensitive
|
||||||
@AppStorage("app_default_post_visibility") public var appDefaultPostVisibility:Models.Visibility = .pub
|
@AppStorage("app_default_post_visibility") public var appDefaultPostVisibility: Models.Visibility = .pub
|
||||||
@AppStorage("app_default_posts_sensitive") public var appDefaultPostsSensitive = false
|
@AppStorage("app_default_posts_sensitive") public var appDefaultPostsSensitive = false
|
||||||
|
|
||||||
|
public var postVisibility: Models.Visibility {
|
||||||
|
if useInstanceContentSettings {
|
||||||
|
return serverPreferences?.postVisibility ?? .pub
|
||||||
|
} else {
|
||||||
|
return appDefaultPostVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var postVisibility:Models.Visibility {
|
public var postIsSensitive: Bool {
|
||||||
get{
|
if useInstanceContentSettings {
|
||||||
if useInstanceContentSettings {
|
return serverPreferences?.postIsSensitive ?? false
|
||||||
return serverPreferences?.postVisibility ?? .pub
|
} else {
|
||||||
}
|
return appDefaultPostsSensitive
|
||||||
else {
|
|
||||||
return appDefaultPostVisibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var postIsSensitive:Bool {
|
|
||||||
get {
|
|
||||||
if useInstanceContentSettings {
|
|
||||||
return serverPreferences?.postIsSensitive ?? false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return appDefaultPostsSensitive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public var autoExpandSpoilers: Bool {
|
public var autoExpandSpoilers: Bool {
|
||||||
get {
|
if useInstanceContentSettings {
|
||||||
if useInstanceContentSettings {
|
return serverPreferences?.autoExpandSpoilers ?? true
|
||||||
return serverPreferences?.autoExpandSpoilers ?? true
|
} else {
|
||||||
}
|
return appAutoExpandSpoilers
|
||||||
else {
|
|
||||||
return appAutoExpandSpoilers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var autoExpandMedia: ServerPreferences.AutoExpandMedia {
|
public var autoExpandMedia: ServerPreferences.AutoExpandMedia {
|
||||||
get {
|
if useInstanceContentSettings {
|
||||||
if useInstanceContentSettings {
|
return serverPreferences?.autoExpandMedia ?? .hideSensitive
|
||||||
return serverPreferences?.autoExpandMedia ?? .hideSensitive
|
} else {
|
||||||
}
|
return appAutoExpandMedia
|
||||||
else {
|
|
||||||
return appAutoExpandMedia
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public var pushNotificationsCount: Int {
|
public var pushNotificationsCount: Int {
|
||||||
get {
|
get {
|
||||||
|
|
|
@ -33,7 +33,7 @@ extension ServerDate {
|
||||||
public var relativeFormatted: String {
|
public var relativeFormatted: String {
|
||||||
return Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date())
|
return Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
public var shortDateFormatted: String {
|
public var shortDateFormatted: String {
|
||||||
return Self.createdAtShortDateFormatted.string(from: asDate)
|
return Self.createdAtShortDateFormatted.string(from: asDate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||||
public let server: String
|
public let server: String
|
||||||
public var accountName: String?
|
public var accountName: String?
|
||||||
public let oauthToken: OauthToken?
|
public let oauthToken: OauthToken?
|
||||||
|
|
||||||
public var key: String {
|
public var key: String {
|
||||||
if let oauthToken {
|
if let oauthToken {
|
||||||
return "\(server):\(oauthToken.createdAt)"
|
return "\(server):\(oauthToken.createdAt)"
|
||||||
|
@ -13,14 +13,15 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||||
return "\(server):anonymous"
|
return "\(server):anonymous"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(server: String,
|
public init(server: String,
|
||||||
accountName: String?,
|
accountName: String?,
|
||||||
oauthToken: OauthToken? = nil) {
|
oauthToken: OauthToken? = nil)
|
||||||
|
{
|
||||||
self.server = server
|
self.server = server
|
||||||
self.accountName = accountName
|
self.accountName = accountName
|
||||||
self.oauthToken = oauthToken
|
self.oauthToken = oauthToken
|
||||||
|
|
|
@ -31,13 +31,13 @@ public struct Poll: Codable, Equatable, Hashable {
|
||||||
|
|
||||||
public struct NullableString: Codable, Equatable, Hashable {
|
public struct NullableString: Codable, Equatable, Hashable {
|
||||||
public let value: String?
|
public let value: String?
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
do {
|
do {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
self.value = try container.decode(String.self)
|
value = try container.decode(String.self)
|
||||||
} catch {
|
} catch {
|
||||||
self.value = nil
|
value = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,15 @@ public struct ServerFilter: Codable, Identifiable, Hashable {
|
||||||
public let keyword: String
|
public let keyword: String
|
||||||
public let wholeWord: Bool
|
public let wholeWord: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Context: String, Codable, CaseIterable {
|
public enum Context: String, Codable, CaseIterable {
|
||||||
case home, notifications, `public`, thread, account
|
case home, notifications, `public`, thread, account
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Action: String, Codable, CaseIterable {
|
public enum Action: String, Codable, CaseIterable {
|
||||||
case warn, hide
|
case warn, hide
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let keywords: [Keyword]
|
public let keywords: [Keyword]
|
||||||
|
@ -23,8 +23,8 @@ public struct ServerFilter: Codable, Identifiable, Hashable {
|
||||||
public let expireIn: Int?
|
public let expireIn: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ServerFilter.Context {
|
public extension ServerFilter.Context {
|
||||||
public var iconName: String {
|
var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "rectangle.on.rectangle"
|
return "rectangle.on.rectangle"
|
||||||
|
@ -38,8 +38,8 @@ extension ServerFilter.Context {
|
||||||
return "person.crop.circle"
|
return "person.crop.circle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var name: String {
|
var name: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "Home and lists"
|
return "Home and lists"
|
||||||
|
@ -55,8 +55,8 @@ extension ServerFilter.Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ServerFilter.Action {
|
public extension ServerFilter.Action {
|
||||||
public var label: String {
|
var label: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .warn:
|
case .warn:
|
||||||
return "Hide with a warning"
|
return "Hide with a warning"
|
||||||
|
|
|
@ -12,7 +12,7 @@ public struct ServerPreferences: Decodable {
|
||||||
case showAll = "show_all"
|
case showAll = "show_all"
|
||||||
case hideAll = "hide_all"
|
case hideAll = "hide_all"
|
||||||
case hideSensitive = "default"
|
case hideSensitive = "default"
|
||||||
|
|
||||||
public var description: LocalizedStringKey {
|
public var description: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .showAll:
|
case .showAll:
|
||||||
|
|
|
@ -30,12 +30,12 @@ public enum ServerFilters: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case let .addKeyword(_, keyword, wholeWord):
|
case let .addKeyword(_, keyword, wholeWord):
|
||||||
return [.init(name: "keyword", value: keyword),
|
return [.init(name: "keyword", value: keyword),
|
||||||
.init(name: "whole_word", value: wholeWord ? "true": "false")]
|
.init(name: "whole_word", value: wholeWord ? "true" : "false")]
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var jsonValue: Encodable? {
|
public var jsonValue: Encodable? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .createFilter(json):
|
case let .createFilter(json):
|
||||||
|
@ -53,11 +53,12 @@ public struct ServerFilterData: Encodable {
|
||||||
public let context: [ServerFilter.Context]
|
public let context: [ServerFilter.Context]
|
||||||
public let filterAction: ServerFilter.Action
|
public let filterAction: ServerFilter.Action
|
||||||
public let expireIn: Int?
|
public let expireIn: Int?
|
||||||
|
|
||||||
public init(title: String,
|
public init(title: String,
|
||||||
context: [ServerFilter.Context],
|
context: [ServerFilter.Context],
|
||||||
filterAction: ServerFilter.Action,
|
filterAction: ServerFilter.Action,
|
||||||
expireIn: Int?) {
|
expireIn: Int?)
|
||||||
|
{
|
||||||
self.title = title
|
self.title = title
|
||||||
self.context = context
|
self.context = context
|
||||||
self.filterAction = filterAction
|
self.filterAction = filterAction
|
||||||
|
|
|
@ -76,7 +76,7 @@ class NotificationsViewModel: ObservableObject {
|
||||||
newNotifications = newNotifications.filter { notification in
|
newNotifications = newNotifications.filter { notification in
|
||||||
!consolidatedNotifications.contains(where: { $0.id == notification.id })
|
!consolidatedNotifications.contains(where: { $0.id == notification.id })
|
||||||
}
|
}
|
||||||
self.notifications.append(contentsOf: newNotifications)
|
notifications.append(contentsOf: newNotifications)
|
||||||
consolidatedNotifications.insert(contentsOf: newNotifications.consolidated(), at: 0)
|
consolidatedNotifications.insert(contentsOf: newNotifications.consolidated(), at: 0)
|
||||||
}
|
}
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
@ -98,7 +98,7 @@ class NotificationsViewModel: ObservableObject {
|
||||||
maxId: lastId,
|
maxId: lastId,
|
||||||
types: queryTypes))
|
types: queryTypes))
|
||||||
consolidatedNotifications.append(contentsOf: newNotifications.consolidated())
|
consolidatedNotifications.append(contentsOf: newNotifications.consolidated())
|
||||||
self.notifications.append(contentsOf: newNotifications)
|
notifications.append(contentsOf: newNotifications)
|
||||||
state = .display(notifications: consolidatedNotifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage)
|
state = .display(notifications: consolidatedNotifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
state = .error(error: error)
|
||||||
|
|
|
@ -9,7 +9,7 @@ struct StatusEditorMediaView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@ObservedObject var viewModel: StatusEditorViewModel
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
@State private var editingContainer: StatusEditorMediaContainer?
|
@State private var editingContainer: StatusEditorMediaContainer?
|
||||||
|
|
||||||
@State private var isErrorDisplayed: Bool = false
|
@State private var isErrorDisplayed: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -142,7 +142,7 @@ struct StatusEditorMediaView: View {
|
||||||
Label("action.view.error", systemImage: "exclamationmark.triangle")
|
Label("action.view.error", systemImage: "exclamationmark.triangle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
||||||
|
@ -151,7 +151,7 @@ struct StatusEditorMediaView: View {
|
||||||
Label("action.delete", systemImage: "trash")
|
Label("action.delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeErrorView(error: ServerError) -> some View {
|
private func makeErrorView(error: ServerError) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
placeholderView
|
placeholderView
|
||||||
|
@ -159,11 +159,10 @@ struct StatusEditorMediaView: View {
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
.alert("alert.error", isPresented: $isErrorDisplayed) {
|
.alert("alert.error", isPresented: $isErrorDisplayed) {
|
||||||
Button("Ok", action: { })
|
Button("Ok", action: {})
|
||||||
} message: {
|
} message: {
|
||||||
Text(error.error ?? "")
|
Text(error.error ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var altMarker: some View {
|
private var altMarker: some View {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
|
@ -74,7 +74,7 @@ struct MovieFileTranseferable: Transferable {
|
||||||
private let url: URL
|
private let url: URL
|
||||||
var compressedVideoURL: URL? {
|
var compressedVideoURL: URL? {
|
||||||
get async {
|
get async {
|
||||||
return await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
let urlAsset = AVURLAsset(url: url, options: nil)
|
let urlAsset = AVURLAsset(url: url, options: nil)
|
||||||
guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else {
|
guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else {
|
||||||
continuation.resume(returning: nil)
|
continuation.resume(returning: nil)
|
||||||
|
@ -84,7 +84,7 @@ struct MovieFileTranseferable: Transferable {
|
||||||
exportSession.outputURL = outputURL
|
exportSession.outputURL = outputURL
|
||||||
exportSession.outputFileType = .mp4
|
exportSession.outputFileType = .mp4
|
||||||
exportSession.shouldOptimizeForNetworkUse = true
|
exportSession.shouldOptimizeForNetworkUse = true
|
||||||
exportSession.exportAsynchronously { () -> Void in
|
exportSession.exportAsynchronously { () in
|
||||||
continuation.resume(returning: outputURL)
|
continuation.resume(returning: outputURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
inflateSelectedMedias()
|
inflateSelectedMedias()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var isMediasLoading: Bool = false
|
@Published var isMediasLoading: Bool = false
|
||||||
|
|
||||||
@Published var mediasImages: [StatusEditorMediaContainer] = []
|
@Published var mediasImages: [StatusEditorMediaContainer] = []
|
||||||
|
|
|
@ -22,7 +22,7 @@ public struct StatusRowView: View {
|
||||||
var contextMenu: some View {
|
var contextMenu: some View {
|
||||||
StatusRowContextMenu(viewModel: viewModel)
|
StatusRowContextMenu(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if viewModel.isFiltered, let filter = viewModel.filter {
|
if viewModel.isFiltered, let filter = viewModel.filter {
|
||||||
switch filter.filter.filterAction {
|
switch filter.filter.filterAction {
|
||||||
|
@ -225,13 +225,12 @@ public struct StatusRowView: View {
|
||||||
private func makeStatusContentView(status: AnyStatus) -> some View {
|
private func makeStatusContentView(status: AnyStatus) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if !status.spoilerText.asRawText.isEmpty {
|
if !status.spoilerText.asRawText.isEmpty {
|
||||||
|
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("⚠︎")
|
Text("⚠︎")
|
||||||
.font(.system(.subheadline , weight:.bold))
|
.font(.system(.subheadline, weight: .bold))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
EmojiTextApp(status.spoilerText, emojis: status.emojis, language: status.language)
|
EmojiTextApp(status.spoilerText, emojis: status.emojis, language: status.language)
|
||||||
.font(.system(.subheadline , weight:.bold))
|
.font(.system(.subheadline, weight: .bold))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -247,7 +246,7 @@ public struct StatusRowView: View {
|
||||||
.accessibility(label: viewModel.displaySpoiler ? Text("status.show-more") : Text("status.show-less"))
|
.accessibility(label: viewModel.displaySpoiler ? Text("status.show-more") : Text("status.show-less"))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
.onTapGesture { // make whole row tapable to make up for smaller button size
|
.onTapGesture { // make whole row tapable to make up for smaller button size
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.displaySpoiler.toggle()
|
viewModel.displaySpoiler.toggle()
|
||||||
}
|
}
|
||||||
|
@ -376,7 +375,8 @@ public struct StatusRowView: View {
|
||||||
!viewModel.isCompact,
|
!viewModel.isCompact,
|
||||||
theme.statusDisplayStyle == .large,
|
theme.statusDisplayStyle == .large,
|
||||||
status.content.statusesURLs.isEmpty,
|
status.content.statusesURLs.isEmpty,
|
||||||
status.mediaAttachments.isEmpty {
|
status.mediaAttachments.isEmpty
|
||||||
|
{
|
||||||
StatusCardView(card: card)
|
StatusCardView(card: card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue