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