mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-27 18:51:01 +00:00
Swiftformat
This commit is contained in:
parent
96344e2815
commit
7f6419ebae
161 changed files with 1777 additions and 1746 deletions
1
.swiftformat
Normal file
1
.swiftformat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--indent 2
|
|
@ -644,7 +644,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 730;
|
||||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
||||||
|
@ -674,7 +674,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 730;
|
||||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Lists
|
||||||
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
import Account
|
|
||||||
import Env
|
|
||||||
import Status
|
|
||||||
import DesignSystem
|
|
||||||
import Lists
|
|
||||||
import AppAccount
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
extension View {
|
||||||
func withAppRouteur() -> some View {
|
func withAppRouteur() -> some View {
|
||||||
self.navigationDestination(for: RouteurDestinations.self) { destination in
|
navigationDestination(for: RouteurDestinations.self) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
case let .accountDetail(id):
|
case let .accountDetail(id):
|
||||||
AccountDetailView(accountId: id)
|
AccountDetailView(accountId: id)
|
||||||
|
@ -35,9 +35,9 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
|
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
|
||||||
self.sheet(item: sheetDestinations) { destination in
|
sheet(item: sheetDestinations) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
case let .replyToStatusEditor(status):
|
case let .replyToStatusEditor(status):
|
||||||
StatusEditorView(mode: .replyTo(status: status))
|
StatusEditorView(mode: .replyTo(status: status))
|
||||||
|
@ -69,10 +69,9 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withEnvironments() -> some View {
|
func withEnvironments() -> some View {
|
||||||
self
|
environmentObject(CurrentAccount.shared)
|
||||||
.environmentObject(CurrentAccount.shared)
|
|
||||||
.environmentObject(UserPreferences.shared)
|
.environmentObject(UserPreferences.shared)
|
||||||
.environmentObject(CurrentInstance.shared)
|
.environmentObject(CurrentInstance.shared)
|
||||||
.environmentObject(Theme.shared)
|
.environmentObject(Theme.shared)
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import SwiftUI
|
|
||||||
import AVFoundation
|
|
||||||
import Timeline
|
|
||||||
import Network
|
|
||||||
import KeychainSwift
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
|
||||||
import RevenueCat
|
|
||||||
import AppAccount
|
|
||||||
import Account
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import AVFoundation
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import KeychainSwift
|
||||||
|
import Network
|
||||||
|
import RevenueCat
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct IceCubesApp: App {
|
struct IceCubesApp: App {
|
||||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
||||||
@StateObject private var currentInstance = CurrentInstance.shared
|
@StateObject private var currentInstance = CurrentInstance.shared
|
||||||
|
@ -21,38 +21,38 @@ struct IceCubesApp: App {
|
||||||
@StateObject private var watcher = StreamWatcher()
|
@StateObject private var watcher = StreamWatcher()
|
||||||
@StateObject private var quickLook = QuickLook()
|
@StateObject private var quickLook = QuickLook()
|
||||||
@StateObject private var theme = Theme.shared
|
@StateObject private var theme = Theme.shared
|
||||||
|
|
||||||
@State private var selectedTab: Tab = .timeline
|
@State private var selectedTab: Tab = .timeline
|
||||||
@State private var selectSidebarItem: Tab? = .timeline
|
@State private var selectSidebarItem: Tab? = .timeline
|
||||||
@State private var popToRootTab: Tab = .other
|
@State private var popToRootTab: Tab = .other
|
||||||
@State private var sideBarLoadedTabs: [Tab] = []
|
@State private var sideBarLoadedTabs: [Tab] = []
|
||||||
|
|
||||||
private var availableTabs: [Tab] {
|
private var availableTabs: [Tab] {
|
||||||
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
appView
|
appView
|
||||||
.applyTheme(theme)
|
.applyTheme(theme)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
||||||
setupRevenueCat()
|
setupRevenueCat()
|
||||||
refreshPushSubs()
|
refreshPushSubs()
|
||||||
}
|
}
|
||||||
.environmentObject(appAccountsManager)
|
.environmentObject(appAccountsManager)
|
||||||
.environmentObject(appAccountsManager.currentClient)
|
.environmentObject(appAccountsManager.currentClient)
|
||||||
.environmentObject(quickLook)
|
.environmentObject(quickLook)
|
||||||
.environmentObject(currentAccount)
|
.environmentObject(currentAccount)
|
||||||
.environmentObject(currentInstance)
|
.environmentObject(currentInstance)
|
||||||
.environmentObject(userPreferences)
|
.environmentObject(userPreferences)
|
||||||
.environmentObject(theme)
|
.environmentObject(theme)
|
||||||
.environmentObject(watcher)
|
.environmentObject(watcher)
|
||||||
.environmentObject(PushNotificationsService.shared)
|
.environmentObject(PushNotificationsService.shared)
|
||||||
.sheet(item: $quickLook.url, content: { url in
|
.sheet(item: $quickLook.url, content: { url in
|
||||||
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
|
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { scenePhase in
|
.onChange(of: scenePhase) { scenePhase in
|
||||||
handleScenePhase(scenePhase: scenePhase)
|
handleScenePhase(scenePhase: scenePhase)
|
||||||
|
@ -64,7 +64,7 @@ struct IceCubesApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var appView: some View {
|
private var appView: some View {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
@ -73,14 +73,14 @@ struct IceCubesApp: App {
|
||||||
tabBarView
|
tabBarView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func badgeFor(tab: Tab) -> Int {
|
private func badgeFor(tab: Tab) -> Int {
|
||||||
if tab == .notifications && selectedTab != tab {
|
if tab == .notifications && selectedTab != tab {
|
||||||
return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount
|
return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sidebarView: some View {
|
private var sidebarView: some View {
|
||||||
SideBarView(selectedTab: $selectedTab,
|
SideBarView(selectedTab: $selectedTab,
|
||||||
popToRootTab: $popToRootTab,
|
popToRootTab: $popToRootTab,
|
||||||
|
@ -107,7 +107,7 @@ struct IceCubesApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tabBarView: some View {
|
private var tabBarView: some View {
|
||||||
TabView(selection: .init(get: {
|
TabView(selection: .init(get: {
|
||||||
selectedTab
|
selectedTab
|
||||||
|
@ -132,14 +132,14 @@ struct IceCubesApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setNewClientsInEnv(client: Client) {
|
private func setNewClientsInEnv(client: Client) {
|
||||||
currentAccount.setClient(client: client)
|
currentAccount.setClient(client: client)
|
||||||
currentInstance.setClient(client: client)
|
currentInstance.setClient(client: client)
|
||||||
userPreferences.setClient(client: client)
|
userPreferences.setClient(client: client)
|
||||||
watcher.setClient(client: client)
|
watcher.setClient(client: client)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleScenePhase(scenePhase: ScenePhase) {
|
private func handleScenePhase(scenePhase: ScenePhase) {
|
||||||
switch scenePhase {
|
switch scenePhase {
|
||||||
case .background:
|
case .background:
|
||||||
|
@ -156,33 +156,34 @@ struct IceCubesApp: App {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupRevenueCat() {
|
private func setupRevenueCat() {
|
||||||
Purchases.logLevel = .error
|
Purchases.logLevel = .error
|
||||||
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshPushSubs() {
|
private func refreshPushSubs() {
|
||||||
PushNotificationsService.shared.requestPushNotifications()
|
PushNotificationsService.shared.requestPushNotifications()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
func application(_ application: UIApplication,
|
func application(_: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||||
|
{
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication,
|
func application(_: UIApplication,
|
||||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
|
{
|
||||||
PushNotificationsService.shared.pushToken = deviceToken
|
PushNotificationsService.shared.pushToken = deviceToken
|
||||||
Task {
|
Task {
|
||||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
|
||||||
import QuickLook
|
import QuickLook
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
extension URL: Identifiable {
|
extension URL: Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
|
@ -11,7 +11,7 @@ extension URL: Identifiable {
|
||||||
struct QuickLookPreview: UIViewControllerRepresentable {
|
struct QuickLookPreview: UIViewControllerRepresentable {
|
||||||
let selectedURL: URL
|
let selectedURL: URL
|
||||||
let urls: [URL]
|
let urls: [URL]
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UINavigationController {
|
func makeUIViewController(context: Context) -> UINavigationController {
|
||||||
let controller = AppQLPreviewCpntroller()
|
let controller = AppQLPreviewCpntroller()
|
||||||
controller.dataSource = context.coordinator
|
controller.dataSource = context.coordinator
|
||||||
|
@ -19,30 +19,31 @@ struct QuickLookPreview: UIViewControllerRepresentable {
|
||||||
let nav = UINavigationController(rootViewController: controller)
|
let nav = UINavigationController(rootViewController: controller)
|
||||||
return nav
|
return nav
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIViewController(
|
func updateUIViewController(
|
||||||
_ uiViewController: UINavigationController, context: Context) {}
|
_: UINavigationController, context _: Context
|
||||||
|
) {}
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
return Coordinator(parent: self)
|
return Coordinator(parent: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
|
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
|
||||||
let parent: QuickLookPreview
|
let parent: QuickLookPreview
|
||||||
|
|
||||||
init(parent: QuickLookPreview) {
|
init(parent: QuickLookPreview) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
func numberOfPreviewItems(in _: QLPreviewController) -> Int {
|
||||||
return parent.urls.count
|
return parent.urls.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||||
return parent.urls[index] as QLPreviewItem
|
return parent.urls[index] as QLPreviewItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
|
func previewController(_: QLPreviewController, editingModeFor _: QLPreviewItem) -> QLPreviewItemEditingMode {
|
||||||
.createCopy
|
.createCopy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,22 +53,22 @@ class AppQLPreviewCpntroller: QLPreviewController {
|
||||||
private var closeButton: UIBarButtonItem {
|
private var closeButton: UIBarButtonItem {
|
||||||
.init(title: "Done", style: .plain, target: self, action: #selector(onCloseButton))
|
.init(title: "Done", style: .plain, target: self, action: #selector(onCloseButton))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
navigationItem.rightBarButtonItem = closeButton
|
navigationItem.rightBarButtonItem = closeButton
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
navigationItem.rightBarButtonItem = closeButton
|
navigationItem.rightBarButtonItem = closeButton
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
navigationItem.rightBarButtonItem = closeButton
|
navigationItem.rightBarButtonItem = closeButton
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func onCloseButton() {
|
@objc private func onCloseButton() {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +1,85 @@
|
||||||
import SwiftUI
|
|
||||||
import SafariServices
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SafariServices
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func withSafariRouteur() -> some View {
|
func withSafariRouteur() -> some View {
|
||||||
modifier(SafariRouteur())
|
modifier(SafariRouteur())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SafariRouteur: ViewModifier {
|
private struct SafariRouteur: ViewModifier {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
@State private var safari: SFSafariViewController?
|
@State private var safari: SFSafariViewController?
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
routeurPath.handle(url: url)
|
routeurPath.handle(url: url)
|
||||||
})
|
})
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routeurPath.urlHandler = { url in
|
routeurPath.urlHandler = { url in
|
||||||
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||||
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
||||||
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
||||||
return .systemAction
|
return .systemAction
|
||||||
}
|
}
|
||||||
|
|
||||||
let safari = SFSafariViewController(url: url)
|
let safari = SFSafariViewController(url: url)
|
||||||
safari.preferredBarTintColor = UIColor(theme.primaryBackgroundColor)
|
safari.preferredBarTintColor = UIColor(theme.primaryBackgroundColor)
|
||||||
safari.preferredControlTintColor = UIColor(theme.tintColor)
|
safari.preferredControlTintColor = UIColor(theme.tintColor)
|
||||||
|
|
||||||
self.safari = safari
|
self.safari = safari
|
||||||
return .handled
|
return .handled
|
||||||
}
|
|
||||||
}
|
|
||||||
.background {
|
|
||||||
SafariPresenter(safari: safari)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SafariPresenter: UIViewRepresentable {
|
|
||||||
var safari: SFSafariViewController?
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
|
||||||
let view = UIView(frame: .zero)
|
|
||||||
view.isHidden = true
|
|
||||||
view.isUserInteractionEnabled = false
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIView, context: Context) {
|
|
||||||
guard let safari = safari, let viewController = uiView.findTopViewController() else { return }
|
|
||||||
viewController.present(safari, animated: true)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
SafariPresenter(safari: safari)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SafariPresenter: UIViewRepresentable {
|
||||||
|
var safari: SFSafariViewController?
|
||||||
|
|
||||||
|
func makeUIView(context _: Context) -> UIView {
|
||||||
|
let view = UIView(frame: .zero)
|
||||||
|
view.isHidden = true
|
||||||
|
view.isUserInteractionEnabled = false
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIView, context _: Context) {
|
||||||
|
guard let safari = safari, let viewController = uiView.findTopViewController() else { return }
|
||||||
|
viewController.present(safari, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UIView {
|
private extension UIView {
|
||||||
func findTopViewController() -> UIViewController? {
|
func findTopViewController() -> UIViewController? {
|
||||||
if let nextResponder = self.next as? UIViewController {
|
if let nextResponder = next as? UIViewController {
|
||||||
return nextResponder.topViewController()
|
return nextResponder.topViewController()
|
||||||
} else if let nextResponder = self.next as? UIView {
|
} else if let nextResponder = next as? UIView {
|
||||||
return nextResponder.findTopViewController()
|
return nextResponder.findTopViewController()
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UIViewController {
|
private extension UIViewController {
|
||||||
func topViewController() -> UIViewController? {
|
func topViewController() -> UIViewController? {
|
||||||
if let nvc = self as? UINavigationController {
|
if let nvc = self as? UINavigationController {
|
||||||
return nvc.visibleViewController?.topViewController()
|
return nvc.visibleViewController?.topViewController()
|
||||||
} else if let tbc = self as? UITabBarController, let selected = tbc.selectedViewController {
|
} else if let tbc = self as? UITabBarController, let selected = tbc.selectedViewController {
|
||||||
return selected.topViewController()
|
return selected.topViewController()
|
||||||
} else if let presented = self.presentedViewController {
|
} else if let presented = presentedViewController {
|
||||||
return presented.topViewController()
|
return presented.topViewController()
|
||||||
}
|
|
||||||
return self
|
|
||||||
}
|
}
|
||||||
|
return self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import Account
|
import Account
|
||||||
import DesignSystem
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct SideBarView<Content: View>: View {
|
struct SideBarView<Content: View>: View {
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@Binding var selectedTab: Tab
|
@Binding var selectedTab: Tab
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
var tabs: [Tab]
|
var tabs: [Tab]
|
||||||
@ViewBuilder var content: () -> Content
|
@ViewBuilder var content: () -> Content
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import Models
|
|
||||||
import Shimmer
|
|
||||||
import Explore
|
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import Env
|
||||||
|
import Explore
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ExploreTab: View {
|
struct ExploreTab: View {
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
|
@ -13,7 +12,7 @@ struct ExploreTab: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
ExploreView()
|
ExploreView()
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import Account
|
import Account
|
||||||
import Models
|
import AppAccount
|
||||||
import Shimmer
|
|
||||||
import Conversations
|
import Conversations
|
||||||
import Env
|
import Env
|
||||||
import AppAccount
|
import Models
|
||||||
|
import Network
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct MessagesTab: View {
|
struct MessagesTab: View {
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
|
@ -14,7 +13,7 @@ struct MessagesTab: View {
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
ConversationsListView()
|
ConversationsListView()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import SwiftUI
|
import AppAccount
|
||||||
import Timeline
|
|
||||||
import Env
|
import Env
|
||||||
import Network
|
import Network
|
||||||
import Notifications
|
import Notifications
|
||||||
import AppAccount
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
|
||||||
struct NotificationsTab: View {
|
struct NotificationsTab: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@ -12,7 +12,7 @@ struct NotificationsTab: View {
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@EnvironmentObject private var userPreferences: UserPreferences
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
NotificationsListView()
|
NotificationsListView()
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
|
||||||
import NukeUI
|
|
||||||
import Shimmer
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import Combine
|
import Combine
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import NukeUI
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct AddAccountView: View {
|
struct AddAccountView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@State private var instanceName: String = ""
|
@State private var instanceName: String = ""
|
||||||
@State private var instance: Instance?
|
@State private var instance: Instance?
|
||||||
@State private var isSigninIn = false
|
@State private var isSigninIn = false
|
||||||
|
@ -26,9 +26,9 @@ struct AddAccountView: View {
|
||||||
@State private var instanceFetchError: String?
|
@State private var instanceFetchError: String?
|
||||||
|
|
||||||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
@ -102,7 +102,7 @@ struct AddAccountView: View {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var signInSection: some View {
|
private var signInSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
|
@ -127,13 +127,13 @@ struct AddAccountView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.tintColor)
|
.listRowBackground(theme.tintColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var instancesListView: some View {
|
private var instancesListView: some View {
|
||||||
Section("Suggestions") {
|
Section("Suggestions") {
|
||||||
if instances.isEmpty {
|
if instances.isEmpty {
|
||||||
placeholderRow
|
placeholderRow
|
||||||
} else {
|
} else {
|
||||||
ForEach(instanceName.isEmpty ? instances : instances.filter{ $0.name.contains(instanceName.lowercased()) }) { instance in
|
ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||||
Button {
|
Button {
|
||||||
self.instanceName = instance.name
|
self.instanceName = instance.name
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -154,7 +154,7 @@ struct AddAccountView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var placeholderRow: some View {
|
private var placeholderRow: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Loading...")
|
Text("Loading...")
|
||||||
|
@ -171,7 +171,7 @@ struct AddAccountView: View {
|
||||||
.shimmering()
|
.shimmering()
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func signIn() async {
|
private func signIn() async {
|
||||||
do {
|
do {
|
||||||
signInClient = .init(server: instanceName)
|
signInClient = .init(server: instanceName)
|
||||||
|
@ -184,7 +184,7 @@ struct AddAccountView: View {
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func continueSignIn(url: URL) async {
|
private func continueSignIn(url: URL) async {
|
||||||
guard let client = signInClient else {
|
guard let client = signInClient else {
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Models
|
||||||
import Status
|
import Status
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct DisplaySettingsView: View {
|
struct DisplaySettingsView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@State private var isThemeSelectorPresented = false
|
@State private var isThemeSelectorPresented = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section("Theme") {
|
Section("Theme") {
|
||||||
|
@ -17,7 +17,7 @@ struct DisplaySettingsView: View {
|
||||||
ColorPicker("Secondary Background color", selection: $theme.secondaryBackgroundColor)
|
ColorPicker("Secondary Background color", selection: $theme.secondaryBackgroundColor)
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section("Display") {
|
Section("Display") {
|
||||||
Picker("Avatar position", selection: $theme.avatarPosition) {
|
Picker("Avatar position", selection: $theme.avatarPosition) {
|
||||||
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
|
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
|
||||||
|
@ -34,7 +34,7 @@ struct DisplaySettingsView: View {
|
||||||
Text(buttonStyle.description).tag(buttonStyle)
|
Text(buttonStyle.description).tag(buttonStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker("Status media style", selection: $theme.statusDisplayStyle) {
|
Picker("Status media style", selection: $theme.statusDisplayStyle) {
|
||||||
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
|
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
|
||||||
Text(buttonStyle.description).tag(buttonStyle)
|
Text(buttonStyle.description).tag(buttonStyle)
|
||||||
|
@ -42,7 +42,7 @@ struct DisplaySettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
theme.selectedSet = .iceCubeDark
|
theme.selectedSet = .iceCubeDark
|
||||||
|
@ -59,7 +59,7 @@ struct DisplaySettingsView: View {
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var themeSelectorButton: some View {
|
private var themeSelectorButton: some View {
|
||||||
NavigationLink(destination: ThemePreviewView()) {
|
NavigationLink(destination: ThemePreviewView()) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import SwiftUI
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct IconSelectorView: View {
|
struct IconSelectorView: View {
|
||||||
enum Icon: Int, CaseIterable, Identifiable {
|
enum Icon: Int, CaseIterable, Identifiable {
|
||||||
var id: String {
|
var id: String {
|
||||||
"\(rawValue)"
|
"\(rawValue)"
|
||||||
}
|
}
|
||||||
|
|
||||||
init(string: String) {
|
init(string: String) {
|
||||||
if string == Icon.primary.appIconName {
|
if string == Icon.primary.appIconName {
|
||||||
self = .primary
|
self = .primary
|
||||||
|
@ -14,12 +14,12 @@ struct IconSelectorView: View {
|
||||||
self = .init(rawValue: Int(String(string.last!))!)!
|
self = .init(rawValue: Int(String(string.last!))!)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case primary = 0
|
case primary = 0
|
||||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8
|
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8
|
||||||
case alt9, alt10, alt11, alt12, alt13, alt14
|
case alt9, alt10, alt11, alt12, alt13, alt14
|
||||||
case alt15, alt16, alt17, alt18, alt19
|
case alt15, alt16, alt17, alt18, alt19
|
||||||
|
|
||||||
var appIconName: String {
|
var appIconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .primary:
|
case .primary:
|
||||||
|
@ -28,17 +28,17 @@ struct IconSelectorView: View {
|
||||||
return "AppIconAlternate\(rawValue)"
|
return "AppIconAlternate\(rawValue)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var iconName: String {
|
var iconName: String {
|
||||||
"icon\(rawValue)"
|
"icon\(rawValue)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
@State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
||||||
|
|
||||||
private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))]
|
private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Models
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct InstanceInfoView: View {
|
struct InstanceInfoView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
InstanceInfoSection(instance: instance)
|
InstanceInfoSection(instance: instance)
|
||||||
|
@ -20,9 +20,9 @@ struct InstanceInfoView: View {
|
||||||
|
|
||||||
public struct InstanceInfoSection: View {
|
public struct InstanceInfoSection: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Section("Instance info") {
|
Section("Instance info") {
|
||||||
LabeledContent("Name", value: instance.title)
|
LabeledContent("Name", value: instance.title)
|
||||||
|
@ -34,7 +34,7 @@ public struct InstanceInfoSection: View {
|
||||||
LabeledContent("Domains", value: "\(instance.stats.domainCount)")
|
LabeledContent("Domains", value: "\(instance.stats.domainCount)")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section("Instance rules") {
|
Section("Instance rules") {
|
||||||
ForEach(instance.rules) { rule in
|
ForEach(instance.rules) { rule in
|
||||||
Text(rule.text)
|
Text(rule.text)
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
|
||||||
import NukeUI
|
|
||||||
import Network
|
|
||||||
import UserNotifications
|
|
||||||
import Env
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
struct PushNotificationsView: View {
|
struct PushNotificationsView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||||
|
|
||||||
@State private var subscriptions: [PushSubscription] = []
|
@State private var subscriptions: [PushSubscription] = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -24,7 +24,7 @@ struct PushNotificationsView: View {
|
||||||
Text("Receive push notifications on new activities")
|
Text("Receive push notifications on new activities")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
if pushNotifications.isPushEnabled {
|
if pushNotifications.isPushEnabled {
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
||||||
|
@ -87,7 +87,7 @@ struct PushNotificationsView: View {
|
||||||
updateSubscriptions()
|
updateSubscriptions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSubscriptions() {
|
private func updateSubscriptions() {
|
||||||
Task {
|
Task {
|
||||||
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import Account
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
|
||||||
import AppAccount
|
|
||||||
|
|
||||||
struct SettingsTabs: View {
|
struct SettingsTabs: View {
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||||
|
@ -14,13 +14,13 @@ struct SettingsTabs: View {
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
|
|
||||||
@State private var addAccountSheetPresented = false
|
@State private var addAccountSheetPresented = false
|
||||||
|
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
Form {
|
Form {
|
||||||
|
@ -52,7 +52,7 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
Section("Accounts") {
|
Section("Accounts") {
|
||||||
ForEach(appAccountsManager.availableAccounts) { account in
|
ForEach(appAccountsManager.availableAccounts) { account in
|
||||||
|
@ -73,7 +73,7 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var generalSection: some View {
|
private var generalSection: some View {
|
||||||
Section("General") {
|
Section("General") {
|
||||||
|
@ -106,7 +106,7 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var appSection: some View {
|
private var appSection: some View {
|
||||||
Section("App") {
|
Section("App") {
|
||||||
NavigationLink(destination: IconSelectorView()) {
|
NavigationLink(destination: IconSelectorView()) {
|
||||||
|
@ -121,19 +121,19 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
|
||||||
Label("Source (GitHub link)", systemImage: "link")
|
Label("Source (GitHub link)", systemImage: "link")
|
||||||
}
|
}
|
||||||
.tint(theme.labelColor)
|
.tint(theme.labelColor)
|
||||||
|
|
||||||
NavigationLink(destination: SupportAppView()) {
|
NavigationLink(destination: SupportAppView()) {
|
||||||
Label("Support the app", systemImage: "wand.and.stars")
|
Label("Support the app", systemImage: "wand.and.stars")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addAccountButton: some View {
|
private var addAccountButton: some View {
|
||||||
Button {
|
Button {
|
||||||
addAccountSheetPresented.toggle()
|
addAccountSheetPresented.toggle()
|
||||||
|
@ -144,7 +144,7 @@ struct SettingsTabs: View {
|
||||||
AddAccountView()
|
AddAccountView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var remoteLocalTimelinesView: some View {
|
private var remoteLocalTimelinesView: some View {
|
||||||
Form {
|
Form {
|
||||||
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import RevenueCat
|
import RevenueCat
|
||||||
import Shimmer
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct SupportAppView: View {
|
struct SupportAppView: View {
|
||||||
enum Tips: String, CaseIterable {
|
enum Tips: String, CaseIterable {
|
||||||
case one, two, three
|
case one, two, three
|
||||||
|
|
||||||
init(productId: String) {
|
init(productId: String) {
|
||||||
self = .init(rawValue: String(productId.split(separator: ".")[2]))!
|
self = .init(rawValue: String(productId.split(separator: ".")[2]))!
|
||||||
}
|
}
|
||||||
|
|
||||||
var productId: String {
|
var productId: String {
|
||||||
"icecubes.tipjar.\(rawValue)"
|
"icecubes.tipjar.\(rawValue)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .one:
|
case .one:
|
||||||
|
@ -26,7 +26,7 @@ struct SupportAppView: View {
|
||||||
return "🤯 Generous Tip"
|
return "🤯 Generous Tip"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitle: String {
|
var subtitle: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .one:
|
case .one:
|
||||||
|
@ -38,15 +38,15 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@State private var loadingProducts: Bool = false
|
@State private var loadingProducts: Bool = false
|
||||||
@State private var products: [StoreProduct] = []
|
@State private var products: [StoreProduct] = []
|
||||||
@State private var isProcessingPurchase: Bool = false
|
@State private var isProcessingPurchase: Bool = false
|
||||||
@State private var purchaseSuccessDisplayed: Bool = false
|
@State private var purchaseSuccessDisplayed: Bool = false
|
||||||
@State private var purchaseErrorDisplayed: Bool = false
|
@State private var purchaseErrorDisplayed: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -65,7 +65,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if loadingProducts {
|
if loadingProducts {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -133,7 +133,7 @@ struct SupportAppView: View {
|
||||||
})
|
})
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadingProducts = true
|
loadingProducts = true
|
||||||
Purchases.shared.getProducts(Tips.allCases.map{ $0.productId }) { products in
|
Purchases.shared.getProducts(Tips.allCases.map { $0.productId }) { products in
|
||||||
self.products = products.sorted(by: { $0.price < $1.price })
|
self.products = products.sorted(by: { $0.price < $1.price })
|
||||||
withAnimation {
|
withAnimation {
|
||||||
loadingProducts = false
|
loadingProducts = false
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import Foundation
|
|
||||||
import Status
|
|
||||||
import Account
|
import Account
|
||||||
import Explore
|
import Explore
|
||||||
|
import Foundation
|
||||||
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum Tab: Int, Identifiable, Hashable {
|
enum Tab: Int, Identifiable, Hashable {
|
||||||
case timeline, notifications, explore, messages, settings, other
|
case timeline, notifications, explore, messages, settings, other
|
||||||
case trending, federated, local
|
case trending, federated, local
|
||||||
case profile
|
case profile
|
||||||
|
|
||||||
var id: Int {
|
var id: Int {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loggedOutTab() -> [Tab] {
|
static func loggedOutTab() -> [Tab] {
|
||||||
[.timeline, .settings]
|
[.timeline, .settings]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loggedInTabs() -> [Tab] {
|
static func loggedInTabs() -> [Tab] {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
return [.timeline, .trending, .federated, .local, .notifications, .explore, .messages, .settings]
|
return [.timeline, .trending, .federated, .local, .notifications, .explore, .messages, .settings]
|
||||||
|
@ -24,7 +24,7 @@ enum Tab: Int, Identifiable, Hashable {
|
||||||
return [.timeline, .notifications, .explore, .messages, .settings]
|
return [.timeline, .notifications, .explore, .messages, .settings]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeContentView(popToRootTab: Binding<Tab>) -> some View {
|
func makeContentView(popToRootTab: Binding<Tab>) -> some View {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -48,7 +48,7 @@ enum Tab: Int, Identifiable, Hashable {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var label: some View {
|
var label: some View {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -72,7 +72,7 @@ enum Tab: Int, Identifiable, Hashable {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var iconName: String {
|
var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .timeline:
|
case .timeline:
|
||||||
|
@ -96,4 +96,3 @@ enum Tab: Int, Identifiable, Hashable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import SwiftUI
|
import Combine
|
||||||
import Network
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import Combine
|
import SwiftUI
|
||||||
|
|
||||||
struct AddRemoteTimelineView: View {
|
struct AddRemoteTimelineView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@State private var instanceName: String = ""
|
@State private var instanceName: String = ""
|
||||||
@State private var instance: Instance?
|
@State private var instance: Instance?
|
||||||
@State private var instances: [InstanceSocial] = []
|
@State private var instances: [InstanceSocial] = []
|
||||||
|
|
||||||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
@ -44,7 +44,7 @@ struct AddRemoteTimelineView: View {
|
||||||
Text("Add")
|
Text("Add")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
instancesListView
|
instancesListView
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
|
@ -76,14 +76,14 @@ struct AddRemoteTimelineView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var instancesListView: some View {
|
private var instancesListView: some View {
|
||||||
Section("Suggestions") {
|
Section("Suggestions") {
|
||||||
if instances.isEmpty {
|
if instances.isEmpty {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
} else {
|
} else {
|
||||||
ForEach(instanceName.isEmpty ? instances : instances.filter{ $0.name.contains(instanceName.lowercased()) }) { instance in
|
ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||||
Button {
|
Button {
|
||||||
self.instanceName = instance.name
|
self.instanceName = instance.name
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import SwiftUI
|
import AppAccount
|
||||||
import Timeline
|
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import Combine
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import AppAccount
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
|
||||||
struct TimelineTab: View {
|
struct TimelineTab: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@ -14,19 +14,19 @@ struct TimelineTab: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
@State private var didAppear: Bool = false
|
@State private var didAppear: Bool = false
|
||||||
@State private var timeline: TimelineFilter
|
@State private var timeline: TimelineFilter
|
||||||
@State private var scrollToTopSignal: Int = 0
|
@State private var scrollToTopSignal: Int = 0
|
||||||
|
|
||||||
private let canFilterTimeline: Bool
|
private let canFilterTimeline: Bool
|
||||||
|
|
||||||
init(popToRootTab: Binding<Tab>, timeline: TimelineFilter? = nil) {
|
init(popToRootTab: Binding<Tab>, timeline: TimelineFilter? = nil) {
|
||||||
canFilterTimeline = timeline == nil
|
canFilterTimeline = timeline == nil
|
||||||
self.timeline = timeline ?? .home
|
self.timeline = timeline ?? .home
|
||||||
_popToRootTab = popToRootTab
|
_popToRootTab = popToRootTab
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal)
|
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal)
|
||||||
|
@ -71,8 +71,7 @@ struct TimelineTab: View {
|
||||||
.withSafariRouteur()
|
.withSafariRouteur()
|
||||||
.environmentObject(routeurPath)
|
.environmentObject(routeurPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var timelineFilterButton: some View {
|
private var timelineFilterButton: some View {
|
||||||
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
||||||
|
@ -93,7 +92,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !currentAccount.tags.isEmpty {
|
if !currentAccount.tags.isEmpty {
|
||||||
Menu("Followed Tags") {
|
Menu("Followed Tags") {
|
||||||
ForEach(currentAccount.tags) { tag in
|
ForEach(currentAccount.tags) { tag in
|
||||||
|
@ -105,7 +104,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu("Local Timelines") {
|
Menu("Local Timelines") {
|
||||||
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
||||||
Button {
|
Button {
|
||||||
|
@ -121,7 +120,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addAccountButton: some View {
|
private var addAccountButton: some View {
|
||||||
Button {
|
Button {
|
||||||
routeurPath.presentedSheet = .addAccount
|
routeurPath.presentedSheet = .addAccount
|
||||||
|
@ -129,7 +128,7 @@ struct TimelineTab: View {
|
||||||
Image(systemName: "person.badge.plus")
|
Image(systemName: "person.badge.plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbarView: some ToolbarContent {
|
private var toolbarView: some ToolbarContent {
|
||||||
if canFilterTimeline {
|
if canFilterTimeline {
|
||||||
|
|
|
@ -1,71 +1,75 @@
|
||||||
import UserNotifications
|
|
||||||
import KeychainSwift
|
|
||||||
import Env
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Env
|
||||||
|
import KeychainSwift
|
||||||
import Models
|
import Models
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
var bestAttemptContent: UNMutableNotificationContent?
|
var bestAttemptContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||||
|
|
||||||
if let bestAttemptContent {
|
if let bestAttemptContent {
|
||||||
let privateKey = PushNotificationsService.shared.notificationsPrivateKeyAsKey
|
let privateKey = PushNotificationsService.shared.notificationsPrivateKeyAsKey
|
||||||
let auth = PushNotificationsService.shared.notificationsAuthKeyAsKey
|
let auth = PushNotificationsService.shared.notificationsAuthKeyAsKey
|
||||||
|
|
||||||
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
||||||
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else {
|
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64())
|
||||||
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
||||||
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
|
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
|
||||||
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else {
|
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
|
||||||
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
||||||
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else {
|
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64())
|
||||||
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let plaintextData = NotificationService.decrypt(payload: payload,
|
guard let plaintextData = NotificationService.decrypt(payload: payload,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
publicKey: publicKey),
|
publicKey: publicKey),
|
||||||
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else {
|
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData)
|
||||||
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bestAttemptContent.title = notification.title
|
bestAttemptContent.title = notification.title
|
||||||
bestAttemptContent.subtitle = ""
|
bestAttemptContent.subtitle = ""
|
||||||
bestAttemptContent.body = notification.body.escape()
|
bestAttemptContent.body = notification.body.escape()
|
||||||
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
||||||
bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "glass.wav"))
|
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.wav"))
|
||||||
|
|
||||||
let preferences = UserPreferences.shared
|
let preferences = UserPreferences.shared
|
||||||
preferences.pushNotificationsCount += 1
|
preferences.pushNotificationsCount += 1
|
||||||
|
|
||||||
bestAttemptContent.badge = .init(integerLiteral: preferences.pushNotificationsCount)
|
bestAttemptContent.badge = .init(integerLiteral: preferences.pushNotificationsCount)
|
||||||
|
|
||||||
if let urlString = notification.icon,
|
if let urlString = notification.icon,
|
||||||
let url = URL(string: urlString) {
|
let url = URL(string: urlString)
|
||||||
|
{
|
||||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
|
||||||
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
let filename = url.lastPathComponent
|
let filename = url.lastPathComponent
|
||||||
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
|
if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
|
||||||
if let image = UIImage(data: data) {
|
if let image = UIImage(data: data) {
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
extension NotificationService {
|
extension NotificationService {
|
||||||
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
||||||
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
||||||
|
|
||||||
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||||
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
||||||
|
|
||||||
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||||
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||||
|
|
||||||
let nonceData = nonce.withUnsafeBytes(Array.init)
|
let nonceData = nonce.withUnsafeBytes(Array.init)
|
||||||
|
|
||||||
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _plaintext: Data?
|
var _plaintext: Data?
|
||||||
do {
|
do {
|
||||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||||
|
@ -30,20 +30,20 @@ extension NotificationService {
|
||||||
guard let plaintext = _plaintext else {
|
guard let plaintext = _plaintext else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||||
guard plaintext.count >= 2 + paddingLength else {
|
guard plaintext.count >= 2 + paddingLength else {
|
||||||
print("1")
|
print("1")
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||||
|
|
||||||
return Data(unpadded)
|
return Data(unpadded)
|
||||||
}
|
}
|
||||||
|
|
||||||
static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
private static func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
||||||
var info = Data()
|
var info = Data()
|
||||||
|
|
||||||
info.append("Content-Encoding: ".data(using: .utf8)!)
|
info.append("Content-Encoding: ".data(using: .utf8)!)
|
||||||
info.append(type.data(using: .utf8)!)
|
info.append(type.data(using: .utf8)!)
|
||||||
info.append(0)
|
info.append(0)
|
||||||
|
@ -55,32 +55,29 @@ extension NotificationService {
|
||||||
info.append(0)
|
info.append(0)
|
||||||
info.append(65)
|
info.append(65)
|
||||||
info.append(serverPublicKey)
|
info.append(serverPublicKey)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
func escape() -> String {
|
func escape() -> String {
|
||||||
return self
|
return replacingOccurrences(of: "&", with: "&")
|
||||||
.replacingOccurrences(of: "&", with: "&")
|
|
||||||
.replacingOccurrences(of: "<", with: "<")
|
.replacingOccurrences(of: "<", with: "<")
|
||||||
.replacingOccurrences(of: ">", with: ">")
|
.replacingOccurrences(of: ">", with: ">")
|
||||||
.replacingOccurrences(of: """, with: "\"")
|
.replacingOccurrences(of: """, with: "\"")
|
||||||
.replacingOccurrences(of: "'", with: "'")
|
.replacingOccurrences(of: "'", with: "'")
|
||||||
.replacingOccurrences(of: "'", with: "’")
|
.replacingOccurrences(of: "'", with: "’")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func URLSafeBase64ToBase64() -> String {
|
func URLSafeBase64ToBase64() -> String {
|
||||||
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||||
let countMod4 = count % 4
|
let countMod4 = count % 4
|
||||||
|
|
||||||
if countMod4 != 0 {
|
if countMod4 != 0 {
|
||||||
base64.append(String(repeating: "=", count: 4 - countMod4))
|
base64.append(String(repeating: "=", count: 4 - countMod4))
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64
|
return base64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationSupportsText</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
<integer>4</integer>
|
<integer>4</integer>
|
||||||
|
<key>NSExtensionActivationSupportsText</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtensionMainStoryboard</key>
|
<key>NSExtensionMainStoryboard</key>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Network
|
||||||
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import Status
|
|
||||||
import DesignSystem
|
|
||||||
import Account
|
|
||||||
import Network
|
|
||||||
import Env
|
|
||||||
import AppAccount
|
|
||||||
|
|
||||||
class ShareViewController: UIViewController {
|
class ShareViewController: UIViewController {
|
||||||
@IBOutlet var container: UIView!
|
@IBOutlet var container: UIView!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
let appAccountsManager = AppAccountsManager.shared
|
let appAccountsManager = AppAccountsManager.shared
|
||||||
let client = appAccountsManager.currentClient
|
let client = appAccountsManager.currentClient
|
||||||
let account = CurrentAccount.shared
|
let account = CurrentAccount.shared
|
||||||
|
@ -22,7 +22,7 @@ class ShareViewController: UIViewController {
|
||||||
let colorScheme = traitCollection.userInterfaceStyle
|
let colorScheme = traitCollection.userInterfaceStyle
|
||||||
let theme = Theme.shared
|
let theme = Theme.shared
|
||||||
theme.setColor(withName: colorScheme == .dark ? .iceCubeDark : .iceCubeLight)
|
theme.setColor(withName: colorScheme == .dark ? .iceCubeDark : .iceCubeLight)
|
||||||
|
|
||||||
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
|
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
|
||||||
if let attachments = item.attachments {
|
if let attachments = item.attachments {
|
||||||
let view = StatusEditorView(mode: .shareExtension(items: attachments))
|
let view = StatusEditorView(mode: .shareExtension(items: attachments))
|
||||||
|
@ -35,24 +35,24 @@ class ShareViewController: UIViewController {
|
||||||
.tint(theme.tintColor)
|
.tint(theme.tintColor)
|
||||||
.preferredColorScheme(colorScheme == .light ? .light : .dark)
|
.preferredColorScheme(colorScheme == .light ? .light : .dark)
|
||||||
let childView = UIHostingController(rootView: view)
|
let childView = UIHostingController(rootView: view)
|
||||||
self.addChild(childView)
|
addChild(childView)
|
||||||
childView.view.frame = self.container.bounds
|
childView.view.frame = container.bounds
|
||||||
self.container.addSubview(childView.view)
|
container.addSubview(childView.view)
|
||||||
childView.didMove(toParent: self)
|
childView.didMove(toParent: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
|
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: nil) { _ in
|
queue: nil) { _ in
|
||||||
self.close()
|
self.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Account",
|
name: "Account",
|
||||||
targets: ["Account"]),
|
targets: ["Account"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Network", path: "../Network"),
|
.package(name: "Network", path: "../Network"),
|
||||||
|
@ -25,9 +26,11 @@ let package = Package(
|
||||||
.product(name: "Network", package: "Network"),
|
.product(name: "Network", package: "Network"),
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Status", package: "Status"),
|
.product(name: "Status", package: "Status"),
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "AccountTests",
|
name: "AccountTests",
|
||||||
dependencies: ["Account"]),
|
dependencies: ["Account"]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
|
||||||
import Shimmer
|
|
||||||
import NukeUI
|
|
||||||
import EmojiText
|
import EmojiText
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import NukeUI
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct AccountDetailHeaderView: View {
|
struct AccountDetailHeaderView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var quickLook: QuickLook
|
@EnvironmentObject private var quickLook: QuickLook
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
|
||||||
@ObservedObject var viewModel: AccountDetailViewModel
|
@ObservedObject var viewModel: AccountDetailViewModel
|
||||||
let account: Account
|
let account: Account
|
||||||
let scrollViewProxy: ScrollViewProxy?
|
let scrollViewProxy: ScrollViewProxy?
|
||||||
|
|
||||||
@Binding var scrollOffset: CGFloat
|
@Binding var scrollOffset: CGFloat
|
||||||
|
|
||||||
private var bannerHeight: CGFloat {
|
private var bannerHeight: CGFloat {
|
||||||
200 + (scrollOffset > 0 ? scrollOffset * 2 : 0)
|
200 + (scrollOffset > 0 ? scrollOffset * 2 : 0)
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,9 @@ struct AccountDetailHeaderView: View {
|
||||||
accountInfoView
|
accountInfoView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headerImageView: some View {
|
private var headerImageView: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { _ in
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
if reasons.contains(.placeholder) {
|
if reasons.contains(.placeholder) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
@ -53,7 +53,7 @@ struct AccountDetailHeaderView: View {
|
||||||
}
|
}
|
||||||
.frame(height: bannerHeight)
|
.frame(height: bannerHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.relationship?.followedBy == true {
|
if viewModel.relationship?.followedBy == true {
|
||||||
Text("Follows You")
|
Text("Follows You")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
|
@ -75,15 +75,15 @@ struct AccountDetailHeaderView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountAvatarView: some View {
|
private var accountAvatarView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
AvatarView(url: account.avatar, size: .account)
|
AvatarView(url: account.avatar, size: .account)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
Task {
|
Task {
|
||||||
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
|
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Group {
|
Group {
|
||||||
Button {
|
Button {
|
||||||
|
@ -102,17 +102,17 @@ struct AccountDetailHeaderView: View {
|
||||||
}.offset(y: 20)
|
}.offset(y: 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountInfoView: some View {
|
private var accountInfoView: some View {
|
||||||
Group {
|
Group {
|
||||||
accountAvatarView
|
accountAvatarView
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("@\(account.acct)")
|
Text("@\(account.acct)")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if let relationship = viewModel.relationship, !viewModel.isCurrentUser {
|
if let relationship = viewModel.relationship, !viewModel.isCurrentUser {
|
||||||
|
@ -133,7 +133,7 @@ struct AccountDetailHeaderView: View {
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
.offset(y: -40)
|
.offset(y: -40)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeCustomInfoLabel(title: String, count: Int) -> some View {
|
private func makeCustomInfoLabel(title: String, count: Int) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("\(count)")
|
Text("\(count)")
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import SwiftUI
|
import DesignSystem
|
||||||
|
import EmojiText
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Status
|
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import DesignSystem
|
import Status
|
||||||
import Env
|
import SwiftUI
|
||||||
import EmojiText
|
|
||||||
|
|
||||||
public struct AccountDetailView: View {
|
public struct AccountDetailView: View {
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
|
@ -15,7 +15,7 @@ public struct AccountDetailView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
@StateObject private var viewModel: AccountDetailViewModel
|
@StateObject private var viewModel: AccountDetailViewModel
|
||||||
@State private var scrollOffset: CGFloat = 0
|
@State private var scrollOffset: CGFloat = 0
|
||||||
@State private var isFieldsSheetDisplayed: Bool = false
|
@State private var isFieldsSheetDisplayed: Bool = false
|
||||||
|
@ -23,17 +23,17 @@ public struct AccountDetailView: View {
|
||||||
@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
|
||||||
|
|
||||||
/// When coming from a URL like a mention tap in a status.
|
/// When coming from a URL like a mention tap in a status.
|
||||||
public init(accountId: String) {
|
public init(accountId: String) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
|
_viewModel = StateObject(wrappedValue: .init(accountId: accountId))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When the account is already fetched by the parent caller.
|
/// When the account is already fetched by the parent caller.
|
||||||
public init(account: Account) {
|
public init(account: Account) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(account: account))
|
_viewModel = StateObject(wrappedValue: .init(account: account))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollViewOffsetReader { offset in
|
ScrollViewOffsetReader { offset in
|
||||||
|
@ -58,7 +58,7 @@ public struct AccountDetailView: View {
|
||||||
.offset(y: -20)
|
.offset(y: -20)
|
||||||
}
|
}
|
||||||
.id("status")
|
.id("status")
|
||||||
|
|
||||||
switch viewModel.tabState {
|
switch viewModel.tabState {
|
||||||
case .statuses:
|
case .statuses:
|
||||||
if viewModel.selectedTab == .statuses {
|
if viewModel.selectedTab == .statuses {
|
||||||
|
@ -94,9 +94,10 @@ public struct AccountDetailView: View {
|
||||||
await viewModel.fetchStatuses()
|
await viewModel.fetchStatuses()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: watcher.latestEvent?.id) { id in
|
.onChange(of: watcher.latestEvent?.id) { _ in
|
||||||
if let latestEvent = watcher.latestEvent,
|
if let latestEvent = watcher.latestEvent,
|
||||||
viewModel.accountId == currentAccount.account?.id {
|
viewModel.accountId == currentAccount.account?.id
|
||||||
|
{
|
||||||
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +118,7 @@ public struct AccountDetailView: View {
|
||||||
toolbarContent
|
toolbarContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeHeaderView(proxy: ScrollViewProxy?) -> some View {
|
private func makeHeaderView(proxy: ScrollViewProxy?) -> some View {
|
||||||
switch viewModel.accountState {
|
switch viewModel.accountState {
|
||||||
|
@ -137,7 +138,7 @@ public struct AccountDetailView: View {
|
||||||
Text("Error: \(error.localizedDescription)")
|
Text("Error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var featuredTagsView: some View {
|
private var featuredTagsView: some View {
|
||||||
if !viewModel.featuredTags.isEmpty || !viewModel.fields.isEmpty {
|
if !viewModel.featuredTags.isEmpty || !viewModel.fields.isEmpty {
|
||||||
|
@ -178,7 +179,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var familliarFollowers: some View {
|
private var familliarFollowers: some View {
|
||||||
if !viewModel.familliarFollowers.isEmpty {
|
if !viewModel.familliarFollowers.isEmpty {
|
||||||
|
@ -203,7 +204,7 @@ public struct AccountDetailView: View {
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fieldSheetView: some View {
|
private var fieldSheetView: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
|
@ -244,7 +245,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tagsListView: some View {
|
private var tagsListView: some View {
|
||||||
Group {
|
Group {
|
||||||
ForEach(currentAccount.tags) { tag in
|
ForEach(currentAccount.tags) { tag in
|
||||||
|
@ -260,7 +261,7 @@ public struct AccountDetailView: View {
|
||||||
await currentAccount.fetchFollowedTags()
|
await currentAccount.fetchFollowedTags()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var listsListView: some View {
|
private var listsListView: some View {
|
||||||
Group {
|
Group {
|
||||||
ForEach(currentAccount.lists) { list in
|
ForEach(currentAccount.lists) { list in
|
||||||
|
@ -309,7 +310,7 @@ public struct AccountDetailView: View {
|
||||||
Text("Enter the name for your list")
|
Text("Enter the name for your list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var pinnedPostsView: some View {
|
private var pinnedPostsView: some View {
|
||||||
if !viewModel.pinned.isEmpty {
|
if !viewModel.pinned.isEmpty {
|
||||||
|
@ -327,7 +328,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbarContent: some ToolbarContent {
|
private var toolbarContent: some ToolbarContent {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
|
@ -341,7 +342,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Menu {
|
Menu {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
|
@ -360,7 +361,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.relationship?.following == true {
|
if viewModel.relationship?.following == true {
|
||||||
Button {
|
Button {
|
||||||
routeurPath.presentedSheet = .listAddAccount(account: account)
|
routeurPath.presentedSheet = .listAddAccount(account: account)
|
||||||
|
@ -368,13 +369,13 @@ public struct AccountDetailView: View {
|
||||||
Label("Add/Remove from lists", systemImage: "list.bullet")
|
Label("Add/Remove from lists", systemImage: "list.bullet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let url = account.url {
|
if let url = account.url {
|
||||||
ShareLink(item: url)
|
ShareLink(item: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
if isCurrentUser {
|
if isCurrentUser {
|
||||||
Button {
|
Button {
|
||||||
isEditingAccount = true
|
isEditingAccount = true
|
||||||
|
@ -396,4 +397,3 @@ struct AccountDetailView_Previews: PreviewProvider {
|
||||||
AccountDetailView(account: .placeholder())
|
AccountDetailView(account: .placeholder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import Models
|
|
||||||
import Status
|
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import Status
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
let accountId: String
|
let accountId: String
|
||||||
var client: Client?
|
var client: Client?
|
||||||
var isCurrentUser: Bool = false
|
var isCurrentUser: Bool = false
|
||||||
|
|
||||||
enum AccountState {
|
enum AccountState {
|
||||||
case loading, data(account: Account), error(error: Error)
|
case loading, data(account: Account), error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tab: Int {
|
enum Tab: Int {
|
||||||
case statuses, favourites, bookmarks, followedTags, postsAndReplies, media, lists
|
case statuses, favourites, bookmarks, followedTags, postsAndReplies, media, lists
|
||||||
|
|
||||||
static var currentAccountTabs: [Tab] {
|
static var currentAccountTabs: [Tab] {
|
||||||
[.statuses, .favourites, .bookmarks, .followedTags, .lists]
|
[.statuses, .favourites, .bookmarks, .followedTags, .lists]
|
||||||
}
|
}
|
||||||
|
|
||||||
static var accountTabs: [Tab] {
|
static var accountTabs: [Tab] {
|
||||||
[.statuses, .postsAndReplies, .media]
|
[.statuses, .postsAndReplies, .media]
|
||||||
}
|
}
|
||||||
|
|
||||||
var iconName: String {
|
var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .statuses: return "bubble.right"
|
case .statuses: return "bubble.right"
|
||||||
|
@ -37,13 +37,13 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TabState {
|
enum TabState {
|
||||||
case followedTags
|
case followedTags
|
||||||
case statuses(statusesState: StatusesState)
|
case statuses(statusesState: StatusesState)
|
||||||
case lists
|
case lists
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var accountState: AccountState = .loading
|
@Published var accountState: AccountState = .loading
|
||||||
@Published var tabState: TabState = .statuses(statusesState: .loading) {
|
@Published var tabState: TabState = .statuses(statusesState: .loading) {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -57,8 +57,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var statusesState: StatusesState = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
|
|
||||||
@Published var relationship: Relationshionship?
|
@Published var relationship: Relationshionship?
|
||||||
@Published var pinned: [Status] = []
|
@Published var pinned: [Status] = []
|
||||||
@Published var favourites: [Status] = []
|
@Published var favourites: [Status] = []
|
||||||
|
@ -81,45 +82,45 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var account: Account?
|
private(set) var account: Account?
|
||||||
private var tabTask: Task<Void, Never>?
|
private var tabTask: Task<Void, Never>?
|
||||||
|
|
||||||
private(set) var statuses: [Status] = []
|
private(set) var statuses: [Status] = []
|
||||||
|
|
||||||
/// When coming from a URL like a mention tap in a status.
|
/// When coming from a URL like a mention tap in a status.
|
||||||
init(accountId: String) {
|
init(accountId: String) {
|
||||||
self.accountId = accountId
|
self.accountId = accountId
|
||||||
self.isCurrentUser = false
|
isCurrentUser = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When the account is already fetched by the parent caller.
|
/// When the account is already fetched by the parent caller.
|
||||||
init(account: Account) {
|
init(account: Account) {
|
||||||
self.accountId = account.id
|
accountId = account.id
|
||||||
self.account = account
|
self.account = account
|
||||||
self.accountState = .data(account: account)
|
accountState = .data(account: account)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountData {
|
struct AccountData {
|
||||||
let account: Account
|
let account: Account
|
||||||
let featuredTags: [FeaturedTag]
|
let featuredTags: [FeaturedTag]
|
||||||
let relationships: [Relationshionship]
|
let relationships: [Relationshionship]
|
||||||
let familliarFollowers: [FamilliarAccounts]
|
let familliarFollowers: [FamilliarAccounts]
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let data = try await fetchAccountData(accountId: accountId, client: client)
|
let data = try await fetchAccountData(accountId: accountId, client: client)
|
||||||
accountState = .data(account: data.account)
|
accountState = .data(account: data.account)
|
||||||
|
|
||||||
account = data.account
|
account = data.account
|
||||||
fields = data.account.fields
|
fields = data.account.fields
|
||||||
featuredTags = data.featuredTags
|
featuredTags = data.featuredTags
|
||||||
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
||||||
relationship = data.relationships.first
|
relationship = data.relationships.first
|
||||||
familliarFollowers = data.familliarFollowers.first?.accounts ?? []
|
familliarFollowers = data.familliarFollowers.first?.accounts ?? []
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
if let account {
|
if let account {
|
||||||
accountState = .data(account: account)
|
accountState = .data(account: account)
|
||||||
|
@ -128,7 +129,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccountData(accountId: String, client: Client) async throws -> AccountData {
|
func fetchAccountData(accountId: String, client: Client) async throws -> AccountData {
|
||||||
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
|
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
|
||||||
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
|
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
|
||||||
|
@ -145,26 +146,26 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
relationships: [],
|
relationships: [],
|
||||||
familliarFollowers: [])
|
familliarFollowers: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStatuses() async {
|
func fetchStatuses() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
tabState = .statuses(statusesState: .loading)
|
tabState = .statuses(statusesState: .loading)
|
||||||
statuses =
|
statuses =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
|
||||||
sinceId: nil,
|
|
||||||
tag: nil,
|
|
||||||
onlyMedia: selectedTab == .media ? true : nil,
|
|
||||||
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil,
|
|
||||||
pinned: nil))
|
|
||||||
if selectedTab == .statuses {
|
|
||||||
pinned =
|
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||||
sinceId: nil,
|
sinceId: nil,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: nil,
|
onlyMedia: selectedTab == .media ? true : nil,
|
||||||
excludeReplies: nil,
|
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil,
|
||||||
pinned: true))
|
pinned: nil))
|
||||||
|
if selectedTab == .statuses {
|
||||||
|
pinned =
|
||||||
|
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||||
|
sinceId: nil,
|
||||||
|
tag: nil,
|
||||||
|
onlyMedia: nil,
|
||||||
|
excludeReplies: nil,
|
||||||
|
pinned: true))
|
||||||
}
|
}
|
||||||
if isCurrentUser {
|
if isCurrentUser {
|
||||||
(favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil))
|
(favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil))
|
||||||
|
@ -175,7 +176,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
tabState = .statuses(statusesState: .error(error: error))
|
tabState = .statuses(statusesState: .error(error: error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -184,12 +185,12 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage))
|
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .loadingNextPage))
|
||||||
let newStatuses: [Status] =
|
let newStatuses: [Status] =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||||
sinceId: lastId,
|
sinceId: lastId,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: selectedTab == .media ? true : nil,
|
onlyMedia: selectedTab == .media ? true : nil,
|
||||||
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil,
|
excludeReplies: selectedTab == .statuses && !isCurrentUser ? true : nil,
|
||||||
pinned: nil))
|
pinned: nil))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
tabState = .statuses(statusesState: .display(statuses: statuses,
|
tabState = .statuses(statusesState: .display(statuses: statuses,
|
||||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage))
|
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage))
|
||||||
|
@ -212,7 +213,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
tabState = .statuses(statusesState: .error(error: error))
|
tabState = .statuses(statusesState: .error(error: error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadTabState() {
|
private func reloadTabState() {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses, .postsAndReplies, .media:
|
case .statuses, .postsAndReplies, .media:
|
||||||
|
@ -229,7 +230,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
tabState = .lists
|
tabState = .lists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
|
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
|
||||||
if let event = event as? StreamEventUpdate {
|
if let event = event as? StreamEventUpdate {
|
||||||
if event.status.account.id == currentAccount.account?.id {
|
if event.status.account.id == currentAccount.account?.id {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
import DesignSystem
|
||||||
|
import EmojiText
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import DesignSystem
|
import SwiftUI
|
||||||
import Env
|
|
||||||
import EmojiText
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class AccountsListRowViewModel: ObservableObject {
|
public class AccountsListRowViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
@Published var account: Account
|
@Published var account: Account
|
||||||
@Published var relationShip: Relationshionship
|
@Published var relationShip: Relationshionship
|
||||||
|
|
||||||
public init(account: Account, relationShip: Relationshionship) {
|
public init(account: Account, relationShip: Relationshionship) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.relationShip = relationShip
|
self.relationShip = relationShip
|
||||||
|
@ -22,13 +22,13 @@ public struct AccountsListRow: View {
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@StateObject var viewModel: AccountsListRowViewModel
|
@StateObject var viewModel: AccountsListRowViewModel
|
||||||
|
|
||||||
public init(viewModel: AccountsListRowViewModel) {
|
public init(viewModel: AccountsListRowViewModel) {
|
||||||
_viewModel = StateObject(wrappedValue: viewModel)
|
_viewModel = StateObject(wrappedValue: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
AvatarView(url: viewModel.account.avatar, size: .status)
|
AvatarView(url: viewModel.account.avatar, size: .status)
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import Shimmer
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AccountsListView: View {
|
public struct AccountsListView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@StateObject private var viewModel: AccountsListViewModel
|
@StateObject private var viewModel: AccountsListViewModel
|
||||||
@State private var didAppear: Bool = false
|
@State private var didAppear: Bool = false
|
||||||
|
|
||||||
public init(mode: AccountsListMode) {
|
public init(mode: AccountsListMode) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(mode: mode))
|
_viewModel = StateObject(wrappedValue: .init(mode: mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
switch viewModel.state {
|
switch viewModel.state {
|
||||||
case .loading:
|
case .loading:
|
||||||
ForEach(Account.placeholders()) { account in
|
ForEach(Account.placeholders()) { _ in
|
||||||
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
|
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.shimmering()
|
.shimmering()
|
||||||
|
@ -30,10 +30,10 @@ public struct AccountsListView: View {
|
||||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||||
AccountsListRow(viewModel: .init(account: account,
|
AccountsListRow(viewModel: .init(account: account,
|
||||||
relationShip: relationship))
|
relationShip: relationship))
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
case .hasNextPage:
|
case .hasNextPage:
|
||||||
loadingRow
|
loadingRow
|
||||||
|
@ -43,14 +43,14 @@ public struct AccountsListView: View {
|
||||||
await viewModel.fetchNextPage()
|
await viewModel.fetchNextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .loadingNextPage:
|
case .loadingNextPage:
|
||||||
loadingRow
|
loadingRow
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
Text(error.localizedDescription)
|
Text(error.localizedDescription)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
@ -63,12 +63,12 @@ public struct AccountsListView: View {
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
guard !didAppear else { return}
|
guard !didAppear else { return }
|
||||||
didAppear = true
|
didAppear = true
|
||||||
await viewModel.fetch()
|
await viewModel.fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingRow: some View {
|
private var loadingRow: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public enum AccountsListMode {
|
public enum AccountsListMode {
|
||||||
case following(accountId: String), followers(accountId: String)
|
case following(accountId: String), followers(accountId: String)
|
||||||
case favouritedBy(statusId: String), rebloggedBy(statusId: String)
|
case favouritedBy(statusId: String), rebloggedBy(statusId: String)
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .following:
|
case .following:
|
||||||
|
@ -23,31 +23,32 @@ public enum AccountsListMode {
|
||||||
@MainActor
|
@MainActor
|
||||||
class AccountsListViewModel: ObservableObject {
|
class AccountsListViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
let mode: AccountsListMode
|
let mode: AccountsListMode
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
public enum PagingState {
|
public enum PagingState {
|
||||||
case hasNextPage, loadingNextPage, none
|
case hasNextPage, loadingNextPage, none
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
case display(accounts: [Account],
|
case display(accounts: [Account],
|
||||||
relationships: [Relationshionship],
|
relationships: [Relationshionship],
|
||||||
nextPageState: PagingState)
|
nextPageState: PagingState)
|
||||||
case error(error: Error)
|
case error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accounts: [Account] = []
|
private var accounts: [Account] = []
|
||||||
private var relationships: [Relationshionship] = []
|
private var relationships: [Relationshionship] = []
|
||||||
|
|
||||||
@Published var state = State.loading
|
@Published var state = State.loading
|
||||||
|
|
||||||
private var nextPageId: String?
|
private var nextPageId: String?
|
||||||
|
|
||||||
init(mode: AccountsListMode) {
|
init(mode: AccountsListMode) {
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch() async {
|
func fetch() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -69,13 +70,13 @@ class AccountsListViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
nextPageId = link?.maxId
|
nextPageId = link?.maxId
|
||||||
relationships = try await client.get(endpoint:
|
relationships = try await client.get(endpoint:
|
||||||
Accounts.relationships(ids: accounts.map{ $0.id }))
|
Accounts.relationships(ids: accounts.map { $0.id }))
|
||||||
state = .display(accounts: accounts,
|
state = .display(accounts: accounts,
|
||||||
relationships: relationships,
|
relationships: relationships,
|
||||||
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async {
|
||||||
guard let client, let nextPageId else { return }
|
guard let client, let nextPageId else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -93,13 +94,13 @@ class AccountsListViewModel: ObservableObject {
|
||||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
|
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
|
||||||
maxId: nextPageId))
|
maxId: nextPageId))
|
||||||
case let .favouritedBy(statusId):
|
case let .favouritedBy(statusId):
|
||||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId,
|
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId,
|
||||||
maxId: nextPageId))
|
maxId: nextPageId))
|
||||||
}
|
}
|
||||||
accounts.append(contentsOf: newAccounts)
|
accounts.append(contentsOf: newAccounts)
|
||||||
let newRelationships: [Relationshionship] =
|
let newRelationships: [Relationshionship] =
|
||||||
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id }))
|
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map { $0.id }))
|
||||||
|
|
||||||
relationships.append(contentsOf: newRelationships)
|
relationships.append(contentsOf: newRelationships)
|
||||||
self.nextPageId = link?.maxId
|
self.nextPageId = link?.maxId
|
||||||
state = .display(accounts: accounts,
|
state = .display(accounts: accounts,
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import SwiftUI
|
import DesignSystem
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import DesignSystem
|
import SwiftUI
|
||||||
|
|
||||||
struct EditAccountView: View {
|
struct EditAccountView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@StateObject private var viewModel = EditAccountViewModel()
|
@StateObject private var viewModel = EditAccountViewModel()
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
@ -31,15 +31,15 @@ struct EditAccountView: View {
|
||||||
.alert("Error while saving your profile",
|
.alert("Error while saving your profile",
|
||||||
isPresented: $viewModel.saveError,
|
isPresented: $viewModel.saveError,
|
||||||
actions: {
|
actions: {
|
||||||
Button("Ok", action: { })
|
Button("Ok", action: {})
|
||||||
}, message: { Text("Error while saving your profile, please try again.") })
|
}, message: { Text("Error while saving your profile, please try again.") })
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
await viewModel.fetchAccount()
|
await viewModel.fetchAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingSection: some View {
|
private var loadingSection: some View {
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -50,7 +50,7 @@ struct EditAccountView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var aboutSections: some View {
|
private var aboutSections: some View {
|
||||||
Section("Display Name") {
|
Section("Display Name") {
|
||||||
|
@ -63,7 +63,7 @@ struct EditAccountView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postSettingsSection: some View {
|
private var postSettingsSection: some View {
|
||||||
Section("Post settings") {
|
Section("Post settings") {
|
||||||
Picker(selection: $viewModel.postPrivacy) {
|
Picker(selection: $viewModel.postPrivacy) {
|
||||||
|
@ -80,7 +80,7 @@ struct EditAccountView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountSection: some View {
|
private var accountSection: some View {
|
||||||
Section("Account settings") {
|
Section("Account settings") {
|
||||||
Toggle(isOn: $viewModel.isLocked) {
|
Toggle(isOn: $viewModel.isLocked) {
|
||||||
|
@ -95,7 +95,7 @@ struct EditAccountView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbarContent: some ToolbarContent {
|
private var toolbarContent: some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
@ -103,7 +103,7 @@ struct EditAccountView: View {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class EditAccountViewModel: ObservableObject {
|
class EditAccountViewModel: ObservableObject {
|
||||||
public var client: Client?
|
public var client: Client?
|
||||||
|
|
||||||
@Published var displayName: String = ""
|
@Published var displayName: String = ""
|
||||||
@Published var note: String = ""
|
@Published var note: String = ""
|
||||||
@Published var postPrivacy = Models.Visibility.pub
|
@Published var postPrivacy = Models.Visibility.pub
|
||||||
|
@ -13,13 +13,13 @@ class EditAccountViewModel: ObservableObject {
|
||||||
@Published var isBot: Bool = false
|
@Published var isBot: Bool = false
|
||||||
@Published var isLocked: Bool = false
|
@Published var isLocked: Bool = false
|
||||||
@Published var isDiscoverable: Bool = false
|
@Published var isDiscoverable: Bool = false
|
||||||
|
|
||||||
@Published var isLoading: Bool = true
|
@Published var isLoading: Bool = true
|
||||||
@Published var isSaving: Bool = false
|
@Published var isSaving: Bool = false
|
||||||
@Published var saveError: Bool = false
|
@Published var saveError: Bool = false
|
||||||
|
|
||||||
init() { }
|
init() {}
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -34,20 +34,20 @@ class EditAccountViewModel: ObservableObject {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func save() async {
|
func save() async {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
do {
|
do {
|
||||||
let response =
|
let response =
|
||||||
try await client?.patch(endpoint: Accounts.updateCredentials(displayName: displayName,
|
try await client?.patch(endpoint: Accounts.updateCredentials(displayName: displayName,
|
||||||
note: note,
|
note: note,
|
||||||
privacy: postPrivacy,
|
privacy: postPrivacy,
|
||||||
isSensitive: isSensitive,
|
isSensitive: isSensitive,
|
||||||
isBot: isBot,
|
isBot: isBot,
|
||||||
isLocked: isLocked,
|
isLocked: isLocked,
|
||||||
isDiscoverable: isDiscoverable))
|
isDiscoverable: isDiscoverable))
|
||||||
if response?.statusCode != 200 {
|
if response?.statusCode != 200 {
|
||||||
saveError = true
|
saveError = true
|
||||||
}
|
}
|
||||||
|
@ -57,5 +57,4 @@ class EditAccountViewModel: ObservableObject {
|
||||||
saveError = true
|
saveError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class FollowButtonViewModel: ObservableObject {
|
public class FollowButtonViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
public let accountId: String
|
public let accountId: String
|
||||||
public let shouldDisplayNotify: Bool
|
public let shouldDisplayNotify: Bool
|
||||||
@Published private(set) public var relationship: Relationshionship
|
@Published public private(set) var relationship: Relationshionship
|
||||||
@Published private(set) public var isUpdating: Bool = false
|
@Published public private(set) var isUpdating: Bool = false
|
||||||
|
|
||||||
public init(accountId: String, relationship: Relationshionship, shouldDisplayNotify: Bool) {
|
public init(accountId: String, relationship: Relationshionship, shouldDisplayNotify: Bool) {
|
||||||
self.accountId = accountId
|
self.accountId = accountId
|
||||||
self.relationship = relationship
|
self.relationship = relationship
|
||||||
self.shouldDisplayNotify = shouldDisplayNotify
|
self.shouldDisplayNotify = shouldDisplayNotify
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow() async {
|
func follow() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
isUpdating = true
|
isUpdating = true
|
||||||
|
@ -28,7 +28,7 @@ public class FollowButtonViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow() async {
|
func unfollow() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
isUpdating = true
|
isUpdating = true
|
||||||
|
@ -39,7 +39,7 @@ public class FollowButtonViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleNotify() async {
|
func toggleNotify() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -53,11 +53,11 @@ public class FollowButtonViewModel: ObservableObject {
|
||||||
public struct FollowButton: View {
|
public struct FollowButton: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@StateObject private var viewModel: FollowButtonViewModel
|
@StateObject private var viewModel: FollowButtonViewModel
|
||||||
|
|
||||||
public init(viewModel: FollowButtonViewModel) {
|
public init(viewModel: FollowButtonViewModel) {
|
||||||
_viewModel = StateObject(wrappedValue: viewModel)
|
_viewModel = StateObject(wrappedValue: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import XCTest
|
|
||||||
@testable import Account
|
@testable import Account
|
||||||
|
import XCTest
|
||||||
|
|
||||||
final class AccountTests: XCTestCase {
|
final class AccountTests: XCTestCase {
|
||||||
func testExample() throws {
|
func testExample() throws {
|
||||||
// This is an example of a functional test case.
|
// This is an example of a functional test case.
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||||
// results.
|
// results.
|
||||||
XCTAssertEqual(Account().text, "Hello, World!")
|
XCTAssertEqual(Account().text, "Hello, World!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "AppAccount",
|
name: "AppAccount",
|
||||||
targets: ["AppAccount"]),
|
targets: ["AppAccount"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Network", path: "../Network"),
|
.package(name: "Network", path: "../Network"),
|
||||||
|
@ -27,6 +28,7 @@ let package = Package(
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "DesignSystem", package: "DesignSystem"),
|
.product(name: "DesignSystem", package: "DesignSystem"),
|
||||||
])
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
import CryptoKit
|
||||||
import Network
|
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
import Models
|
import Models
|
||||||
import CryptoKit
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AppAccount: Codable, Identifiable {
|
public struct AppAccount: Codable, Identifiable {
|
||||||
public let server: String
|
public let server: String
|
||||||
public let oauthToken: OauthToken?
|
public let oauthToken: OauthToken?
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var keychain: KeychainSwift {
|
private static var keychain: KeychainSwift {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
|
@ -19,7 +19,7 @@ public struct AppAccount: Codable, Identifiable {
|
||||||
#endif
|
#endif
|
||||||
return keychain
|
return keychain
|
||||||
}
|
}
|
||||||
|
|
||||||
public var key: String {
|
public var key: String {
|
||||||
if let oauthToken {
|
if let oauthToken {
|
||||||
return "\(server):\(oauthToken.createdAt)"
|
return "\(server):\(oauthToken.createdAt)"
|
||||||
|
@ -27,22 +27,22 @@ public struct AppAccount: Codable, Identifiable {
|
||||||
return "\(server):anonymous:\(Date().timeIntervalSince1970)"
|
return "\(server):anonymous:\(Date().timeIntervalSince1970)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(server: String, oauthToken: OauthToken? = nil) {
|
public init(server: String, oauthToken: OauthToken? = nil) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.oauthToken = oauthToken
|
self.oauthToken = oauthToken
|
||||||
}
|
}
|
||||||
|
|
||||||
public func save() throws {
|
public 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() {
|
public func delete() {
|
||||||
Self.keychain.delete(key)
|
Self.keychain.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func retrieveAll() -> [AppAccount] {
|
public static func retrieveAll() -> [AppAccount] {
|
||||||
migrateLegacyAccounts()
|
migrateLegacyAccounts()
|
||||||
let keychain = Self.keychain
|
let keychain = Self.keychain
|
||||||
|
@ -58,7 +58,7 @@ public struct AppAccount: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
return accounts
|
return accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func migrateLegacyAccounts() {
|
public static func migrateLegacyAccounts() {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
|
@ -71,7 +71,7 @@ public struct AppAccount: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func deleteAll() {
|
public static func deleteAll() {
|
||||||
let keychain = Self.keychain
|
let keychain = Self.keychain
|
||||||
let keys = keychain.allKeys
|
let keys = keychain.allKeys
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
|
||||||
import EmojiText
|
import EmojiText
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AppAccountView: View {
|
public struct AppAccountView: View {
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
@EnvironmentObject var appAccounts: AppAccountsManager
|
@EnvironmentObject var appAccounts: AppAccountsManager
|
||||||
@StateObject var viewModel: AppAccountViewModel
|
@StateObject var viewModel: AppAccountViewModel
|
||||||
|
|
||||||
public init(viewModel: AppAccountViewModel) {
|
public init(viewModel: AppAccountViewModel) {
|
||||||
_viewModel = .init(wrappedValue: viewModel)
|
_viewModel = .init(wrappedValue: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
|
@ -43,7 +43,8 @@ public struct AppAccountView: View {
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if appAccounts.currentAccount.id == viewModel.appAccount.id,
|
if appAccounts.currentAccount.id == viewModel.appAccount.id,
|
||||||
let account = viewModel.account {
|
let account = viewModel.account
|
||||||
|
{
|
||||||
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||||
} else {
|
} else {
|
||||||
appAccounts.currentAccount = viewModel.appAccount
|
appAccounts.currentAccount = viewModel.appAccount
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class AppAccountViewModel: ObservableObject {
|
public class AppAccountViewModel: ObservableObject {
|
||||||
let appAccount: AppAccount
|
let appAccount: AppAccount
|
||||||
let client: Client
|
let client: Client
|
||||||
|
|
||||||
@Published var account: Account?
|
@Published var account: Account?
|
||||||
|
|
||||||
var acct: String {
|
var acct: String {
|
||||||
"@\(account?.acct ?? "...")@\(appAccount.server)"
|
"@\(account?.acct ?? "...")@\(appAccount.server)"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(appAccount: AppAccount) {
|
public init(appAccount: AppAccount) {
|
||||||
self.appAccount = appAccount
|
self.appAccount = appAccount
|
||||||
self.client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
|
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
do {
|
do {
|
||||||
account = try await client.get(endpoint: Accounts.verifyCredentials)
|
account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class AppAccountsManager: ObservableObject {
|
public class AppAccountsManager: ObservableObject {
|
||||||
@AppStorage("latestCurrentAccountKey", store: UserPreferences.sharedDefault)
|
@AppStorage("latestCurrentAccountKey", store: UserPreferences.sharedDefault)
|
||||||
static public var latestCurrentAccountKey: String = ""
|
public static var latestCurrentAccountKey: String = ""
|
||||||
|
|
||||||
@Published public var currentAccount: AppAccount {
|
@Published public var currentAccount: AppAccount {
|
||||||
didSet {
|
didSet {
|
||||||
Self.latestCurrentAccountKey = currentAccount.id
|
Self.latestCurrentAccountKey = currentAccount.id
|
||||||
|
@ -15,16 +15,17 @@ public class AppAccountsManager: ObservableObject {
|
||||||
oauthToken: currentAccount.oauthToken)
|
oauthToken: currentAccount.oauthToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published public var availableAccounts: [AppAccount]
|
@Published public var availableAccounts: [AppAccount]
|
||||||
@Published public var currentClient: Client
|
@Published public var currentClient: Client
|
||||||
|
|
||||||
public var pushAccounts: [PushNotificationsService.PushAccounts] {
|
public var pushAccounts: [PushNotificationsService.PushAccounts] {
|
||||||
availableAccounts.filter{ $0.oauthToken != nil}
|
availableAccounts.filter { $0.oauthToken != nil }
|
||||||
.map{ .init(server: $0.server, token: $0.oauthToken!) }
|
.map { .init(server: $0.server, token: $0.oauthToken!) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var shared = AppAccountsManager()
|
public static var shared = AppAccountsManager()
|
||||||
|
|
||||||
internal init() {
|
internal init() {
|
||||||
var defaultAccount = AppAccount(server: AppInfo.defaultServer, oauthToken: nil)
|
var defaultAccount = AppAccount(server: AppInfo.defaultServer, oauthToken: nil)
|
||||||
let keychainAccounts = AppAccount.retrieveAll()
|
let keychainAccounts = AppAccount.retrieveAll()
|
||||||
|
@ -37,15 +38,15 @@ public class AppAccountsManager: ObservableObject {
|
||||||
currentAccount = defaultAccount
|
currentAccount = defaultAccount
|
||||||
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func add(account: AppAccount) {
|
public func add(account: AppAccount) {
|
||||||
do {
|
do {
|
||||||
try account.save()
|
try account.save()
|
||||||
availableAccounts.append(account)
|
availableAccounts.append(account)
|
||||||
currentAccount = account
|
currentAccount = account
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(account: AppAccount) {
|
public func delete(account: AppAccount) {
|
||||||
availableAccounts.removeAll(where: { $0.id == account.id })
|
availableAccounts.removeAll(where: { $0.id == account.id })
|
||||||
account.delete()
|
account.delete()
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AppAccountsSelectorView: View {
|
public struct AppAccountsSelectorView: View {
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
|
|
||||||
@ObservedObject var routeurPath: RouterPath
|
@ObservedObject var routeurPath: RouterPath
|
||||||
|
|
||||||
@State private var accountsViewModel: [AppAccountViewModel] = []
|
@State private var accountsViewModel: [AppAccountViewModel] = []
|
||||||
|
|
||||||
private let accountCreationEnabled: Bool
|
private let accountCreationEnabled: Bool
|
||||||
private let avatarSize: AvatarView.Size
|
private let avatarSize: AvatarView.Size
|
||||||
|
|
||||||
public init(routeurPath: RouterPath,
|
public init(routeurPath: RouterPath,
|
||||||
accountCreationEnabled: Bool = true,
|
accountCreationEnabled: Bool = true,
|
||||||
avatarSize: AvatarView.Size = .badge) {
|
avatarSize: AvatarView.Size = .badge)
|
||||||
|
{
|
||||||
self.routeurPath = routeurPath
|
self.routeurPath = routeurPath
|
||||||
self.accountCreationEnabled = accountCreationEnabled
|
self.accountCreationEnabled = accountCreationEnabled
|
||||||
self.avatarSize = avatarSize
|
self.avatarSize = avatarSize
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
@ -43,7 +44,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
refreshAccounts()
|
refreshAccounts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var labelView: some View {
|
private var labelView: some View {
|
||||||
if let avatar = currentAccount.account?.avatar {
|
if let avatar = currentAccount.account?.avatar {
|
||||||
|
@ -52,14 +53,15 @@ public struct AppAccountsSelectorView: View {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var menuView: some View {
|
private var menuView: some View {
|
||||||
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
||||||
Section(viewModel.acct) {
|
Section(viewModel.acct) {
|
||||||
Button {
|
Button {
|
||||||
if let account = currentAccount.account,
|
if let account = currentAccount.account,
|
||||||
viewModel.account?.id == account.id {
|
viewModel.account?.id == account.id
|
||||||
|
{
|
||||||
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
routeurPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||||
} else {
|
} else {
|
||||||
appAccounts.currentAccount = viewModel.appAccount
|
appAccounts.currentAccount = viewModel.appAccount
|
||||||
|
@ -83,7 +85,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshAccounts() {
|
private func refreshAccounts() {
|
||||||
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count {
|
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count {
|
||||||
accountsViewModel = []
|
accountsViewModel = []
|
||||||
|
@ -98,5 +100,4 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Conversations",
|
name: "Conversations",
|
||||||
targets: ["Conversations"]),
|
targets: ["Conversations"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
|
@ -27,7 +28,7 @@ let package = Package(
|
||||||
.product(name: "Network", package: "Network"),
|
.product(name: "Network", package: "Network"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "DesignSystem", package: "DesignSystem"),
|
.product(name: "DesignSystem", package: "DesignSystem"),
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import Accounts
|
import Accounts
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ConversationsListRow: View {
|
struct ConversationsListRow: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let conversation: Conversation
|
let conversation: Conversation
|
||||||
@ObservedObject var viewModel: ConversationsListViewModel
|
@ObservedObject var viewModel: ConversationsListViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
AvatarView(url: conversation.accounts.first!.avatar)
|
AvatarView(url: conversation.accounts.first!.avatar)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(conversation.accounts.map{ $0.safeDisplayName }.joined(separator: ", "))
|
Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", "))
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(theme.labelColor)
|
.foregroundColor(theme.labelColor)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
@ -52,7 +52,7 @@ struct ConversationsListRow: View {
|
||||||
contextMenu
|
contextMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionsView: some View {
|
private var actionsView: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button {
|
Button {
|
||||||
|
@ -71,7 +71,7 @@ struct ConversationsListRow: View {
|
||||||
.padding(.leading, 48)
|
.padding(.leading, 48)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var contextMenu: some View {
|
private var contextMenu: some View {
|
||||||
Button {
|
Button {
|
||||||
|
@ -81,7 +81,7 @@ struct ConversationsListRow: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("Mark as read", systemImage: "eye")
|
Label("Mark as read", systemImage: "eye")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.delete(conversation: conversation)
|
await viewModel.delete(conversation: conversation)
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Shimmer
|
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ConversationsListView: View {
|
public struct ConversationsListView: View {
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@StateObject private var viewModel = ConversationsListViewModel()
|
@StateObject private var viewModel = ConversationsListViewModel()
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
private var conversations: [Conversation] {
|
private var conversations: [Conversation] {
|
||||||
if viewModel.isLoadingFirstPage {
|
if viewModel.isLoadingFirstPage {
|
||||||
return Conversation.placeholders()
|
return Conversation.placeholders()
|
||||||
}
|
}
|
||||||
return viewModel.conversations
|
return viewModel.conversations
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
|
@ -61,7 +61,7 @@ public struct ConversationsListView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
StatusEditorToolbarItem(visibility: .direct)
|
StatusEditorToolbarItem(visibility: .direct)
|
||||||
}
|
}
|
||||||
.onChange(of: watcher.latestEvent?.id) { id in
|
.onChange(of: watcher.latestEvent?.id) { _ in
|
||||||
if let latestEvent = watcher.latestEvent {
|
if let latestEvent = watcher.latestEvent {
|
||||||
viewModel.handleEvent(event: latestEvent)
|
viewModel.handleEvent(event: latestEvent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ import SwiftUI
|
||||||
@MainActor
|
@MainActor
|
||||||
class ConversationsListViewModel: ObservableObject {
|
class ConversationsListViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
@Published var isLoadingFirstPage: Bool = true
|
@Published var isLoadingFirstPage: Bool = true
|
||||||
@Published var conversations: [Conversation] = []
|
@Published var conversations: [Conversation] = []
|
||||||
@Published var isError: Bool = false
|
@Published var isError: Bool = false
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
func fetchConversations() async {
|
func fetchConversations() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
if conversations.isEmpty {
|
if conversations.isEmpty {
|
||||||
|
@ -25,18 +25,18 @@ class ConversationsListViewModel: ObservableObject {
|
||||||
isLoadingFirstPage = false
|
isLoadingFirstPage = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAsRead(conversation: Conversation) async {
|
func markAsRead(conversation: Conversation) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
_ = try? await client.post(endpoint: Conversations.read(id: conversation.id))
|
_ = try? await client.post(endpoint: Conversations.read(id: conversation.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(conversation: Conversation) async {
|
func delete(conversation: Conversation) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
_ = try? await client.delete(endpoint: Conversations.delete(id: conversation.id))
|
_ = try? await client.delete(endpoint: Conversations.delete(id: conversation.id))
|
||||||
await fetchConversations()
|
await fetchConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleEvent(event: any StreamEvent) {
|
func handleEvent(event: any StreamEvent) {
|
||||||
if let event = event as? StreamEventConversation {
|
if let event = event as? StreamEventConversation {
|
||||||
if let index = conversations.firstIndex(where: { $0.id == event.conversation.id }) {
|
if let index = conversations.firstIndex(where: { $0.id == event.conversation.id }) {
|
||||||
|
|
|
@ -11,14 +11,15 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "DesignSystem",
|
name: "DesignSystem",
|
||||||
targets: ["DesignSystem"]),
|
targets: ["DesignSystem"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
.package(name: "Env", path: "../Env"),
|
.package(name: "Env", path: "../Env"),
|
||||||
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0"),
|
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0"),
|
||||||
.package(url: "https://github.com/kean/Nuke", from: "11.5.0"),
|
.package(url: "https://github.com/kean/Nuke", from: "11.5.0"),
|
||||||
.package(url: "https://github.com/divadretlaw/EmojiText", from: "1.1.0")
|
.package(url: "https://github.com/divadretlaw/EmojiText", from: "1.1.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -29,8 +30,8 @@ let package = Package(
|
||||||
.product(name: "Shimmer", package: "SwiftUI-Shimmer"),
|
.product(name: "Shimmer", package: "SwiftUI-Shimmer"),
|
||||||
.product(name: "NukeUI", package: "Nuke"),
|
.product(name: "NukeUI", package: "Nuke"),
|
||||||
.product(name: "Nuke", package: "Nuke"),
|
.product(name: "Nuke", package: "Nuke"),
|
||||||
.product(name: "EmojiText", package: "EmojiText")
|
.product(name: "EmojiText", package: "EmojiText"),
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import NukeUI
|
|
||||||
import Models
|
import Models
|
||||||
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension Account {
|
public extension Account {
|
||||||
private struct Part: Identifiable {
|
private struct Part: Identifiable {
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
let value: Substring
|
let value: Substring
|
||||||
}
|
}
|
||||||
|
|
||||||
public var safeDisplayName: String {
|
var safeDisplayName: String {
|
||||||
if displayName.isEmpty {
|
if displayName.isEmpty {
|
||||||
return username
|
return username
|
||||||
}
|
}
|
||||||
return displayName
|
return displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
public var displayNameWithoutEmojis: String {
|
var displayNameWithoutEmojis: String {
|
||||||
var name = safeDisplayName
|
var name = safeDisplayName
|
||||||
for emoji in emojis {
|
for emoji in emojis {
|
||||||
name = name.replacingOccurrences(of: ":\(emoji.shortcode):", with: "")
|
name = name.replacingOccurrences(of: ":\(emoji.shortcode):", with: "")
|
||||||
|
|
|
@ -24,68 +24,65 @@ public enum ColorSetName: String {
|
||||||
public struct IceCubeDark: ColorSet {
|
public struct IceCubeDark: ColorSet {
|
||||||
public var name: ColorSetName = .iceCubeDark
|
public var name: ColorSetName = .iceCubeDark
|
||||||
public var scheme: ColorScheme = .dark
|
public var scheme: ColorScheme = .dark
|
||||||
public var tintColor: Color = Color(red: 187/255, green: 59/255, blue: 226/255)
|
public var tintColor: Color = .init(red: 187 / 255, green: 59 / 255, blue: 226 / 255)
|
||||||
public var primaryBackgroundColor: Color = Color(red: 16/255, green: 21/255, blue: 35/255)
|
public var primaryBackgroundColor: Color = .init(red: 16 / 255, green: 21 / 255, blue: 35 / 255)
|
||||||
public var secondaryBackgroundColor: Color = Color(red: 30/255, green: 35/255, blue: 62/255)
|
public var secondaryBackgroundColor: Color = .init(red: 30 / 255, green: 35 / 255, blue: 62 / 255)
|
||||||
public var labelColor: Color = .white
|
public var labelColor: Color = .white
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct IceCubeLight: ColorSet {
|
public struct IceCubeLight: ColorSet {
|
||||||
public var name: ColorSetName = .iceCubeLight
|
public var name: ColorSetName = .iceCubeLight
|
||||||
public var scheme: ColorScheme = .light
|
public var scheme: ColorScheme = .light
|
||||||
public var tintColor: Color = Color(red: 187/255, green: 59/255, blue: 226/255)
|
public var tintColor: Color = .init(red: 187 / 255, green: 59 / 255, blue: 226 / 255)
|
||||||
public var primaryBackgroundColor: Color = .white
|
public var primaryBackgroundColor: Color = .white
|
||||||
public var secondaryBackgroundColor: Color = Color(hex:0xF0F1F2)
|
public var secondaryBackgroundColor: Color = .init(hex: 0xF0F1F2)
|
||||||
public var labelColor: Color = .black
|
public var labelColor: Color = .black
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct DesertDark: ColorSet {
|
public struct DesertDark: ColorSet {
|
||||||
public var name: ColorSetName = .desertDark
|
public var name: ColorSetName = .desertDark
|
||||||
public var scheme: ColorScheme = .dark
|
public var scheme: ColorScheme = .dark
|
||||||
public var tintColor: Color = Color(hex: 0xdf915e)
|
public var tintColor: Color = .init(hex: 0xDF915E)
|
||||||
public var primaryBackgroundColor: Color = Color(hex: 0x433744)
|
public var primaryBackgroundColor: Color = .init(hex: 0x433744)
|
||||||
public var secondaryBackgroundColor: Color = Color(hex:0x654868)
|
public var secondaryBackgroundColor: Color = .init(hex: 0x654868)
|
||||||
public var labelColor: Color = .white
|
public var labelColor: Color = .white
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct DesertLight: ColorSet {
|
public struct DesertLight: ColorSet {
|
||||||
public var name: ColorSetName = .desertLight
|
public var name: ColorSetName = .desertLight
|
||||||
public var scheme: ColorScheme = .light
|
public var scheme: ColorScheme = .light
|
||||||
public var tintColor: Color = Color(hex: 0xdf915e)
|
public var tintColor: Color = .init(hex: 0xDF915E)
|
||||||
public var primaryBackgroundColor: Color = Color(hex: 0xfcf2eb)
|
public var primaryBackgroundColor: Color = .init(hex: 0xFCF2EB)
|
||||||
public var secondaryBackgroundColor: Color = Color(hex:0xeeede7)
|
public var secondaryBackgroundColor: Color = .init(hex: 0xEEEDE7)
|
||||||
public var labelColor: Color = .black
|
public var labelColor: Color = .black
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct NemesisDark: ColorSet {
|
public struct NemesisDark: ColorSet {
|
||||||
public var name: ColorSetName = .nemesisDark
|
public var name: ColorSetName = .nemesisDark
|
||||||
public var scheme: ColorScheme = .dark
|
public var scheme: ColorScheme = .dark
|
||||||
public var tintColor: Color = Color(hex: 0x17a2f2)
|
public var tintColor: Color = .init(hex: 0x17A2F2)
|
||||||
public var primaryBackgroundColor: Color = Color(hex: 0x000000)
|
public var primaryBackgroundColor: Color = .init(hex: 0x000000)
|
||||||
public var secondaryBackgroundColor: Color = Color(hex:0x151e2b)
|
public var secondaryBackgroundColor: Color = .init(hex: 0x151E2B)
|
||||||
public var labelColor: Color = .white
|
public var labelColor: Color = .white
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct NemesisLight: ColorSet {
|
public struct NemesisLight: ColorSet {
|
||||||
public var name: ColorSetName = .nemesisLight
|
public var name: ColorSetName = .nemesisLight
|
||||||
public var scheme: ColorScheme = .light
|
public var scheme: ColorScheme = .light
|
||||||
public var tintColor: Color = Color(hex: 0x17a2f2)
|
public var tintColor: Color = .init(hex: 0x17A2F2)
|
||||||
public var primaryBackgroundColor: Color = Color(hex: 0xffffff)
|
public var primaryBackgroundColor: Color = .init(hex: 0xFFFFFF)
|
||||||
public var secondaryBackgroundColor: Color = Color(hex:0xe8ecef)
|
public var secondaryBackgroundColor: Color = .init(hex: 0xE8ECEF)
|
||||||
public var labelColor: Color = .black
|
public var labelColor: Color = .black
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension CGFloat {
|
public extension CGFloat {
|
||||||
public static let layoutPadding: CGFloat = 20
|
static let layoutPadding: CGFloat = 20
|
||||||
public static let dividerPadding: CGFloat = 2
|
static let dividerPadding: CGFloat = 2
|
||||||
public static let statusColumnsSpacing: CGFloat = 8
|
static let statusColumnsSpacing: CGFloat = 8
|
||||||
public static let maxColumnWidth: CGFloat = 650
|
static let maxColumnWidth: CGFloat = 650
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension Color {
|
public extension Color {
|
||||||
public static var brand: Color {
|
static var brand: Color {
|
||||||
Color(red: 187/255, green: 59/255, blue: 226/255)
|
Color(red: 187 / 255, green: 59 / 255, blue: 226 / 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var primaryBackground: Color {
|
static var primaryBackground: Color {
|
||||||
Color(red: 16/255, green: 21/255, blue: 35/255)
|
Color(red: 16 / 255, green: 21 / 255, blue: 35 / 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var secondaryBackground: Color {
|
static var secondaryBackground: Color {
|
||||||
Color(red: 30/255, green: 35/255, blue: 62/255)
|
Color(red: 30 / 255, green: 35 / 255, blue: 62 / 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var label: Color {
|
static var label: Color {
|
||||||
Color("label", bundle: .module)
|
Color("label", bundle: .module)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Color: RawRepresentable {
|
extension Color: RawRepresentable {
|
||||||
public init?(rawValue: Int) {
|
public init?(rawValue: Int) {
|
||||||
let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF
|
let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF
|
||||||
let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF
|
let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF
|
||||||
let blue = Double(rawValue & 0x0000FF) / 0xFF
|
let blue = Double(rawValue & 0x0000FF) / 0xFF
|
||||||
self = Color(red: red, green: green, blue: blue)
|
self = Color(red: red, green: green, blue: blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +42,10 @@ extension Color: RawRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
init(hex: Int, opacity: Double = 1.0) {
|
init(hex: Int, opacity: Double = 1.0) {
|
||||||
let red = Double((hex & 0xff0000) >> 16) / 255.0
|
let red = Double((hex & 0xFF0000) >> 16) / 255.0
|
||||||
let green = Double((hex & 0xff00) >> 8) / 255.0
|
let green = Double((hex & 0xFF00) >> 8) / 255.0
|
||||||
let blue = Double((hex & 0xff) >> 0) / 255.0
|
let blue = Double((hex & 0xFF) >> 0) / 255.0
|
||||||
self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
|
self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ public class Theme: ObservableObject {
|
||||||
case avatarPosition, avatarShape, statusActionsDisplay, statusDisplayStyle
|
case avatarPosition, avatarShape, statusActionsDisplay, statusDisplayStyle
|
||||||
case selectedSet, selectedScheme
|
case selectedSet, selectedScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AvatarPosition: String, CaseIterable {
|
public enum AvatarPosition: String, CaseIterable {
|
||||||
case leading, top
|
case leading, top
|
||||||
|
|
||||||
public var description: LocalizedStringKey {
|
public var description: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .leading:
|
case .leading:
|
||||||
|
@ -33,7 +33,7 @@ public class Theme: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StatusActionsDisplay: String, CaseIterable {
|
public enum StatusActionsDisplay: String, CaseIterable {
|
||||||
case full, discret, none
|
case full, discret, none
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ public class Theme: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StatusDisplayStyle: String, CaseIterable {
|
public enum StatusDisplayStyle: String, CaseIterable {
|
||||||
case large, compact
|
case large, compact
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public class Theme: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AppStorage("is_previously_set") private var isSet: Bool = false
|
@AppStorage("is_previously_set") private var isSet: Bool = false
|
||||||
@AppStorage(ThemeKey.selectedScheme.rawValue) public var selectedScheme: ColorScheme = .dark
|
@AppStorage(ThemeKey.selectedScheme.rawValue) public var selectedScheme: ColorScheme = .dark
|
||||||
@AppStorage(ThemeKey.tint.rawValue) public var tintColor: Color = .black
|
@AppStorage(ThemeKey.tint.rawValue) public var tintColor: Color = .black
|
||||||
|
@ -79,19 +79,19 @@ public class Theme: ObservableObject {
|
||||||
@Published public var selectedSet: ColorSetName = .iceCubeDark
|
@Published public var selectedSet: ColorSetName = .iceCubeDark
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public static let shared = Theme()
|
public static let shared = Theme()
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
selectedSet = storedSet
|
selectedSet = storedSet
|
||||||
|
|
||||||
// If theme is never set before set the default store. This should only execute once after install.
|
// If theme is never set before set the default store. This should only execute once after install.
|
||||||
|
|
||||||
if !isSet {
|
if !isSet {
|
||||||
setColor(withName: .iceCubeDark)
|
setColor(withName: .iceCubeDark)
|
||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarPosition = AvatarPosition(rawValue: rawAvatarPosition) ?? .top
|
avatarPosition = AvatarPosition(rawValue: rawAvatarPosition) ?? .top
|
||||||
avatarShape = AvatarShape(rawValue: rawAvatarShape) ?? .rounded
|
avatarShape = AvatarShape(rawValue: rawAvatarShape) ?? .rounded
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ public class Theme: ObservableObject {
|
||||||
self?.rawAvatarShape = shape
|
self?.rawAvatarShape = shape
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// Workaround, since @AppStorage can't be directly observed
|
// Workaround, since @AppStorage can't be directly observed
|
||||||
$selectedSet
|
$selectedSet
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
|
@ -119,7 +119,7 @@ public class Theme: ObservableObject {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var allColorSet: [ColorSet] {
|
public static var allColorSet: [ColorSet] {
|
||||||
[
|
[
|
||||||
IceCubeDark(),
|
IceCubeDark(),
|
||||||
|
@ -127,17 +127,17 @@ public class Theme: ObservableObject {
|
||||||
DesertDark(),
|
DesertDark(),
|
||||||
DesertLight(),
|
DesertLight(),
|
||||||
NemesisDark(),
|
NemesisDark(),
|
||||||
NemesisLight()
|
NemesisLight(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setColor(withName name: ColorSetName) {
|
public func setColor(withName name: ColorSetName) {
|
||||||
let colorSet = Theme.allColorSet.filter { $0.name == name }.first ?? IceCubeDark()
|
let colorSet = Theme.allColorSet.filter { $0.name == name }.first ?? IceCubeDark()
|
||||||
self.selectedScheme = colorSet.scheme
|
selectedScheme = colorSet.scheme
|
||||||
self.tintColor = colorSet.tintColor
|
tintColor = colorSet.tintColor
|
||||||
self.primaryBackgroundColor = colorSet.primaryBackgroundColor
|
primaryBackgroundColor = colorSet.primaryBackgroundColor
|
||||||
self.secondaryBackgroundColor = colorSet.secondaryBackgroundColor
|
secondaryBackgroundColor = colorSet.secondaryBackgroundColor
|
||||||
self.labelColor = colorSet.labelColor
|
labelColor = colorSet.labelColor
|
||||||
self.storedSet = name
|
storedSet = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,68 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func applyTheme(_ theme: Theme) -> some View {
|
func applyTheme(_ theme: Theme) -> some View {
|
||||||
modifier(ThemeApplier(theme: theme))
|
modifier(ThemeApplier(theme: theme))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeApplier: ViewModifier {
|
struct ThemeApplier: ViewModifier {
|
||||||
@ObservedObject var theme: Theme
|
@ObservedObject var theme: Theme
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.tint(theme.tintColor)
|
.tint(theme.tintColor)
|
||||||
.preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light)
|
.preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light)
|
||||||
#if canImport(UIKit)
|
|
||||||
.onAppear {
|
|
||||||
setWindowTint(theme.tintColor)
|
|
||||||
setWindowUserInterfaceStyle(theme.selectedScheme)
|
|
||||||
setBarsColor(theme.primaryBackgroundColor)
|
|
||||||
}
|
|
||||||
.onChange(of: theme.tintColor) { newValue in
|
|
||||||
setWindowTint(newValue)
|
|
||||||
}
|
|
||||||
.onChange(of: theme.selectedScheme) { newValue in
|
|
||||||
setWindowUserInterfaceStyle(newValue)
|
|
||||||
}
|
|
||||||
.onChange(of: theme.primaryBackgroundColor) { newValue in
|
|
||||||
setBarsColor(newValue)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
private func setWindowUserInterfaceStyle(_ colorScheme: ColorScheme) {
|
.onAppear {
|
||||||
allWindows()
|
setWindowTint(theme.tintColor)
|
||||||
.forEach {
|
setWindowUserInterfaceStyle(theme.selectedScheme)
|
||||||
switch colorScheme {
|
setBarsColor(theme.primaryBackgroundColor)
|
||||||
case .dark:
|
}
|
||||||
$0.overrideUserInterfaceStyle = .dark
|
.onChange(of: theme.tintColor) { newValue in
|
||||||
case .light:
|
setWindowTint(newValue)
|
||||||
$0.overrideUserInterfaceStyle = .light
|
}
|
||||||
}
|
.onChange(of: theme.selectedScheme) { newValue in
|
||||||
}
|
setWindowUserInterfaceStyle(newValue)
|
||||||
}
|
}
|
||||||
|
.onChange(of: theme.primaryBackgroundColor) { newValue in
|
||||||
private func setWindowTint(_ color: Color) {
|
setBarsColor(newValue)
|
||||||
allWindows()
|
}
|
||||||
.forEach {
|
|
||||||
$0.tintColor = UIColor(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setBarsColor(_ color: Color) {
|
|
||||||
UINavigationBar.appearance().isTranslucent = true
|
|
||||||
UINavigationBar.appearance().barTintColor = UIColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func allWindows() -> [UIWindow] {
|
|
||||||
UIApplication.shared.connectedScenes
|
|
||||||
.compactMap { $0 as? UIWindowScene }
|
|
||||||
.flatMap { $0.windows }
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(UIKit)
|
||||||
|
private func setWindowUserInterfaceStyle(_ colorScheme: ColorScheme) {
|
||||||
|
allWindows()
|
||||||
|
.forEach {
|
||||||
|
switch colorScheme {
|
||||||
|
case .dark:
|
||||||
|
$0.overrideUserInterfaceStyle = .dark
|
||||||
|
case .light:
|
||||||
|
$0.overrideUserInterfaceStyle = .light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setWindowTint(_ color: Color) {
|
||||||
|
allWindows()
|
||||||
|
.forEach {
|
||||||
|
$0.tintColor = UIColor(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setBarsColor(_ color: Color) {
|
||||||
|
UINavigationBar.appearance().isTranslucent = true
|
||||||
|
UINavigationBar.appearance().barTintColor = UIColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func allWindows() -> [UIWindow] {
|
||||||
|
UIApplication.shared.connectedScenes
|
||||||
|
.compactMap { $0 as? UIWindowScene }
|
||||||
|
.flatMap { $0.windows }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import SwiftUI
|
|
||||||
import Shimmer
|
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import Shimmer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AvatarView: View {
|
public struct AvatarView: View {
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
@ -8,7 +8,7 @@ public struct AvatarView: View {
|
||||||
|
|
||||||
public enum Size {
|
public enum Size {
|
||||||
case account, status, embed, badge, boost
|
case account, status, embed, badge, boost
|
||||||
|
|
||||||
public var size: CGSize {
|
public var size: CGSize {
|
||||||
switch self {
|
switch self {
|
||||||
case .account:
|
case .account:
|
||||||
|
@ -23,7 +23,7 @@ public struct AvatarView: View {
|
||||||
return .init(width: 12, height: 12)
|
return .init(width: 12, height: 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cornerRadius: CGFloat {
|
var cornerRadius: CGFloat {
|
||||||
switch self {
|
switch self {
|
||||||
case .badge, .boost:
|
case .badge, .boost:
|
||||||
|
@ -33,36 +33,36 @@ public struct AvatarView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let size: Size
|
public let size: Size
|
||||||
|
|
||||||
public init(url: URL, size: Size = .status) {
|
public init(url: URL, size: Size = .status) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.size = size
|
self.size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if reasons == .placeholder {
|
if reasons == .placeholder {
|
||||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
RoundedRectangle(cornerRadius: size.cornerRadius)
|
||||||
.fill(.gray)
|
.fill(.gray)
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
.frame(width: size.size.width, height: size.size.height)
|
||||||
} else {
|
} else {
|
||||||
LazyImage(url: url) { state in
|
LazyImage(url: url) { state in
|
||||||
if let image = state.image {
|
if let image = state.image {
|
||||||
image
|
image
|
||||||
.resizingMode(.aspectFit)
|
.resizingMode(.aspectFit)
|
||||||
} else if state.isLoading {
|
} else if state.isLoading {
|
||||||
placeholderView
|
placeholderView
|
||||||
.shimmering()
|
.shimmering()
|
||||||
} else {
|
} else {
|
||||||
placeholderView
|
placeholderView
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.processors([.resize(size: size.size), .roundedCorners(radius: size.cornerRadius)])
|
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
|
||||||
}
|
}
|
||||||
|
.processors([.resize(size: size.size), .roundedCorners(radius: size.cornerRadius)])
|
||||||
|
.frame(width: size.size.width, height: size.size.height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.clipShape(clipShape)
|
.clipShape(clipShape)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
@ -78,7 +78,7 @@ public struct AvatarView: View {
|
||||||
return AnyShape(RoundedRectangle(cornerRadius: size.cornerRadius))
|
return AnyShape(RoundedRectangle(cornerRadius: size.cornerRadius))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var placeholderView: some View {
|
private var placeholderView: some View {
|
||||||
if size == .badge {
|
if size == .badge {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import Foundation
|
|
||||||
import EmojiText
|
import EmojiText
|
||||||
import Models
|
import Foundation
|
||||||
import HTML2Markdown
|
import HTML2Markdown
|
||||||
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct EmojiTextApp: View {
|
public struct EmojiTextApp: View {
|
||||||
private let markdown: String
|
private let markdown: String
|
||||||
private let emojis: [any CustomEmoji]
|
private let emojis: [any CustomEmoji]
|
||||||
private let append: (() -> Text)?
|
private let append: (() -> Text)?
|
||||||
|
|
||||||
public init(_ markdown: HTMLString, emojis: [Emoji], append: (() -> Text)? = nil) {
|
public init(_ markdown: HTMLString, emojis: [Emoji], append: (() -> Text)? = nil) {
|
||||||
self.markdown = markdown
|
self.markdown = markdown
|
||||||
self.emojis = emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) }
|
self.emojis = emojis.map { RemoteEmoji(shortcode: $0.shortcode, url: $0.url) }
|
||||||
self.append = append
|
self.append = append
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if let append {
|
if let append {
|
||||||
EmojiText(markdown: markdown, emojis: emojis)
|
EmojiText(markdown: markdown, emojis: emojis)
|
||||||
|
|
|
@ -4,13 +4,13 @@ public struct EmptyView: View {
|
||||||
public let iconName: String
|
public let iconName: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let message: String
|
public let message: String
|
||||||
|
|
||||||
public init(iconName: String, title: String, message: String) {
|
public init(iconName: String, title: String, message: String) {
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.title = title
|
self.title = title
|
||||||
self.message = message
|
self.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: iconName)
|
Image(systemName: iconName)
|
||||||
|
|
|
@ -4,15 +4,15 @@ public struct ErrorView: View {
|
||||||
public let title: String
|
public let title: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let buttonTitle: String
|
public let buttonTitle: String
|
||||||
public let onButtonPress: (() -> Void)
|
public let onButtonPress: () -> Void
|
||||||
|
|
||||||
public init(title: String, message: String, buttonTitle: String, onButtonPress: @escaping (() -> Void)) {
|
public init(title: String, message: String, buttonTitle: String, onButtonPress: @escaping (() -> Void)) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.message = message
|
self.message = message
|
||||||
self.buttonTitle = buttonTitle
|
self.buttonTitle = buttonTitle
|
||||||
self.onButtonPress = onButtonPress
|
self.onButtonPress = onButtonPress
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
|
|
@ -15,7 +15,7 @@ public struct ScrollViewOffsetReader<Content: View>: View {
|
||||||
self.onOffsetChange = onOffsetChange
|
self.onOffsetChange = onOffsetChange
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
offsetReader
|
offsetReader
|
||||||
|
@ -40,5 +40,5 @@ public struct ScrollViewOffsetReader<Content: View>: View {
|
||||||
|
|
||||||
private struct OffsetPreferenceKey: PreferenceKey {
|
private struct OffsetPreferenceKey: PreferenceKey {
|
||||||
static var defaultValue: CGFloat = .zero
|
static var defaultValue: CGFloat = .zero
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
|
static func reduce(value _: inout CGFloat, nextValue _: () -> CGFloat) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
public extension View {
|
||||||
public func statusEditorToolbarItem(routeurPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent {
|
func statusEditorToolbarItem(routeurPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
routeurPath.presentedSheet = .newStatusEditor(visibility: visibility)
|
routeurPath.presentedSheet = .newStatusEditor(visibility: visibility)
|
||||||
|
@ -18,11 +18,11 @@ extension View {
|
||||||
public struct StatusEditorToolbarItem: ToolbarContent {
|
public struct StatusEditorToolbarItem: ToolbarContent {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
let visibility: Models.Visibility
|
let visibility: Models.Visibility
|
||||||
|
|
||||||
public init(visibility: Models.Visibility) {
|
public init(visibility: Models.Visibility) {
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some ToolbarContent {
|
public var body: some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
public struct TagRowView: View {
|
public struct TagRowView: View {
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
let tag: Tag
|
let tag: Tag
|
||||||
|
|
||||||
public init(tag: Tag) {
|
public init(tag: Tag) {
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ThemePreviewView: View {
|
public struct ThemePreviewView: View {
|
||||||
private let gutterSpace: Double = 8
|
private let gutterSpace: Double = 8
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
HStack (spacing: gutterSpace) {
|
HStack(spacing: gutterSpace) {
|
||||||
ThemeBoxView(color: IceCubeDark())
|
ThemeBoxView(color: IceCubeDark())
|
||||||
ThemeBoxView(color: IceCubeLight())
|
ThemeBoxView(color: IceCubeLight())
|
||||||
}
|
}
|
||||||
HStack (spacing: gutterSpace) {
|
HStack(spacing: gutterSpace) {
|
||||||
ThemeBoxView(color: DesertDark())
|
ThemeBoxView(color: DesertDark())
|
||||||
ThemeBoxView(color: DesertLight())
|
ThemeBoxView(color: DesertLight())
|
||||||
}
|
}
|
||||||
HStack (spacing: gutterSpace) {
|
HStack(spacing: gutterSpace) {
|
||||||
ThemeBoxView(color: NemesisDark())
|
ThemeBoxView(color: NemesisDark())
|
||||||
ThemeBoxView(color: NemesisLight())
|
ThemeBoxView(color: NemesisLight())
|
||||||
}
|
}
|
||||||
|
@ -31,13 +31,12 @@ public struct ThemePreviewView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeBoxView: View {
|
struct ThemeBoxView: View {
|
||||||
|
|
||||||
@EnvironmentObject var theme: Theme
|
@EnvironmentObject var theme: Theme
|
||||||
private let gutterSpace = 8.0
|
private let gutterSpace = 8.0
|
||||||
@State private var isSelected = false
|
@State private var isSelected = false
|
||||||
|
|
||||||
var color: ColorSet
|
var color: ColorSet
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
@ -45,19 +44,19 @@ struct ThemeBoxView: View {
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
.shadow(radius: 2, x: 2, y: 4)
|
.shadow(radius: 2, x: 2, y: 4)
|
||||||
|
|
||||||
VStack (spacing: gutterSpace) {
|
VStack(spacing: gutterSpace) {
|
||||||
Text(color.name.rawValue)
|
Text(color.name.rawValue)
|
||||||
.foregroundColor(color.tintColor)
|
.foregroundColor(color.tintColor)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Toots preview")
|
Text("Toots preview")
|
||||||
.foregroundColor(color.labelColor)
|
.foregroundColor(color.labelColor)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
.background(color.primaryBackgroundColor)
|
.background(color.primaryBackgroundColor)
|
||||||
|
|
||||||
Text("#icecube, #techhub")
|
Text("#icecube, #techhub")
|
||||||
.foregroundColor(color.tintColor)
|
.foregroundColor(color.tintColor)
|
||||||
if isSelected {
|
if isSelected {
|
||||||
|
@ -95,4 +94,3 @@ struct ThemeBoxView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,12 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Env",
|
name: "Env",
|
||||||
targets: ["Env"]),
|
targets: ["Env"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
.package(name: "Network", path: "../Network")
|
.package(name: "Network", path: "../Network"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -23,6 +24,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Network", package: "Network"),
|
.product(name: "Network", package: "Network"),
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,21 +7,21 @@ public class CurrentAccount: ObservableObject {
|
||||||
@Published public private(set) var account: Account?
|
@Published public private(set) var account: Account?
|
||||||
@Published public private(set) var lists: [List] = []
|
@Published public private(set) var lists: [List] = []
|
||||||
@Published public private(set) var tags: [Tag] = []
|
@Published public private(set) var tags: [Tag] = []
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
static public let shared = CurrentAccount()
|
public static let shared = CurrentAccount()
|
||||||
|
|
||||||
private init() { }
|
private init() {}
|
||||||
|
|
||||||
public func setClient(client: Client) {
|
public func setClient(client: Client) {
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
Task(priority: .userInitiated) {
|
Task(priority: .userInitiated) {
|
||||||
await fetchUserData()
|
await fetchUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchUserData() async {
|
private func fetchUserData() async {
|
||||||
await withTaskGroup(of: Void.self) { group in
|
await withTaskGroup(of: Void.self) { group in
|
||||||
group.addTask { await self.fetchConnections() }
|
group.addTask { await self.fetchConnections() }
|
||||||
|
@ -30,15 +30,15 @@ public class CurrentAccount: ObservableObject {
|
||||||
group.addTask { await self.fetchFollowedTags() }
|
group.addTask { await self.fetchFollowedTags() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchConnections() async {
|
public func fetchConnections() async {
|
||||||
guard let client = client else { return }
|
guard let client = client else { return }
|
||||||
do {
|
do {
|
||||||
let connections: [String] = try await client.get(endpoint: Instances.peers)
|
let connections: [String] = try await client.get(endpoint: Instances.peers)
|
||||||
client.addConnections(connections)
|
client.addConnections(connections)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchCurrentAccount() async {
|
public func fetchCurrentAccount() async {
|
||||||
guard let client = client, client.isAuth else {
|
guard let client = client, client.isAuth else {
|
||||||
account = nil
|
account = nil
|
||||||
|
@ -46,7 +46,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
}
|
}
|
||||||
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchLists() async {
|
public func fetchLists() async {
|
||||||
guard let client, client.isAuth else { return }
|
guard let client, client.isAuth else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -55,7 +55,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
lists = []
|
lists = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchFollowedTags() async {
|
public func fetchFollowedTags() async {
|
||||||
guard let client, client.isAuth else { return }
|
guard let client, client.isAuth else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -64,15 +64,15 @@ public class CurrentAccount: ObservableObject {
|
||||||
tags = []
|
tags = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createList(title: String) async {
|
public func createList(title: String) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let list: Models.List = try await client.post(endpoint: Lists.createList(title: title))
|
let list: Models.List = try await client.post(endpoint: Lists.createList(title: title))
|
||||||
lists.append(list)
|
lists.append(list)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteList(list: Models.List) async {
|
public func deleteList(list: Models.List) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
lists.removeAll(where: { $0.id == list.id })
|
lists.removeAll(where: { $0.id == list.id })
|
||||||
|
@ -81,7 +81,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
lists.append(list)
|
lists.append(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func followTag(id: String) async -> Tag? {
|
public func followTag(id: String) async -> Tag? {
|
||||||
guard let client else { return nil }
|
guard let client else { return nil }
|
||||||
do {
|
do {
|
||||||
|
@ -92,12 +92,12 @@ public class CurrentAccount: ObservableObject {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func unfollowTag(id: String) async -> Tag? {
|
public func unfollowTag(id: String) async -> Tag? {
|
||||||
guard let client else { return nil }
|
guard let client else { return nil }
|
||||||
do {
|
do {
|
||||||
let tag: Tag = try await client.post(endpoint: Tags.unfollow(id: id))
|
let tag: Tag = try await client.post(endpoint: Tags.unfollow(id: id))
|
||||||
tags.removeAll{ $0.id == tag.id }
|
tags.removeAll { $0.id == tag.id }
|
||||||
return tag
|
return tag
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -5,20 +5,20 @@ import Network
|
||||||
@MainActor
|
@MainActor
|
||||||
public class CurrentInstance: ObservableObject {
|
public class CurrentInstance: ObservableObject {
|
||||||
@Published public private(set) var instance: Instance?
|
@Published public private(set) var instance: Instance?
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
static public let shared = CurrentInstance()
|
public static let shared = CurrentInstance()
|
||||||
|
|
||||||
private init() { }
|
private init() {}
|
||||||
|
|
||||||
public func setClient(client: Client) {
|
public func setClient(client: Client) {
|
||||||
self.client = client
|
self.client = client
|
||||||
Task {
|
Task {
|
||||||
await fetchCurrentInstance()
|
await fetchCurrentInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchCurrentInstance() async {
|
public func fetchCurrentInstance() async {
|
||||||
guard let client = client else { return }
|
guard let client = client else { return }
|
||||||
Task {
|
Task {
|
||||||
|
|
|
@ -9,7 +9,7 @@ extension Array: RawRepresentable where Element: Codable {
|
||||||
}
|
}
|
||||||
self = result
|
self = result
|
||||||
}
|
}
|
||||||
|
|
||||||
public var rawValue: String {
|
public var rawValue: String {
|
||||||
guard let data = try? JSONEncoder().encode(self),
|
guard let data = try? JSONEncoder().encode(self),
|
||||||
let result = String(data: data, encoding: .utf8)
|
let result = String(data: data, encoding: .utf8)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum PreferredBrowser: Int, CaseIterable {
|
public enum PreferredBrowser: Int, CaseIterable {
|
||||||
case inAppSafari
|
case inAppSafari
|
||||||
case safari
|
case safari
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Foundation
|
|
||||||
import UserNotifications
|
|
||||||
import SwiftUI
|
|
||||||
import KeychainSwift
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import KeychainSwift
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class PushNotificationsService: ObservableObject {
|
public class PushNotificationsService: ObservableObject {
|
||||||
|
@ -13,21 +13,21 @@ public class PushNotificationsService: ObservableObject {
|
||||||
static let keychainAuthKey = "notifications_auth_key"
|
static let keychainAuthKey = "notifications_auth_key"
|
||||||
static let keychainPrivateKey = "notifications_private_key"
|
static let keychainPrivateKey = "notifications_private_key"
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PushAccounts {
|
public struct PushAccounts {
|
||||||
public let server: String
|
public let server: String
|
||||||
public let token: OauthToken
|
public let token: OauthToken
|
||||||
|
|
||||||
public init(server: String, token: OauthToken) {
|
public init(server: String, token: OauthToken) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.token = token
|
self.token = token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static let shared = PushNotificationsService()
|
public static let shared = PushNotificationsService()
|
||||||
|
|
||||||
@Published public var pushToken: Data?
|
@Published public var pushToken: Data?
|
||||||
|
|
||||||
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
|
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
|
||||||
@Published public var isPushEnabled: Bool = false {
|
@Published public var isPushEnabled: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -36,15 +36,16 @@ public class PushNotificationsService: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published public var isFollowNotificationEnabled: Bool = true
|
@Published public var isFollowNotificationEnabled: Bool = true
|
||||||
@Published public var isFavoriteNotificationEnabled: Bool = true
|
@Published public var isFavoriteNotificationEnabled: Bool = true
|
||||||
@Published public var isReblogNotificationEnabled: Bool = true
|
@Published public var isReblogNotificationEnabled: Bool = true
|
||||||
@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
|
||||||
|
|
||||||
private var subscriptions: [PushSubscription] = []
|
private var subscriptions: [PushSubscription] = []
|
||||||
|
|
||||||
private var keychain: KeychainSwift {
|
private var keychain: KeychainSwift {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
|
@ -52,15 +53,15 @@ public class PushNotificationsService: ObservableObject {
|
||||||
#endif
|
#endif
|
||||||
return keychain
|
return keychain
|
||||||
}
|
}
|
||||||
|
|
||||||
public func requestPushNotifications() {
|
public func requestPushNotifications() {
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (_, _) in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchSubscriptions(accounts: [PushAccounts]) async {
|
public func fetchSubscriptions(accounts: [PushAccounts]) async {
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
|
@ -68,11 +69,11 @@ public class PushNotificationsService: ObservableObject {
|
||||||
do {
|
do {
|
||||||
let sub: PushSubscription = try await client.get(endpoint: Push.subscription)
|
let sub: PushSubscription = try await client.get(endpoint: Push.subscription)
|
||||||
subscriptions.append(sub)
|
subscriptions.append(sub)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
refreshSubscriptionsUI()
|
refreshSubscriptionsUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateSubscriptions(accounts: [PushAccounts]) async {
|
public func updateSubscriptions(accounts: [PushAccounts]) async {
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
let key = notificationsPrivateKeyAsKey.publicKey.x963Representation
|
let key = notificationsPrivateKeyAsKey.publicKey.x963Representation
|
||||||
|
@ -80,15 +81,15 @@ public class PushNotificationsService: ObservableObject {
|
||||||
guard let pushToken = pushToken, isUserPushEnabled else { return }
|
guard let pushToken = pushToken, isUserPushEnabled else { return }
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
let client = Client(server: account.server, oauthToken: account.token)
|
let client = Client(server: account.server, oauthToken: account.token)
|
||||||
do {
|
do {
|
||||||
var listenerURL = Constants.endpoint
|
var listenerURL = Constants.endpoint
|
||||||
listenerURL += "/push/"
|
listenerURL += "/push/"
|
||||||
listenerURL += pushToken.hexString
|
listenerURL += pushToken.hexString
|
||||||
listenerURL += "/\(account.server)"
|
listenerURL += "/\(account.server)"
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
listenerURL += "?sandbox=true"
|
listenerURL += "?sandbox=true"
|
||||||
#endif
|
#endif
|
||||||
let sub: PushSubscription =
|
let sub: PushSubscription =
|
||||||
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,
|
||||||
|
@ -98,23 +99,23 @@ public class PushNotificationsService: ObservableObject {
|
||||||
follow: isFollowNotificationEnabled,
|
follow: isFollowNotificationEnabled,
|
||||||
favourite: isFavoriteNotificationEnabled,
|
favourite: isFavoriteNotificationEnabled,
|
||||||
poll: isPollNotificationEnabled))
|
poll: isPollNotificationEnabled))
|
||||||
subscriptions.append(sub)
|
subscriptions.append(sub)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
refreshSubscriptionsUI()
|
refreshSubscriptionsUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteSubscriptions(accounts: [PushAccounts]) async {
|
public func deleteSubscriptions(accounts: [PushAccounts]) async {
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
let client = Client(server: account.server, oauthToken: account.token)
|
let client = Client(server: account.server, oauthToken: account.token)
|
||||||
do {
|
do {
|
||||||
_ = try await client.delete(endpoint: Push.subscription)
|
_ = try await client.delete(endpoint: Push.subscription)
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
await fetchSubscriptions(accounts: accounts)
|
await fetchSubscriptions(accounts: accounts)
|
||||||
refreshSubscriptionsUI()
|
refreshSubscriptionsUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshSubscriptionsUI() {
|
private func refreshSubscriptionsUI() {
|
||||||
if let sub = subscriptions.first {
|
if let sub = subscriptions.first {
|
||||||
isPushEnabled = true
|
isPushEnabled = true
|
||||||
|
@ -128,12 +129,13 @@ public class PushNotificationsService: ObservableObject {
|
||||||
isPushEnabled = false
|
isPushEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Key management
|
// MARK: - Key management
|
||||||
|
|
||||||
public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey {
|
public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey {
|
||||||
if let key = keychain.get(Constants.keychainPrivateKey),
|
if let key = keychain.get(Constants.keychainPrivateKey),
|
||||||
let data = Data(base64Encoded: key) {
|
let data = Data(base64Encoded: key)
|
||||||
|
{
|
||||||
do {
|
do {
|
||||||
return try P256.KeyAgreement.PrivateKey(rawRepresentation: data)
|
return try P256.KeyAgreement.PrivateKey(rawRepresentation: data)
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -151,10 +153,11 @@ public class PushNotificationsService: ObservableObject {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var notificationsAuthKeyAsKey: Data {
|
public var notificationsAuthKeyAsKey: Data {
|
||||||
if let key = keychain.get(Constants.keychainAuthKey),
|
if let key = keychain.get(Constants.keychainAuthKey),
|
||||||
let data = Data(base64Encoded: key) {
|
let data = Data(base64Encoded: key)
|
||||||
|
{
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
let key = Self.makeRandomeNotificationsAuthKey()
|
let key = Self.makeRandomeNotificationsAuthKey()
|
||||||
|
@ -164,8 +167,8 @@ public class PushNotificationsService: ObservableObject {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static private func makeRandomeNotificationsAuthKey() -> Data {
|
private static func makeRandomeNotificationsAuthKey() -> Data {
|
||||||
let byteCount = 16
|
let byteCount = 16
|
||||||
var bytes = Data(count: byteCount)
|
var bytes = Data(count: byteCount)
|
||||||
_ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) }
|
_ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) }
|
||||||
|
@ -178,4 +181,3 @@ extension Data {
|
||||||
return map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
|
return map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,9 @@ public class QuickLook: ObservableObject {
|
||||||
@Published public private(set) var urls: [URL] = []
|
@Published public private(set) var urls: [URL] = []
|
||||||
@Published public private(set) var isPreparing: Bool = false
|
@Published public private(set) var isPreparing: Bool = false
|
||||||
@Published public private(set) var latestError: Error?
|
@Published public private(set) var latestError: Error?
|
||||||
|
|
||||||
public init() {
|
public init() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareFor(urls: [URL], selectedURL: URL) async {
|
public func prepareFor(urls: [URL], selectedURL: URL) async {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isPreparing = true
|
isPreparing = true
|
||||||
|
@ -43,7 +41,7 @@ public class QuickLook: ObservableObject {
|
||||||
latestError = error
|
latestError = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func localPathFor(url: URL) async throws -> URL {
|
private func localPathFor(url: URL) async throws -> URL {
|
||||||
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||||
let path = tempDir.appendingPathComponent(url.lastPathComponent)
|
let path = tempDir.appendingPathComponent(url.lastPathComponent)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public enum RouteurDestinations: Hashable {
|
public enum RouteurDestinations: Hashable {
|
||||||
case accountDetail(id: String)
|
case accountDetail(id: String)
|
||||||
|
@ -26,7 +26,7 @@ public enum SheetDestinations: Identifiable {
|
||||||
case listAddAccount(account: Account)
|
case listAddAccount(account: Account)
|
||||||
case addAccount
|
case addAccount
|
||||||
case addRemoteLocalTimeline
|
case addRemoteLocalTimeline
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, .mentionStatusEditor:
|
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, .mentionStatusEditor:
|
||||||
|
@ -47,19 +47,20 @@ public enum SheetDestinations: Identifiable {
|
||||||
public class RouterPath: ObservableObject {
|
public class RouterPath: ObservableObject {
|
||||||
public var client: Client?
|
public var client: Client?
|
||||||
public var urlHandler: ((URL) -> OpenURLAction.Result)?
|
public var urlHandler: ((URL) -> OpenURLAction.Result)?
|
||||||
|
|
||||||
@Published public var path: [RouteurDestinations] = []
|
@Published public var path: [RouteurDestinations] = []
|
||||||
@Published public var presentedSheet: SheetDestinations?
|
@Published public var presentedSheet: SheetDestinations?
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func navigate(to: RouteurDestinations) {
|
public func navigate(to: RouteurDestinations) {
|
||||||
path.append(to)
|
path.append(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
|
public func handleStatus(status: AnyStatus, url: URL) -> OpenURLAction.Result {
|
||||||
if url.pathComponents.contains(where: { $0 == "tags" }),
|
if url.pathComponents.contains(where: { $0 == "tags" }),
|
||||||
let tag = url.pathComponents.last {
|
let tag = url.pathComponents.last
|
||||||
|
{
|
||||||
navigate(to: .hashTag(tag: tag, account: nil))
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
return .handled
|
return .handled
|
||||||
} else if let mention = status.mentions.first(where: { $0.url == url }) {
|
} else if let mention = status.mentions.first(where: { $0.url == url }) {
|
||||||
|
@ -68,7 +69,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 {
|
||||||
|
@ -78,10 +80,11 @@ public class RouterPath: ObservableObject {
|
||||||
}
|
}
|
||||||
return urlHandler?(url) ?? .systemAction
|
return urlHandler?(url) ?? .systemAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(url: URL) -> OpenURLAction.Result {
|
public func handle(url: URL) -> OpenURLAction.Result {
|
||||||
if url.pathComponents.contains(where: { $0 == "tags" }),
|
if url.pathComponents.contains(where: { $0 == "tags" }),
|
||||||
let tag = url.pathComponents.last {
|
let tag = url.pathComponents.last
|
||||||
|
{
|
||||||
navigate(to: .hashTag(tag: tag, account: nil))
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
return .handled
|
return .handled
|
||||||
} else if url.lastPathComponent.first == "@", let host = url.host {
|
} else if url.lastPathComponent.first == "@", let host = url.host {
|
||||||
|
@ -93,7 +96,7 @@ public class RouterPath: ObservableObject {
|
||||||
}
|
}
|
||||||
return urlHandler?(url) ?? .systemAction
|
return urlHandler?(url) ?? .systemAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToAccountFrom(acct: String, url: URL) async {
|
public func navigateToAccountFrom(acct: String, url: URL) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
Task {
|
Task {
|
||||||
|
@ -109,7 +112,7 @@ public class RouterPath: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToAccountFrom(url: URL) async {
|
public func navigateToAccountFrom(url: URL) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
Task {
|
Task {
|
||||||
|
|
|
@ -3,28 +3,28 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class StreamWatcher: ObservableObject {
|
public class StreamWatcher: ObservableObject {
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
private var task: URLSessionWebSocketTask?
|
private var task: URLSessionWebSocketTask?
|
||||||
private var watchedStreams: [Stream] = []
|
private var watchedStreams: [Stream] = []
|
||||||
|
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
public enum Stream: String {
|
public enum Stream: String {
|
||||||
case publicTimeline = "public"
|
case publicTimeline = "public"
|
||||||
case user
|
case user
|
||||||
case direct
|
case direct
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published public var events: [any StreamEvent] = []
|
@Published public var events: [any StreamEvent] = []
|
||||||
@Published public var unreadNotificationsCount: Int = 0
|
@Published public var unreadNotificationsCount: Int = 0
|
||||||
@Published public var latestEvent: (any StreamEvent)?
|
@Published public var latestEvent: (any StreamEvent)?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setClient(client: Client) {
|
public func setClient(client: Client) {
|
||||||
if self.client != nil {
|
if self.client != nil {
|
||||||
stopWatching()
|
stopWatching()
|
||||||
|
@ -32,13 +32,13 @@ public class StreamWatcher: ObservableObject {
|
||||||
self.client = client
|
self.client = client
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connect() {
|
private func connect() {
|
||||||
task = client?.makeWebSocketTask(endpoint: Streaming.streaming)
|
task = client?.makeWebSocketTask(endpoint: Streaming.streaming)
|
||||||
task?.resume()
|
task?.resume()
|
||||||
receiveMessage()
|
receiveMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func watch(streams: [Stream]) {
|
public func watch(streams: [Stream]) {
|
||||||
if client?.isAuth == false {
|
if client?.isAuth == false {
|
||||||
return
|
return
|
||||||
|
@ -51,17 +51,17 @@ public class StreamWatcher: ObservableObject {
|
||||||
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
|
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stopWatching() {
|
public func stopWatching() {
|
||||||
task?.cancel()
|
task?.cancel()
|
||||||
task = nil
|
task = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendMessage(message: StreamMessage) {
|
private func sendMessage(message: StreamMessage) {
|
||||||
task?.send(.data(try! encoder.encode(message)),
|
task?.send(.data(try! encoder.encode(message)),
|
||||||
completionHandler: { _ in })
|
completionHandler: { _ in })
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receiveMessage() {
|
private func receiveMessage() {
|
||||||
task?.receive(completionHandler: { result in
|
task?.receive(completionHandler: { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -86,13 +86,13 @@ public class StreamWatcher: ObservableObject {
|
||||||
} catch {
|
} catch {
|
||||||
print("Error decoding streaming event: \(error.localizedDescription)")
|
print("Error decoding streaming event: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
self.receiveMessage()
|
self.receiveMessage()
|
||||||
|
|
||||||
case .failure:
|
case .failure:
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -103,7 +103,7 @@ public class StreamWatcher: ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rawEventToEvent(rawEvent: RawStreamEvent) -> (any StreamEvent)? {
|
private func rawEventToEvent(rawEvent: RawStreamEvent) -> (any StreamEvent)? {
|
||||||
guard let payloadData = rawEvent.payload.data(using: .utf8) else {
|
guard let payloadData = rawEvent.payload.data(using: .utf8) else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import SwiftUI
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class UserPreferences: ObservableObject {
|
public class UserPreferences: ObservableObject {
|
||||||
public static let sharedDefault = UserDefaults.init(suiteName: "group.icecubesapps")
|
public static let sharedDefault = UserDefaults(suiteName: "group.icecubesapps")
|
||||||
public static let shared = UserPreferences()
|
public static let shared = UserPreferences()
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
|
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
|
||||||
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
|
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
|
||||||
@AppStorage("draft_posts") public var draftsPosts: [String] = []
|
@AppStorage("draft_posts") public var draftsPosts: [String] = []
|
||||||
|
|
||||||
public var pushNotificationsCount: Int {
|
public var pushNotificationsCount: Int {
|
||||||
get {
|
get {
|
||||||
Self.sharedDefault?.integer(forKey: "push_notifications_count") ?? 0
|
Self.sharedDefault?.integer(forKey: "push_notifications_count") ?? 0
|
||||||
|
@ -22,18 +22,18 @@ public class UserPreferences: ObservableObject {
|
||||||
Self.sharedDefault?.set(newValue, forKey: "push_notifications_count")
|
Self.sharedDefault?.set(newValue, forKey: "push_notifications_count")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published public var serverPreferences: ServerPreferences?
|
@Published public var serverPreferences: ServerPreferences?
|
||||||
|
|
||||||
private init() { }
|
private init() {}
|
||||||
|
|
||||||
public func setClient(client: Client) {
|
public func setClient(client: Client) {
|
||||||
self.client = client
|
self.client = client
|
||||||
Task {
|
Task {
|
||||||
await refreshServerPreferences()
|
await refreshServerPreferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func refreshServerPreferences() async {
|
public func refreshServerPreferences() async {
|
||||||
guard let client, client.isAuth else { return }
|
guard let client, client.isAuth else { return }
|
||||||
serverPreferences = try? await client.get(endpoint: Accounts.preferences)
|
serverPreferences = try? await client.get(endpoint: Accounts.preferences)
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Explore",
|
name: "Explore",
|
||||||
targets: ["Explore"]),
|
targets: ["Explore"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Account", path: "../Account"),
|
.package(name: "Account", path: "../Account"),
|
||||||
|
@ -30,8 +31,8 @@ let package = Package(
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "Status", package: "Status"),
|
.product(name: "Status", package: "Status"),
|
||||||
.product(name: "DesignSystem", package: "DesignSystem")
|
.product(name: "DesignSystem", package: "DesignSystem"),
|
||||||
])
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import Network
|
|
||||||
import DesignSystem
|
|
||||||
import Models
|
|
||||||
import Status
|
|
||||||
import Shimmer
|
|
||||||
import Account
|
import Account
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import Shimmer
|
||||||
|
import Status
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ExploreView: View {
|
public struct ExploreView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var routeurPath: RouterPath
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
@StateObject private var viewModel = ExploreViewModel()
|
@StateObject private var viewModel = ExploreViewModel()
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
if !viewModel.searchQuery.isEmpty {
|
if !viewModel.searchQuery.isEmpty {
|
||||||
|
@ -30,7 +30,7 @@ public struct ExploreView: View {
|
||||||
EmptyView(iconName: "magnifyingglass",
|
EmptyView(iconName: "magnifyingglass",
|
||||||
title: "Search your instance",
|
title: "Search your instance",
|
||||||
message: "From this screen you can search anything on \(client.server)")
|
message: "From this screen you can search anything on \(client.server)")
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
} else {
|
} else {
|
||||||
if !viewModel.trendingTags.isEmpty {
|
if !viewModel.trendingTags.isEmpty {
|
||||||
trendingTagsSection
|
trendingTagsSection
|
||||||
|
@ -64,10 +64,10 @@ public struct ExploreView: View {
|
||||||
suggestedTokens: $viewModel.suggestedToken,
|
suggestedTokens: $viewModel.suggestedToken,
|
||||||
prompt: Text("Search users, posts and tags"),
|
prompt: Text("Search users, posts and tags"),
|
||||||
token: { token in
|
token: { token in
|
||||||
Text(token.rawValue)
|
Text(token.rawValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingView: some View {
|
private var loadingView: some View {
|
||||||
ForEach(Status.placeholders()) { status in
|
ForEach(Status.placeholders()) { status in
|
||||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||||
|
@ -77,7 +77,7 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeSearchResultsView(results: SearchResults) -> some View {
|
private func makeSearchResultsView(results: SearchResults) -> some View {
|
||||||
if !results.accounts.isEmpty {
|
if !results.accounts.isEmpty {
|
||||||
|
@ -109,16 +109,16 @@ public struct ExploreView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var suggestedAccountsSection: some View {
|
private var suggestedAccountsSection: some View {
|
||||||
Section("Suggested Users") {
|
Section("Suggested Users") {
|
||||||
ForEach(viewModel.suggestedAccounts
|
ForEach(viewModel.suggestedAccounts
|
||||||
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
||||||
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.suggestedAccounts) { account in
|
ForEach(viewModel.suggestedAccounts) { account in
|
||||||
|
@ -140,15 +140,15 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var trendingTagsSection: some View {
|
private var trendingTagsSection: some View {
|
||||||
Section("Trending Tags") {
|
Section("Trending Tags") {
|
||||||
ForEach(viewModel.trendingTags
|
ForEach(viewModel.trendingTags
|
||||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.trendingTags) { tag in
|
ForEach(viewModel.trendingTags) { tag in
|
||||||
|
@ -169,16 +169,16 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var trendingPostsSection: some View {
|
private var trendingPostsSection: some View {
|
||||||
Section("Trending Posts") {
|
Section("Trending Posts") {
|
||||||
ForEach(viewModel.trendingStatuses
|
ForEach(viewModel.trendingStatuses
|
||||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
||||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.trendingStatuses) { status in
|
ForEach(viewModel.trendingStatuses) { status in
|
||||||
|
@ -199,15 +199,15 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var trendingLinksSection: some View {
|
private var trendingLinksSection: some View {
|
||||||
Section("Trending Links") {
|
Section("Trending Links") {
|
||||||
ForEach(viewModel.trendingLinks
|
ForEach(viewModel.trendingLinks
|
||||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||||
StatusCardView(card: card)
|
StatusCardView(card: card)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.trendingLinks) { card in
|
ForEach(viewModel.trendingLinks) { card in
|
||||||
|
@ -228,5 +228,4 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import SwiftUI
|
import Combine
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Combine
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class ExploreViewModel: ObservableObject {
|
class ExploreViewModel: ObservableObject {
|
||||||
|
@ -17,16 +17,16 @@ class ExploreViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Token: String, Identifiable {
|
enum Token: String, Identifiable {
|
||||||
case user = "@user"
|
case user = "@user"
|
||||||
case statuses = "@posts"
|
case statuses = "@posts"
|
||||||
case tag = "#hashtag"
|
case tag = "#hashtag"
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiType: String {
|
var apiType: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .user:
|
case .user:
|
||||||
|
@ -42,7 +42,7 @@ class ExploreViewModel: ObservableObject {
|
||||||
var allSectionsEmpty: Bool {
|
var allSectionsEmpty: Bool {
|
||||||
trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty
|
trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var tokens: [Token] = []
|
@Published var tokens: [Token] = []
|
||||||
@Published var suggestedToken: [Token] = []
|
@Published var suggestedToken: [Token] = []
|
||||||
@Published var searchQuery = ""
|
@Published var searchQuery = ""
|
||||||
|
@ -53,7 +53,7 @@ class ExploreViewModel: ObservableObject {
|
||||||
@Published var trendingTags: [Tag] = []
|
@Published var trendingTags: [Tag] = []
|
||||||
@Published var trendingStatuses: [Status] = []
|
@Published var trendingStatuses: [Status] = []
|
||||||
@Published var trendingLinks: [Card] = []
|
@Published var trendingLinks: [Card] = []
|
||||||
|
|
||||||
private var searchTask: Task<Void, Never>?
|
private var searchTask: Task<Void, Never>?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class ExploreViewModel: ObservableObject {
|
||||||
$searchQuery
|
$searchQuery
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
||||||
.sink(receiveValue: { [weak self] newValue in
|
.sink(receiveValue: { [weak self] _ in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
if self.searchQuery.starts(with: "@") {
|
if self.searchQuery.starts(with: "@") {
|
||||||
|
@ -76,17 +76,17 @@ class ExploreViewModel: ObservableObject {
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchTrending() async {
|
func fetchTrending() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let data = try await fetchTrendingsData(client: client)
|
let data = try await fetchTrendingsData(client: client)
|
||||||
self.suggestedAccounts = data.suggestedAccounts
|
suggestedAccounts = data.suggestedAccounts
|
||||||
self.trendingTags = data.trendingTags
|
trendingTags = data.trendingTags
|
||||||
self.trendingStatuses = data.trendingStatuses
|
trendingStatuses = data.trendingStatuses
|
||||||
self.trendingLinks = data.trendingLinks
|
trendingLinks = data.trendingLinks
|
||||||
|
|
||||||
self.suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: self.suggestedAccounts.map{ $0.id }))
|
suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: suggestedAccounts.map { $0.id }))
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLoaded = true
|
isLoaded = true
|
||||||
}
|
}
|
||||||
|
@ -94,14 +94,14 @@ class ExploreViewModel: ObservableObject {
|
||||||
isLoaded = true
|
isLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct TrendingData {
|
private struct TrendingData {
|
||||||
let suggestedAccounts: [Account]
|
let suggestedAccounts: [Account]
|
||||||
let trendingTags: [Tag]
|
let trendingTags: [Tag]
|
||||||
let trendingStatuses: [Status]
|
let trendingStatuses: [Status]
|
||||||
let trendingLinks: [Card]
|
let trendingLinks: [Card]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchTrendingsData(client: Client) async throws -> TrendingData {
|
private func fetchTrendingsData(client: Client) async throws -> TrendingData {
|
||||||
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
||||||
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
||||||
|
@ -112,7 +112,7 @@ class ExploreViewModel: ObservableObject {
|
||||||
trendingStatuses: trendingStatuses,
|
trendingStatuses: trendingStatuses,
|
||||||
trendingLinks: trendingLinks)
|
trendingLinks: trendingLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func search() {
|
func search() {
|
||||||
guard !searchQuery.isEmpty else { return }
|
guard !searchQuery.isEmpty else { return }
|
||||||
searchTask?.cancel()
|
searchTask?.cancel()
|
||||||
|
@ -127,10 +127,10 @@ class ExploreViewModel: ObservableObject {
|
||||||
following: nil),
|
following: nil),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
let relationships: [Relationshionship] =
|
let relationships: [Relationshionship] =
|
||||||
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map{ $0.id }))
|
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map { $0.id }))
|
||||||
results.relationships = relationships
|
results.relationships = relationships
|
||||||
self.results[searchQuery] = results
|
self.results[searchQuery] = results
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Lists",
|
name: "Lists",
|
||||||
targets: ["Lists"]),
|
targets: ["Lists"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Network", path: "../Network"),
|
.package(name: "Network", path: "../Network"),
|
||||||
|
@ -26,8 +27,8 @@ let package = Package(
|
||||||
.product(name: "Network", package: "Network"),
|
.product(name: "Network", package: "Network"),
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "DesignSystem", package: "DesignSystem")
|
.product(name: "DesignSystem", package: "DesignSystem"),
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import SwiftUI
|
|
||||||
import Network
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ListAddAccountView: View {
|
public struct ListAddAccountView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@ -10,15 +10,14 @@ public struct ListAddAccountView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@StateObject private var viewModel: ListAddAccountViewModel
|
@StateObject private var viewModel: ListAddAccountViewModel
|
||||||
|
|
||||||
@State private var isCreateListAlertPresented: Bool = false
|
@State private var isCreateListAlertPresented: Bool = false
|
||||||
@State private var createListTitle: String = ""
|
@State private var createListTitle: String = ""
|
||||||
|
|
||||||
|
|
||||||
public init(account: Account) {
|
public init(account: Account) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(account: account))
|
_viewModel = StateObject(wrappedValue: .init(account: account))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class ListAddAccountViewModel: ObservableObject {
|
class ListAddAccountViewModel: ObservableObject {
|
||||||
let account: Account
|
let account: Account
|
||||||
|
|
||||||
@Published var inLists: [Models.List] = []
|
@Published var inLists: [Models.List] = []
|
||||||
@Published var isLoadingInfo: Bool = true
|
@Published var isLoadingInfo: Bool = true
|
||||||
|
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
init(account: Account) {
|
init(account: Account) {
|
||||||
self.account = account
|
self.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchInfo() async {
|
func fetchInfo() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
isLoadingInfo = true
|
isLoadingInfo = true
|
||||||
|
@ -27,7 +27,7 @@ class ListAddAccountViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addToList(list: Models.List) async {
|
func addToList(list: Models.List) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
let response = try? await client.post(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
let response = try? await client.post(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
|
@ -35,7 +35,7 @@ class ListAddAccountViewModel: ObservableObject {
|
||||||
inLists.append(list)
|
inLists.append(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFromList(list: Models.List) async {
|
func removeFromList(list: Models.List) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
let response = try? await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
let response = try? await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Network
|
|
||||||
import EmojiText
|
import EmojiText
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ListEditView: View {
|
public struct ListEditView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@StateObject private var viewModel: ListEditViewModel
|
@StateObject private var viewModel: ListEditViewModel
|
||||||
|
|
||||||
public init(list: Models.List) {
|
public init(list: Models.List) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(list: list))
|
_viewModel = StateObject(wrappedValue: .init(list: list))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class ListEditViewModel: ObservableObject {
|
public class ListEditViewModel: ObservableObject {
|
||||||
let list: Models.List
|
let list: Models.List
|
||||||
|
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
@Published var isLoadingAccounts: Bool = true
|
@Published var isLoadingAccounts: Bool = true
|
||||||
@Published var accounts: [Account] = []
|
@Published var accounts: [Account] = []
|
||||||
|
|
||||||
init(list: Models.List) {
|
init(list: Models.List) {
|
||||||
self.list = list
|
self.list = list
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccounts() async {
|
func fetchAccounts() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
isLoadingAccounts = true
|
isLoadingAccounts = true
|
||||||
|
@ -25,14 +25,14 @@ public class ListEditViewModel: ObservableObject {
|
||||||
isLoadingAccounts = false
|
isLoadingAccounts = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(account: Account) async {
|
func delete(account: Account) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let response = try await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
let response = try await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
if response?.statusCode == 200 {
|
if response?.statusCode == 200 {
|
||||||
accounts.removeAll(where: { $0.id == account.id })
|
accounts.removeAll(where: { $0.id == account.id })
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "Models",
|
name: "Models",
|
||||||
targets: ["Models"]),
|
targets: ["Models"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://gitlab.com/mflint/HTML2Markdown", exact: "1.0.0"),
|
.package(url: "https://gitlab.com/mflint/HTML2Markdown", exact: "1.0.0"),
|
||||||
|
@ -21,9 +22,11 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "Models",
|
name: "Models",
|
||||||
dependencies: ["HTML2Markdown",
|
dependencies: ["HTML2Markdown",
|
||||||
"SwiftSoup"]),
|
"SwiftSoup"]
|
||||||
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ModelsTests",
|
name: "ModelsTests",
|
||||||
dependencies: ["Models"]),
|
dependencies: ["Models"]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Account: Codable, Identifiable, Equatable, Hashable {
|
public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Field: Codable, Equatable, Identifiable {
|
public struct Field: Codable, Equatable, Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
value + name
|
value + name
|
||||||
}
|
}
|
||||||
|
|
||||||
public let name: String
|
public let name: String
|
||||||
public let value: HTMLString
|
public let value: HTMLString
|
||||||
public let verifiedAt: String?
|
public let verifiedAt: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Source: Codable, Equatable {
|
public struct Source: Codable, Equatable {
|
||||||
public let privacy: Visibility
|
public let privacy: Visibility
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
@ -23,7 +22,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let note: String
|
public let note: String
|
||||||
public let fields: [Field]
|
public let fields: [Field]
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
|
@ -43,7 +42,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let source: Source?
|
public let source: Source?
|
||||||
public let bot: Bool
|
public let bot: Bool
|
||||||
public let discoverable: Bool?
|
public let discoverable: Bool?
|
||||||
|
|
||||||
public static func placeholder() -> Account {
|
public static func placeholder() -> Account {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
username: "Username",
|
username: "Username",
|
||||||
|
@ -65,7 +64,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
bot: false,
|
bot: false,
|
||||||
discoverable: true)
|
discoverable: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Account] {
|
public static func placeholders() -> [Account] {
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
|
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
|
||||||
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
|
|
|
@ -5,8 +5,8 @@ import SwiftUI
|
||||||
|
|
||||||
public typealias HTMLString = String
|
public typealias HTMLString = String
|
||||||
|
|
||||||
extension HTMLString {
|
public extension HTMLString {
|
||||||
public var asMarkdown: String {
|
var asMarkdown: String {
|
||||||
do {
|
do {
|
||||||
let dom = try HTMLParser().parse(html: self)
|
let dom = try HTMLParser().parse(html: self)
|
||||||
return dom.toMarkdown()
|
return dom.toMarkdown()
|
||||||
|
@ -16,8 +16,8 @@ extension HTMLString {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var asRawText: String {
|
var asRawText: String {
|
||||||
do {
|
do {
|
||||||
let document: Document = try SwiftSoup.parse(self)
|
let document: Document = try SwiftSoup.parse(self)
|
||||||
return try document.text()
|
return try document.text()
|
||||||
|
@ -25,8 +25,8 @@ extension HTMLString {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func findStatusesURLs() -> [URL]? {
|
func findStatusesURLs() -> [URL]? {
|
||||||
do {
|
do {
|
||||||
let document: Document = try SwiftSoup.parse(self)
|
let document: Document = try SwiftSoup.parse(self)
|
||||||
let links: Elements = try document.select("a")
|
let links: Elements = try document.select("a")
|
||||||
|
@ -34,7 +34,8 @@ extension HTMLString {
|
||||||
for link in links {
|
for link in links {
|
||||||
let href = try link.attr("href")
|
let href = try link.attr("href")
|
||||||
if let url = URL(string: href),
|
if let url = URL(string: href),
|
||||||
let _ = Int(url.lastPathComponent) {
|
let _ = Int(url.lastPathComponent)
|
||||||
|
{
|
||||||
URLs.append(url)
|
URLs.append(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,8 +44,8 @@ extension HTMLString {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var asSafeAttributedString: AttributedString {
|
var asSafeAttributedString: AttributedString {
|
||||||
do {
|
do {
|
||||||
let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true,
|
let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true,
|
||||||
interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||||
|
@ -54,4 +55,3 @@ extension HTMLString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,23 +10,23 @@ extension ServerDate {
|
||||||
dateFormatter.timeZone = .init(abbreviation: "UTC")
|
dateFormatter.timeZone = .init(abbreviation: "UTC")
|
||||||
return dateFormatter
|
return dateFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var createdAtRelativeFormatter: RelativeDateTimeFormatter {
|
private static var createdAtRelativeFormatter: RelativeDateTimeFormatter {
|
||||||
let dateFormatter = RelativeDateTimeFormatter()
|
let dateFormatter = RelativeDateTimeFormatter()
|
||||||
dateFormatter.unitsStyle = .abbreviated
|
dateFormatter.unitsStyle = .abbreviated
|
||||||
return dateFormatter
|
return dateFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var createdAtShortDateFormatted: DateFormatter {
|
private static var createdAtShortDateFormatted: DateFormatter {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateStyle = .medium
|
dateFormatter.dateStyle = .medium
|
||||||
return dateFormatter
|
return dateFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
public var asDate: Date {
|
public var asDate: Date {
|
||||||
Self.createdAtDateFormatter.date(from: self)!
|
Self.createdAtDateFormatter.date(from: self)!
|
||||||
}
|
}
|
||||||
|
|
||||||
public var formatted: String {
|
public var formatted: String {
|
||||||
let calendar = Calendar(identifier: .gregorian)
|
let calendar = Calendar(identifier: .gregorian)
|
||||||
if calendar.numberOfDaysBetween(asDate, and: Date()) > 1 {
|
if calendar.numberOfDaysBetween(asDate, and: Date()) > 1 {
|
||||||
|
@ -37,13 +37,12 @@ extension ServerDate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Calendar {
|
extension Calendar {
|
||||||
func numberOfDaysBetween(_ from: Date, and to: Date) -> Int {
|
func numberOfDaysBetween(_ from: Date, and to: Date) -> Int {
|
||||||
let fromDate = startOfDay(for: from)
|
let fromDate = startOfDay(for: from)
|
||||||
let toDate = startOfDay(for: to)
|
let toDate = startOfDay(for: to)
|
||||||
let numberOfDays = dateComponents([.day], from: fromDate, to: toDate)
|
let numberOfDays = dateComponents([.day], from: fromDate, to: toDate)
|
||||||
|
|
||||||
return numberOfDays.day!
|
return numberOfDays.day!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct AppInfo {
|
public enum AppInfo {
|
||||||
public static let clientName = "IceCubesApp"
|
public static let clientName = "IceCubesApp"
|
||||||
public static let scheme = "icecubesapp://"
|
public static let scheme = "icecubesapp://"
|
||||||
public static let scopes = "read write follow push"
|
public static let scopes = "read write follow push"
|
||||||
|
|
|
@ -4,7 +4,7 @@ public struct Card: Codable, Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
url.absoluteString
|
url.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let title: String?
|
public let title: String?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
|
|
|
@ -5,11 +5,11 @@ public struct Conversation: Identifiable, Decodable {
|
||||||
public let unread: Bool
|
public let unread: Bool
|
||||||
public let lastStatus: Status
|
public let lastStatus: Status
|
||||||
public let accounts: [Account]
|
public let accounts: [Account]
|
||||||
|
|
||||||
public static func placeholder() -> Conversation {
|
public static func placeholder() -> Conversation {
|
||||||
.init(id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()])
|
.init(id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Conversation] {
|
public static func placeholders() -> [Conversation] {
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
|
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(),
|
||||||
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Emoji: Codable, Hashable, Identifiable {
|
public struct Emoji: Codable, Hashable, Identifiable {
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(shortcode)
|
hasher.combine(shortcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
shortcode
|
shortcode
|
||||||
}
|
}
|
||||||
|
|
||||||
public let shortcode: String
|
public let shortcode: String
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let staticUrl: URL
|
public let staticUrl: URL
|
||||||
|
|
|
@ -9,12 +9,12 @@ public struct Filter: Codable, Identifiable {
|
||||||
public enum Action: String, Codable {
|
public enum Action: String, Codable {
|
||||||
case warn, hide
|
case warn, hide
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Context: String, Codable {
|
public enum Context: String, Codable {
|
||||||
case home, notifications, account, thread
|
case home, notifications, account, thread
|
||||||
case pub = "public"
|
case pub = "public"
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let context: [String]
|
public let context: [String]
|
||||||
|
|
|
@ -6,29 +6,29 @@ public struct Instance: Codable {
|
||||||
public let statusCount: Int
|
public let statusCount: Int
|
||||||
public let domainCount: Int
|
public let domainCount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Configuration: Codable {
|
public struct Configuration: Codable {
|
||||||
public struct Statuses: Codable {
|
public struct Statuses: Codable {
|
||||||
public let maxCharacters: Int
|
public let maxCharacters: Int
|
||||||
public let maxMediaAttachments: Int
|
public let maxMediaAttachments: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Polls: Codable {
|
public struct Polls: Codable {
|
||||||
public let maxOptions: Int
|
public let maxOptions: Int
|
||||||
public let maxCharactersPerOption: Int
|
public let maxCharactersPerOption: Int
|
||||||
public let minExpiration: Int
|
public let minExpiration: Int
|
||||||
public let maxExpiration: Int
|
public let maxExpiration: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public let statuses: Statuses
|
public let statuses: Statuses
|
||||||
public let polls: Polls
|
public let polls: Polls
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Rule: Codable, Identifiable {
|
public struct Rule: Codable, Identifiable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let text: String
|
public let text: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public let title: String
|
public let title: String
|
||||||
public let shortDescription: String
|
public let shortDescription: String
|
||||||
public let email: String
|
public let email: String
|
||||||
|
|
|
@ -4,6 +4,7 @@ public struct InstanceSocial: Decodable, Identifiable {
|
||||||
public struct Info: Decodable {
|
public struct Info: Decodable {
|
||||||
public let shortDescription: String
|
public let shortDescription: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let name: String
|
public let name: String
|
||||||
public let dead: Bool
|
public let dead: Bool
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct MastodonPushNotification: Codable {
|
public struct MastodonPushNotification: Codable {
|
||||||
|
|
||||||
public let accessToken: String
|
public let accessToken: String
|
||||||
|
|
||||||
public let notificationID: Int
|
public let notificationID: Int
|
||||||
public let notificationType: String
|
public let notificationType: String
|
||||||
|
|
||||||
public let preferredLocale: String?
|
public let preferredLocale: String?
|
||||||
public let icon: String?
|
public let icon: String?
|
||||||
public let title: String
|
public let title: String
|
||||||
public let body: String
|
public let body: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case accessToken = "access_token"
|
case accessToken = "access_token"
|
||||||
case notificationID = "notification_id"
|
case notificationID = "notification_id"
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct MediaAttachement: Codable, Identifiable, Hashable {
|
public struct MediaAttachement: Codable, Identifiable, Hashable {
|
||||||
|
|
||||||
public struct MetaContainer: Codable, Equatable {
|
public struct MetaContainer: Codable, Equatable {
|
||||||
public struct Meta: Codable, Equatable {
|
public struct Meta: Codable, Equatable {
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
public let original: Meta?
|
public let original: Meta?
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SupportedType: String {
|
public enum SupportedType: String {
|
||||||
case image, gifv, video, audio
|
case image, gifv, video, audio
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let type: String
|
public let type: String
|
||||||
public var supportedType: SupportedType? {
|
public var supportedType: SupportedType? {
|
||||||
SupportedType(rawValue: type)
|
SupportedType(rawValue: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let url: URL?
|
public let url: URL?
|
||||||
public let previewUrl: URL?
|
public let previewUrl: URL?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
public let meta: MetaContainer?
|
public let meta: MetaContainer?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,17 @@ public struct Notification: Codable, Identifiable {
|
||||||
public enum NotificationType: String, CaseIterable {
|
public enum NotificationType: String, CaseIterable {
|
||||||
case follow, follow_request, mention, reblog, status, favourite, poll, update
|
case follow, follow_request, mention, reblog, status, favourite, poll, update
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let type: String
|
public let type: String
|
||||||
public let createdAt: ServerDate
|
public let createdAt: ServerDate
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let status: Status?
|
public let status: Status?
|
||||||
|
|
||||||
public var supportedType: NotificationType? {
|
public var supportedType: NotificationType? {
|
||||||
.init(rawValue: type)
|
.init(rawValue: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholder() -> Notification {
|
public static func placeholder() -> Notification {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
type: NotificationType.favourite.rawValue,
|
type: NotificationType.favourite.rawValue,
|
||||||
|
@ -22,9 +22,8 @@ public struct Notification: Codable, Identifiable {
|
||||||
account: .placeholder(),
|
account: .placeholder(),
|
||||||
status: .placeholder())
|
status: .placeholder())
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Notification] {
|
public static func placeholders() -> [Notification] {
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@ public struct Poll: Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case title, votesCount
|
case title, votesCount
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id = UUID().uuidString
|
public var id = UUID().uuidString
|
||||||
public let title: String
|
public let title: String
|
||||||
public let votesCount: Int
|
public let votesCount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let expiresAt: ServerDate
|
public let expiresAt: ServerDate
|
||||||
public let expired: Bool
|
public let expired: Bool
|
||||||
|
|
|
@ -9,7 +9,7 @@ public struct PushSubscription: Identifiable, Decodable {
|
||||||
public let poll: Bool
|
public let poll: Bool
|
||||||
public let status: Bool
|
public let status: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: Int
|
public let id: Int
|
||||||
public let endpoint: URL
|
public let endpoint: URL
|
||||||
public let serverKey: String
|
public let serverKey: String
|
||||||
|
|
|
@ -14,8 +14,8 @@ public struct Relationshionship: Codable {
|
||||||
public let endorsed: Bool
|
public let endorsed: Bool
|
||||||
public let note: String
|
public let note: String
|
||||||
public let notifying: Bool
|
public let notifying: Bool
|
||||||
|
|
||||||
static public func placeholder() -> Relationshionship {
|
public static func placeholder() -> Relationshionship {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
following: false,
|
following: false,
|
||||||
showingReblogs: false,
|
showingReblogs: false,
|
||||||
|
|
|
@ -4,7 +4,7 @@ public struct SearchResults: Decodable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case accounts, statuses, hashtags
|
case accounts, statuses, hashtags
|
||||||
}
|
}
|
||||||
|
|
||||||
public let accounts: [Account]
|
public let accounts: [Account]
|
||||||
public var relationships: [Relationshionship] = []
|
public var relationships: [Relationshionship] = []
|
||||||
public let statuses: [Status]
|
public let statuses: [Status]
|
||||||
|
|
|
@ -6,13 +6,13 @@ public struct ServerPreferences: Decodable {
|
||||||
public let postLanguage: String?
|
public let postLanguage: String?
|
||||||
public let autoExpandmedia: AutoExpandMedia?
|
public let autoExpandmedia: AutoExpandMedia?
|
||||||
public let autoExpandSpoilers: Bool?
|
public let autoExpandSpoilers: Bool?
|
||||||
|
|
||||||
public enum AutoExpandMedia: String, Decodable {
|
public enum AutoExpandMedia: String, Decodable {
|
||||||
case showAll = "show_all"
|
case showAll = "show_all"
|
||||||
case hideAll = "hide_all"
|
case hideAll = "hide_all"
|
||||||
case hideSensitive = "default"
|
case hideSensitive = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case postVisibility = "posting:default:visibility"
|
case postVisibility = "posting:default:visibility"
|
||||||
case postIsSensitive = "posting:default:sensitive"
|
case postIsSensitive = "posting:default:sensitive"
|
||||||
|
|
|
@ -4,12 +4,13 @@ public struct Application: Codable, Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
public let name: String
|
public let name: String
|
||||||
public let website: URL?
|
public let website: URL?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Application {
|
public extension Application {
|
||||||
public init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
|
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||||
|
@ -53,12 +54,11 @@ public protocol AnyStatus {
|
||||||
var language: String? { get }
|
var language: String? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct Status: AnyStatus, Codable, Identifiable {
|
public struct Status: AnyStatus, Codable, Identifiable {
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let content: HTMLString
|
public let content: HTMLString
|
||||||
public let account: Account
|
public let account: Account
|
||||||
|
@ -85,7 +85,7 @@ public struct Status: AnyStatus, Codable, Identifiable {
|
||||||
public let filtered: [Filtered]?
|
public let filtered: [Filtered]?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let language: String?
|
public let language: String?
|
||||||
|
|
||||||
public static func placeholder() -> Status {
|
public static func placeholder() -> Status {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
content: "This is a #toot\nWith some @content\nAnd some more content for your #eyes @only",
|
content: "This is a #toot\nWith some @content\nAnd some more content for your #eyes @only",
|
||||||
|
@ -114,7 +114,7 @@ public struct Status: AnyStatus, Codable, Identifiable {
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
language: nil)
|
language: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Status] {
|
public static func placeholders() -> [Status] {
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let content: String
|
public let content: String
|
||||||
public let account: Account
|
public let account: Account
|
||||||
|
|
|
@ -6,7 +6,7 @@ public struct RawStreamEvent: Decodable {
|
||||||
public let payload: String
|
public let payload: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol StreamEvent: Identifiable{
|
public protocol StreamEvent: Identifiable {
|
||||||
var date: Date { get }
|
var date: Date { get }
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@ import Foundation
|
||||||
public struct StreamMessage: Encodable {
|
public struct StreamMessage: Encodable {
|
||||||
public let type: String
|
public let type: String
|
||||||
public let stream: String
|
public let stream: String
|
||||||
|
|
||||||
public init(type: String, stream: String) {
|
public init(type: String, stream: String) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue