diff --git a/IceCubesApp/App/AppRouter.swift b/IceCubesApp/App/AppRouter.swift index 373177bc..a2941d2e 100644 --- a/IceCubesApp/App/AppRouter.swift +++ b/IceCubesApp/App/AppRouter.swift @@ -1,12 +1,12 @@ import Account import AppAccount +import Conversations import DesignSystem import Env import Lists import Status import SwiftUI import Timeline -import Conversations @MainActor extension View { diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index aaa29272..72c1557a 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -12,9 +12,9 @@ import Timeline @main 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 @StateObject private var currentAccount = CurrentAccount.shared @@ -23,18 +23,18 @@ struct IceCubesApp: App { @StateObject private var quickLook = QuickLook() @StateObject private var theme = Theme.shared @StateObject private var sidebarRouterPath = RouterPath() - + @State private var selectedTab: Tab = .timeline @State private var selectSidebarItem: Tab? = .timeline @State private var popToRootTab: Tab = .other @State private var sideBarLoadedTabs: Set = Set() - + private let feedbackGenerator = UISelectionFeedbackGenerator() - + private var availableTabs: [Tab] { appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() } - + var body: some Scene { WindowGroup { appView @@ -71,7 +71,7 @@ struct IceCubesApp: App { } } } - + @ViewBuilder private var appView: some View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { @@ -80,14 +80,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, @@ -116,7 +116,7 @@ struct IceCubesApp: App { sideBarLoadedTabs.removeAll() } } - + private var tabBarView: some View { TabView(selection: .init(get: { selectedTab @@ -142,14 +142,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: @@ -166,16 +166,16 @@ struct IceCubesApp: App { break } } - + private func setupRevenueCat() { Purchases.logLevel = .error Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi") } - + private func refreshPushSubs() { PushNotificationsService.shared.requestPushNotifications() } - + @CommandsBuilder private var appMenu: some Commands { CommandGroup(replacing: .newItem) { @@ -202,29 +202,29 @@ struct IceCubesApp: App { class AppDelegate: NSObject, UIApplicationDelegate { let themeObserver = ThemeObserverViewController(nibName: nil, bundle: nil) - + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers) return true } - + func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { PushNotificationsService.shared.pushToken = deviceToken -#if !DEBUG - Task { - await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) - await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) - } -#endif + #if !DEBUG + Task { + await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) + await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) + } + #endif } - + func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) if connectingSceneSession.role == .windowApplication { configuration.delegateClass = SceneDelegate.self @@ -236,7 +236,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { class ThemeObserverViewController: UIViewController { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - + print(traitCollection.userInterfaceStyle.rawValue) } } diff --git a/IceCubesApp/App/QuickLookRepresentable.swift b/IceCubesApp/App/QuickLookRepresentable.swift index ff238b2a..292a104a 100644 --- a/IceCubesApp/App/QuickLookRepresentable.swift +++ b/IceCubesApp/App/QuickLookRepresentable.swift @@ -51,12 +51,12 @@ struct QuickLookPreview: UIViewControllerRepresentable { class AppQLPreviewController: QLPreviewController { private var closeButton: UIBarButtonItem { - .init( - title: NSLocalizedString("action.done", comment: ""), - style: .plain, - target: self, - action: #selector(onCloseButton) - ) + .init( + title: NSLocalizedString("action.done", comment: ""), + style: .plain, + target: self, + action: #selector(onCloseButton) + ) } override func viewDidLayoutSubviews() { diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index c554cb55..d3bc7357 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -100,7 +100,7 @@ struct SettingsTabs: View { } .listRowBackground(theme.primaryBackgroundColor) } - + private var otherSections: some View { Section("settings.section.other") { if !ProcessInfo.processInfo.isiOSAppOnMac { diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index 3ba91cc3..9159cbc3 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -166,6 +166,10 @@ "account.follow.follow" = "Folgen"; "account.follow.following" = "Gefolgt"; "account.follow.requested" = "Angefragt"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "Follower"; "account.following" = "Folgt"; "account.list.create" = "Neue Liste erstellen"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index c1021ada..4c413b98 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -169,6 +169,10 @@ "account.follow.follow" = "Follow"; "account.follow.following" = "Following"; "account.follow.requested" = "Requested"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "Followers"; "account.following" = "Following"; "account.list.create" = "Create a new list"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index e45c5c10..233b0b16 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -166,6 +166,10 @@ "account.follow.follow" = "Seguir"; "account.follow.following" = "Siguiendo"; "account.follow.requested" = "Solicitado"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "Seguidores"; "account.following" = "Siguiendo"; "account.list.create" = "Crear una lista nueva"; diff --git a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings index be4f545d..a13677ae 100644 --- a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings @@ -166,6 +166,10 @@ "account.follow.follow" = "Segui"; "account.follow.following" = "Segui già"; "account.follow.requested" = "Richiesto"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "Seguito da"; "account.following" = "Seguiti"; "account.list.create" = "Crea una nuova lista"; diff --git a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings index fafb82e5..c521db68 100644 --- a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings @@ -152,6 +152,10 @@ "account.follow.follow" = "フォロー"; "account.follow.following" = "フォローしている"; "account.follow.requested" = "リクエストしました"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "フォロワー"; "account.following" = "フォローしている"; "account.list.create" = "新しいリストを作成"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index 4743422e..a14fc685 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -166,6 +166,10 @@ "account.follow.follow" = "Volg"; "account.follow.following" = "Volgend"; "account.follow.requested" = "Verzocht"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "Volgers"; "account.following" = "Volgend"; "account.list.create" = "Maak een nieuwe lijst"; diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index f4ed9cba..b06af0bd 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -167,6 +167,10 @@ "account.follow.follow" = "关注"; "account.follow.following" = "正在关注"; "account.follow.requested" = "已申请"; +"account.follow-request.accept" = "Accept"; +"account.follow-request.reject" = "Reject"; +"account.follow-requests.pending-requests" = "Pending requests"; +"account.follow-requests.instructions" = "Those users won't see your posts until you accept them."; "account.followers" = "粉丝"; "account.following" = "关注"; "account.list.create" = "新建一个列表"; diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index e37c3c82..81af354e 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -1,10 +1,10 @@ +import AppAccount import CryptoKit import Env import KeychainSwift import Models import UIKit import UserNotifications -import AppAccount @MainActor class NotificationService: UNNotificationServiceExtension { @@ -51,7 +51,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) return } - + bestAttemptContent.title = notification.title if AppAccountsManager.shared.availableAccounts.count > 1 { bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? "" diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index d24047e6..a4003e99 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -10,6 +10,7 @@ struct AccountDetailHeaderView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var quickLook: QuickLook @EnvironmentObject private var routerPath: RouterPath + @EnvironmentObject private var currentAccount: CurrentAccount @Environment(\.redactionReasons) private var reasons @ObservedObject var viewModel: AccountDetailViewModel @@ -95,7 +96,11 @@ struct AccountDetailHeaderView: View { makeCustomInfoLabel(title: "account.following", count: account.followingCount) } NavigationLink(value: RouterDestinations.followers(id: account.id)) { - makeCustomInfoLabel(title: "account.followers", count: account.followersCount) + makeCustomInfoLabel( + title: "account.followers", + count: account.followersCount, + needsBadge: currentAccount.account?.id == account.id && !currentAccount.followRequests.isEmpty + ) } }.offset(y: 20) } @@ -136,11 +141,19 @@ struct AccountDetailHeaderView: View { .offset(y: -40) } - private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int) -> some View { + private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View { VStack { Text("\(count)") .font(.scaledHeadline) .foregroundColor(theme.tintColor) + .overlay(alignment: .trailing) { + if needsBadge { + Circle() + .fill(Color.red) + .frame(width: 9, height: 9) + .offset(x: 12) + } + } Text(title) .font(.scaledFootnote) .foregroundColor(.gray) diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index 09b31b1a..d9531d06 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -10,9 +10,9 @@ public class AccountsListRowViewModel: ObservableObject { var client: Client? @Published var account: Account - @Published var relationShip: Relationship + @Published var relationShip: Relationship? - public init(account: Account, relationShip: Relationship) { + public init(account: Account, relationShip: Relationship? = nil) { self.account = account self.relationShip = relationShip } @@ -24,9 +24,13 @@ public struct AccountsListRow: View { @EnvironmentObject private var client: Client @StateObject var viewModel: AccountsListRowViewModel + let isFollowRequest: Bool + let requestUpdated: (() -> Void)? - public init(viewModel: AccountsListRowViewModel) { + public init(viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, requestUpdated: (() -> Void)? = nil) { _viewModel = StateObject(wrappedValue: viewModel) + self.isFollowRequest = isFollowRequest + self.requestUpdated = requestUpdated } public var body: some View { @@ -45,11 +49,17 @@ public struct AccountsListRow: View { .environment(\.openURL, OpenURLAction { url in routerPath.handle(url: url) }) + if isFollowRequest { + FollowRequestButtons(account: viewModel.account, + requestUpdated: requestUpdated) + } } Spacer() - if currentAccount.account?.id != viewModel.account.id { + if currentAccount.account?.id != viewModel.account.id, + let relationShip = viewModel.relationShip + { FollowButton(viewModel: .init(accountId: viewModel.account.id, - relationship: viewModel.relationShip, + relationship: relationShip, shouldDisplayNotify: false, relationshipUpdated: { _ in })) } diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift index 91298757..233b3a8a 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift @@ -8,6 +8,7 @@ import SwiftUI public struct AccountsListView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client + @EnvironmentObject private var currentAccount: CurrentAccount @StateObject private var viewModel: AccountsListViewModel @State private var didAppear: Bool = false @@ -26,11 +27,37 @@ public struct AccountsListView: View { .listRowBackground(theme.primaryBackgroundColor) } case let .display(accounts, relationships, nextPageState): - ForEach(accounts) { account in - if let relationship = relationships.first(where: { $0.id == account.id }) { - AccountsListRow(viewModel: .init(account: account, - relationShip: relationship)) + if case .followers = viewModel.mode, + !currentAccount.followRequests.isEmpty + { + Section( + header: Text("account.follow-requests.pending-requests"), + footer: Text("account.follow-requests.instructions") + .font(.scaledFootnote) + .foregroundColor(.secondary) + .offset(y: -8) + ) { + ForEach(currentAccount.followRequests) { account in + AccountsListRow( + viewModel: .init(account: account), + isFollowRequest: true, + requestUpdated: { + Task { + await viewModel.fetch() + } + } + ) .listRowBackground(theme.primaryBackgroundColor) + } + } + } + Section { + ForEach(accounts) { account in + if let relationship = relationships.first(where: { $0.id == account.id }) { + AccountsListRow(viewModel: .init(account: account, + relationShip: relationship)) + .listRowBackground(theme.primaryBackgroundColor) + } } } diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift index 3fe0f58e..9ec85b9a 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift @@ -66,7 +66,7 @@ class AccountsListViewModel: ObservableObject { maxId: nil)) case let .favoritedBy(statusId): (accounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId, - maxId: nil)) + maxId: nil)) } nextPageId = link?.maxId relationships = try await client.get(endpoint: @@ -95,7 +95,7 @@ class AccountsListViewModel: ObservableObject { maxId: nextPageId)) case let .favoritedBy(statusId): (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId, - maxId: nextPageId)) + maxId: nextPageId)) } accounts.append(contentsOf: newAccounts) let newRelationships: [Relationship] = diff --git a/Packages/Account/Sources/Account/Follow/FollowButton.swift b/Packages/Account/Sources/Account/Follow/FollowButton.swift index 89d58afe..0a624918 100644 --- a/Packages/Account/Sources/Account/Follow/FollowButton.swift +++ b/Packages/Account/Sources/Account/Follow/FollowButton.swift @@ -111,7 +111,7 @@ public struct FollowButton: View { } .buttonStyle(.bordered) .disabled(viewModel.isUpdating) - + Button { Task { await viewModel.toggleReboosts() diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift index 40040020..c9eb9544 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccount.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccount.swift @@ -31,7 +31,8 @@ public struct AppAccount: Codable, Identifiable { public init(server: String, accountName: String?, - oauthToken: OauthToken? = nil) { + oauthToken: OauthToken? = nil) + { self.server = server self.accountName = accountName self.oauthToken = oauthToken diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index eb0af227..3a35640f 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -9,7 +9,7 @@ public struct AppAccountsSelectorView: View { @ObservedObject var routerPath: RouterPath @State private var accountsViewModel: [AppAccountViewModel] = [] - + let feedbackGenerator = UIImpactFeedbackGenerator() private let accountCreationEnabled: Bool @@ -22,7 +22,7 @@ public struct AppAccountsSelectorView: View { self.routerPath = routerPath self.accountCreationEnabled = accountCreationEnabled self.avatarSize = avatarSize - + feedbackGenerator.prepare() } @@ -54,10 +54,18 @@ public struct AppAccountsSelectorView: View { @ViewBuilder private var labelView: some View { - if let avatar = currentAccount.account?.avatar { - AvatarView(url: avatar, size: avatarSize) - } else { - EmptyView() + Group { + if let avatar = currentAccount.account?.avatar { + AvatarView(url: avatar, size: avatarSize) + } else { + EmptyView() + } + }.overlay(alignment: .topTrailing) { + if !currentAccount.followRequests.isEmpty { + Circle() + .fill(Color.red) + .frame(width: 9, height: 9) + } } } @@ -73,7 +81,7 @@ public struct AppAccountsSelectorView: View { } else { appAccounts.currentAccount = viewModel.appAccount } - + feedbackGenerator.impactOccurred(intensity: 0.7) } label: { HStack { diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift index 763b6fb3..0b224ca3 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift @@ -1,9 +1,9 @@ -import SwiftUI -import Models import DesignSystem -import Network import Env +import Models +import Network import NukeUI +import SwiftUI public struct ConversationDetailView: View { private enum Constants { @@ -16,18 +16,18 @@ public struct ConversationDetailView: View { @EnvironmentObject private var client: Client @EnvironmentObject private var theme: Theme @EnvironmentObject private var watcher: StreamWatcher - + @StateObject private var viewModel: ConversationDetailViewModel - + @FocusState private var isMessageFieldFocused: Bool - + @State private var scrollProxy: ScrollViewProxy? @State private var didAppear: Bool = false - + public init(conversation: Conversation) { _viewModel = StateObject(wrappedValue: .init(conversation: conversation)) } - + public var body: some View { ScrollViewReader { proxy in ZStack(alignment: .bottom) { @@ -72,7 +72,8 @@ public struct ConversationDetailView: View { .toolbar { ToolbarItem(placement: .principal) { if viewModel.conversation.accounts.count == 1, - let account = viewModel.conversation.accounts.first { + let account = viewModel.conversation.accounts.first + { EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis) .font(.scaledHeadline) } else { @@ -92,7 +93,7 @@ public struct ConversationDetailView: View { } } } - + private var loadingView: some View { ForEach(Status.placeholders()) { message in ConversationMessageView(message: message, conversation: viewModel.conversation) @@ -100,14 +101,14 @@ public struct ConversationDetailView: View { .shimmering() } } - + private var bottomAnchorView: some View { Rectangle() .fill(Color.clear) .frame(height: 40) .id(Constants.bottomAnchor) } - + private var inputTextView: some View { VStack { HStack(alignment: .bottom, spacing: 8) { @@ -117,7 +118,7 @@ public struct ConversationDetailView: View { Image(systemName: "plus") } .padding(.bottom, 6) - + TextField("conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical) .textFieldStyle(.roundedBorder) .focused($isMessageFieldFocused) diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift index 9ade436e..29002c29 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift @@ -8,18 +8,18 @@ class ConversationDetailViewModel: ObservableObject { var client: Client? var conversation: Conversation - + @Published var isLoadingMessages: Bool = true @Published var messages: [Status] = [] - + @Published var isSendingMessage: Bool = false @Published var newMessageText: String = "" - + init(conversation: Conversation) { self.conversation = conversation messages = [conversation.lastStatus] } - + func fetchMessages() async { guard let client, let lastMessageId = messages.last?.id else { return } do { @@ -27,15 +27,13 @@ class ConversationDetailViewModel: ObservableObject { isLoadingMessages = false messages.insert(contentsOf: context.ancestors, at: 0) messages.append(contentsOf: context.descendants) - } catch { - - } + } catch {} } - + func postMessage() async { guard let client else { return } isSendingMessage = true - var finalText = conversation.accounts.map{ "@\($0.acct)" }.joined(separator: " ") + var finalText = conversation.accounts.map { "@\($0.acct)" }.joined(separator: " ") finalText += " " finalText += newMessageText let data = StatusData(status: finalText, @@ -52,21 +50,24 @@ class ConversationDetailViewModel: ObservableObject { isSendingMessage = false } } - + func handleEvent(event: any StreamEvent) { if let event = event as? StreamEventStatusUpdate, - let index = messages.firstIndex(where: { $0.id == event.status.id }) { + let index = messages.firstIndex(where: { $0.id == event.status.id }) + { messages[index] = event.status } else if let event = event as? StreamEventDelete, - let index = messages.firstIndex(where: { $0.id == event.status }) { + let index = messages.firstIndex(where: { $0.id == event.status }) + { messages.remove(at: index) } else if let event = event as? StreamEventConversation, - event.conversation.id == conversation.id { - self.conversation = event.conversation + event.conversation.id == conversation.id + { + conversation = event.conversation appendNewStatus(status: conversation.lastStatus) } } - + private func appendNewStatus(status: Status) { if !messages.contains(where: { $0.id == status.id }) { messages.append(status) diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift index 00624039..0ae8d542 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift @@ -1,9 +1,9 @@ -import SwiftUI -import Env import DesignSystem -import Network +import Env import Models +import Network import NukeUI +import SwiftUI struct ConversationMessageView: View { @EnvironmentObject private var quickLook: QuickLook @@ -11,12 +11,12 @@ struct ConversationMessageView: View { @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var client: Client @EnvironmentObject private var theme: Theme - + let message: Status let conversation: Conversation - + @State private var isLiked: Bool = false - + var body: some View { let isOwnMessage = message.account.id == currentAccount.account?.id VStack { @@ -49,18 +49,18 @@ struct ConversationMessageView: View { .contextMenu { contextMenu } - + if !isOwnMessage { Spacer() } } - + ForEach(message.mediaAttachments) { media in makeMediaView(media) .padding(.leading, isOwnMessage ? 24 : 0) .padding(.trailing, isOwnMessage ? 0 : 24) } - + if message.id == conversation.lastStatus.id { HStack { if isOwnMessage { @@ -79,7 +79,7 @@ struct ConversationMessageView: View { isLiked = message.favourited == true } } - + @ViewBuilder private var contextMenu: some View { Button { @@ -104,7 +104,7 @@ struct ConversationMessageView: View { withAnimation { isLiked = status.favourited == true } - } catch { } + } catch {} } } label: { Label(isLiked ? "status.action.unfavorite" : "status.action.favorite", @@ -119,7 +119,7 @@ struct ConversationMessageView: View { } } } - + private func makeMediaView(_ attachement: MediaAttachment) -> some View { LazyImage(url: attachement.url) { state in if let image = state.image { @@ -144,7 +144,7 @@ struct ConversationMessageView: View { } } } - + private var likeView: some View { HStack { Spacer() diff --git a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift index 77e67d35..fea179f7 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift @@ -2,15 +2,16 @@ import UIKit public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { public var window: UIWindow? - + public var windowWidth: CGFloat { window?.bounds.size.width ?? UIScreen.main.bounds.size.width } - + public func scene(_ scene: UIScene, - willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions) { + willConnectTo _: UISceneSession, + options _: UIScene.ConnectionOptions) + { guard let windowScene = scene as? UIWindowScene else { return } - self.window = windowScene.keyWindow + window = windowScene.keyWindow } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift index 2d494da0..92663a03 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmojiText.swift @@ -31,9 +31,9 @@ public struct EmojiTextApp: View { .environment(\.layoutDirection, isRTL() ? .rightToLeft : .leftToRight) } } - + private func isRTL() -> Bool { // Arabic, Hebrew, Persian, Urdu, Kurdish, Azeri, Dhivehi - return ["ar", "he", "fa", "ur", "ku", "az", "dv"].contains(self.language) + return ["ar", "he", "fa", "ur", "ku", "az", "dv"].contains(language) } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift new file mode 100644 index 00000000..c8d75abc --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift @@ -0,0 +1,41 @@ +import Env +import Models +import SwiftUI + +public struct FollowRequestButtons: View { + @EnvironmentObject private var currentAccount: CurrentAccount + + let account: Account + let requestUpdated: (() -> Void)? + + public init(account: Account, requestUpdated: (() -> Void)? = nil) { + self.account = account + self.requestUpdated = requestUpdated + } + + public var body: some View { + HStack { + Button { + Task { + await currentAccount.acceptFollowerRequest(id: account.id) + requestUpdated?() + } + } label: { + Text("account.follow-request.accept") + .frame(maxWidth: .infinity) + } + Button { + Task { + await currentAccount.rejectFollowerRequest(id: account.id) + requestUpdated?() + } + } label: { + Text("account.follow-request.reject") + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.bordered) + .disabled(currentAccount.isUpdating) + .padding(.top, 4) + } +} diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift index 1b2fc775..26f78061 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift @@ -19,7 +19,7 @@ public extension View { public struct StatusEditorToolbarItem: ToolbarContent { @EnvironmentObject private var routerPath: RouterPath - + let visibility: Models.Visibility let feedbackGenerator = UISelectionFeedbackGenerator() diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index 10d9119b..a1b14932 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -7,6 +7,8 @@ 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] = [] + @Published public private(set) var followRequests: [Account] = [] + @Published public private(set) var isUpdating: Bool = false private var client: Client? @@ -28,6 +30,7 @@ public class CurrentAccount: ObservableObject { group.addTask { await self.fetchCurrentAccount() } group.addTask { await self.fetchLists() } group.addTask { await self.fetchFollowedTags() } + group.addTask { await self.fetchFollowerRequests() } } } @@ -103,4 +106,37 @@ public class CurrentAccount: ObservableObject { return nil } } + + public func fetchFollowerRequests() async { + guard let client else { return } + do { + followRequests = try await client.get(endpoint: FollowRequests.list) + } catch { + followRequests = [] + } + } + + public func acceptFollowerRequest(id: String) async { + guard let client else { return } + do { + isUpdating = true + defer { + isUpdating = false + } + _ = try await client.post(endpoint: FollowRequests.accept(id: id)) + await fetchFollowerRequests() + } catch {} + } + + public func rejectFollowerRequest(id: String) async { + guard let client else { return } + do { + isUpdating = true + defer { + isUpdating = false + } + _ = try await client.post(endpoint: FollowRequests.reject(id: id)) + await fetchFollowerRequests() + } catch {} + } } diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 6a7461b8..e3ea4c11 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -71,7 +71,6 @@ public class RouterPath: ObservableObject { // That is on the same host as the person that posted the tag, // i.e. not a link that matches the pattern but elsewhere on the internet // In those circumstances, hijack the link and goto the tags page instead - navigate(to: .hashTag(tag: tag, account: nil)) return .handled } else if let mention = status.mentions.first(where: { $0.url == url }) { diff --git a/Packages/Models/Sources/Models/Poll.swift b/Packages/Models/Sources/Models/Poll.swift index ca04f48d..e1ae83e3 100644 --- a/Packages/Models/Sources/Models/Poll.swift +++ b/Packages/Models/Sources/Models/Poll.swift @@ -4,11 +4,11 @@ public struct Poll: Codable, Equatable, Hashable { public static func == (lhs: Poll, rhs: Poll) -> Bool { lhs.id == rhs.id } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } - + public struct Option: Identifiable, Codable { enum CodingKeys: String, CodingKey { case title, votesCount diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 73c69344..fa9425c6 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -58,11 +58,11 @@ public struct Status: AnyStatus, Decodable, Identifiable, Equatable, Hashable { public var viewId: String { id + createdAt + (editedAt ?? "") } - + public static func == (lhs: Status, rhs: Status) -> Bool { lhs.id == rhs.id } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } @@ -132,11 +132,11 @@ public struct ReblogStatus: AnyStatus, Decodable, Identifiable, Equatable, Hasha public var viewId: String { id + createdAt + (editedAt ?? "") } - + public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool { lhs.id == rhs.id } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 66b799a4..6ce8839f 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -146,7 +146,7 @@ public class Client: ObservableObject, Equatable { logResponseOnError(httpResponse: httpResponse, data: data) do { return try decoder.decode(Entity.self, from: data) - } catch let error { + } catch { if let serverError = try? decoder.decode(ServerError.self, from: data) { throw serverError } diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index c59e639b..f9e148e7 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -109,7 +109,7 @@ public enum Accounts: Endpoint { case let .follow(_, notify, reblogs): return [ .init(name: "notify", value: notify ? "true" : "false"), - .init(name: "reblogs", value: reblogs ? "true" : "false") + .init(name: "reblogs", value: reblogs ? "true" : "false"), ] case let .familiarFollowers(withAccount): return [.init(name: "id[]", value: withAccount)] diff --git a/Packages/Network/Sources/Network/Endpoint/FollowRequests.swift b/Packages/Network/Sources/Network/Endpoint/FollowRequests.swift new file mode 100644 index 00000000..a86d174b --- /dev/null +++ b/Packages/Network/Sources/Network/Endpoint/FollowRequests.swift @@ -0,0 +1,22 @@ +import Foundation + +public enum FollowRequests: Endpoint { + case list + case accept(id: String) + case reject(id: String) + + public func path() -> String { + switch self { + case .list: + return "follow_requests" + case let .accept(id): + return "follow_requests/\(id)/authorize" + case let .reject(id): + return "follow_requests/\(id)/reject" + } + } + + public func queryItems() -> [URLQueryItem]? { + nil + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index 7f40a0fc..575015da 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -6,6 +6,7 @@ import Status import SwiftUI struct NotificationRowView: View { + @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var theme: Theme @EnvironmentObject private var routerPath: RouterPath @Environment(\.redactionReasons) private var reasons @@ -19,6 +20,11 @@ struct NotificationRowView: View { VStack(alignment: .leading, spacing: 2) { makeMainLabel(type: type) makeContent(type: type) + if type == .follow_request, + currentAccount.followRequests.map(\.id).contains(notification.account.id) + { + FollowRequestButtons(account: notification.account) + } } } } else { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift index b24ac904..be8451fd 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift @@ -1,8 +1,8 @@ import Foundation -import UIKit import Models import PhotosUI import SwiftUI +import UIKit struct StatusEditorMediaContainer: Identifiable { let id = UUID().uuidString diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift index b0ace485..68b5538b 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift @@ -1,9 +1,9 @@ +import AVKit import DesignSystem import Env import Models import NukeUI import SwiftUI -import AVKit struct StatusEditorMediaView: View { @EnvironmentObject private var theme: Theme @@ -39,7 +39,7 @@ struct StatusEditorMediaView: View { .preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light) } } - + private func makeVideoAttachement(container: StatusEditorMediaContainer) -> some View { ZStack(alignment: .center) { placeholderView @@ -108,7 +108,8 @@ struct StatusEditorMediaView: View { ProgressView() } if mediaAttachement.url != nil, - mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv { + mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv + { Image(systemName: "play.fill") .font(.headline) .tint(.white) @@ -147,7 +148,7 @@ struct StatusEditorMediaView: View { .background(.thinMaterial) .cornerRadius(8) } - + private var placeholderView: some View { Rectangle() .foregroundColor(theme.secondaryBackgroundColor) diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift index 8efb59d4..b4f1c5ba 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift @@ -1,8 +1,8 @@ import Foundation +import PhotosUI +import SwiftUI import UIKit import UniformTypeIdentifiers -import SwiftUI -import PhotosUI @MainActor enum StatusEditorUTTypeSupported: String, CaseIterable { @@ -12,7 +12,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { case image = "public.image" case jpeg = "public.jpeg" case png = "public.png" - + case video = "public.video" case movie = "public.movie" case mp4 = "public.mpeg-4" @@ -21,7 +21,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { static func types() -> [UTType] { [.url, .text, .plainText, .image, .jpeg, .png, .video, .mpeg4Movie, .gif, .movie] } - + var isVideo: Bool { switch self { case .video, .movie, .mp4, .gif: @@ -53,12 +53,12 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { return nil } } - + private func getVideoTransferable(item: NSItemProvider) async -> MovieFileTranseferable? { return await withCheckedContinuation { continuation in _ = item.loadTransferable(type: MovieFileTranseferable.self) { result in switch result { - case .success(let success): + case let .success(success): continuation.resume(with: .success(success)) case .failure: continuation.resume(with: .success(nil)) @@ -70,39 +70,39 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { struct MovieFileTranseferable: Transferable { let url: URL - + static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .movie) { movie in SentTransferredFile(movie.url) } importing: { received in let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)") try FileManager.default.copyItem(at: received.file, to: copy) - return Self.init(url: copy) + return Self(url: copy) } } } struct ImageFileTranseferable: Transferable { let url: URL - + lazy var data: Data? = try? Data(contentsOf: url) lazy var compressedData: Data? = image?.jpegData(compressionQuality: 0.90) lazy var image: UIImage? = UIImage(data: data ?? Data()) - + static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .image) { image in SentTransferredFile(image.url) } importing: { received in let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)") try FileManager.default.copyItem(at: received.file, to: copy) - return Self.init(url: copy) + return Self(url: copy) } } } -extension URL { - public func mimeType() -> String { - if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType { +public extension URL { + func mimeType() -> String { + if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType { return mimeType } else { return "application/octet-stream" diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 0b5e4ef9..9e5da062 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -96,10 +96,10 @@ public struct StatusEditorView: View { .alert("Error while posting", isPresented: $viewModel.showPostingErrorAlert, actions: { - Button("Ok") { } - }, message: { - Text(viewModel.postingError ?? "") - }) + Button("Ok") {} + }, message: { + Text(viewModel.postingError ?? "") + }) .toolbar { if preferences.isOpenAIEnabled { ToolbarItem(placement: .navigationBarTrailing) { diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 723419d1..71c3fe0c 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -24,7 +24,7 @@ public class StatusEditorViewModel: ObservableObject { private var urlLengthAdjustments: Int = 0 private let maxLengthOfUrl = 23 - + private var spoilerTextCount: Int { spoilerOn ? spoilerText.utf16.count : 0 } @@ -61,7 +61,7 @@ public class StatusEditorViewModel: ObservableObject { @Published var embeddedStatus: Status? @Published var customEmojis: [Emoji] = [] - + @Published var postingError: String? @Published var showPostingErrorAlert: Bool = false @@ -143,7 +143,7 @@ public class StatusEditorViewModel: ObservableObject { } isPosting = false return postStatus - } catch let error { + } catch { if let error = error as? Models.ServerError { postingError = error.error showPostingErrorAlert = true @@ -466,9 +466,10 @@ public class StatusEditorViewModel: ObservableObject { mediaAttachment: nil, error: error)) } - + if var imageFile = file as? ImageFileTranseferable, - let image = imageFile.image { + let image = imageFile.image + { medias.append(.init(image: image, movieTransferable: nil, mediaAttachment: nil, @@ -480,7 +481,7 @@ public class StatusEditorViewModel: ObservableObject { error: nil)) } } - + DispatchQueue.main.async { [weak self] in self?.mediasImages = medias self?.processMediasToUpload() @@ -511,22 +512,24 @@ public class StatusEditorViewModel: ObservableObject { do { if let index = indexOf(container: newContainer) { if let image = originalContainer.image, - let data = image.jpegData(compressionQuality: 0.90) { + let data = image.jpegData(compressionQuality: 0.90) + { let uploadedMedia = try await uploadMedia(data: data, mimeType: "image/jpeg") - mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, - movieTransferable: nil, - mediaAttachment: uploadedMedia, - error: nil) + mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, + movieTransferable: nil, + mediaAttachment: uploadedMedia, + error: nil) if let uploadedMedia, uploadedMedia.url == nil { scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) } } else if let videoURL = originalContainer.movieTransferable?.url, - let data = try? Data(contentsOf: videoURL) { + let data = try? Data(contentsOf: videoURL) + { let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType()) mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, - movieTransferable: originalContainer.movieTransferable, - mediaAttachment: uploadedMedia, - error: nil) + movieTransferable: originalContainer.movieTransferable, + mediaAttachment: uploadedMedia, + error: nil) if let uploadedMedia, uploadedMedia.url == nil { scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) } @@ -542,18 +545,19 @@ public class StatusEditorViewModel: ObservableObject { } } } - + private func scheduleAsyncMediaRefresh(mediaAttachement: MediaAttachment) { Task { repeat { if let client, - let index = mediasImages.firstIndex(where: { $0.mediaAttachment?.id == mediaAttachement.id }) { + let index = mediasImages.firstIndex(where: { $0.mediaAttachment?.id == mediaAttachement.id }) + { guard mediasImages[index].mediaAttachment?.url == nil else { return } do { let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id, - description: nil)) + description: nil)) if newAttachement.url != nil { let oldContainer = mediasImages[index] mediasImages[index] = .init(image: oldContainer.image, @@ -561,10 +565,10 @@ public class StatusEditorViewModel: ObservableObject { mediaAttachment: newAttachement, error: nil) } - } catch { } + } catch {} } try? await Task.sleep(for: .seconds(5)) - } while (!Task.isCancelled) + } while !Task.isCancelled } } diff --git a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift index 3296a67b..a62f127f 100644 --- a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift +++ b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift @@ -30,7 +30,7 @@ public struct StatusMediaPreviewView: View { } return sceneDelegate.windowWidth } - + var appLayoutWidth: CGFloat { let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0 var sidebarWidth: CGFloat = 0 @@ -39,7 +39,7 @@ public struct StatusMediaPreviewView: View { } return (.layoutPadding * 2) + avatarColumnWidth + sidebarWidth } - + private var imageMaxHeight: CGFloat { if isNotifications { if UIDevice.current.userInterfaceIdiom == .pad { diff --git a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift index b638aa01..ebe63d44 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift @@ -75,9 +75,10 @@ struct StatusRowContextMenu: View { } label: { Label("status.action.copy-text", systemImage: "doc.on.doc") } - + if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier, - viewModel.status.language != lang { + viewModel.status.language != lang + { Button { Task { await viewModel.translate(userLang: lang) diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 3b0f4a46..f7cd2d1d 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -243,15 +243,15 @@ public struct StatusRowView: View { }) Spacer() } - + makeTranslateView(status: status) if let poll = status.poll { StatusPollView(poll: poll, status: status) } - + embedStatusView - + makeMediasView(status: status) .accessibilityHidden(!viewModel.isFocused) makeCardView(status: status) @@ -314,7 +314,7 @@ public struct StatusRowView: View { } } } - + if let translation = viewModel.translation, !viewModel.isLoadingTranslation { GroupBox { VStack(alignment: .leading, spacing: 4) { @@ -360,12 +360,13 @@ public struct StatusRowView: View { StatusCardView(card: card) } } - + @ViewBuilder private var embedStatusView: some View { if !reasons.contains(.placeholder) { if !viewModel.isCompact, !viewModel.isEmbedLoading, - let embed = viewModel.embeddedStatus { + let embed = viewModel.embeddedStatus + { StatusEmbeddedView(status: embed) } else if viewModel.isEmbedLoading, !viewModel.isCompact { StatusEmbeddedView(status: .placeholder())