mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-16 17:55:13 +00:00
* Support for follow requests (#321) * Run SwiftFormat Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
parent
3ccd66a6bb
commit
9b3b3692ee
42 changed files with 371 additions and 170 deletions
|
@ -1,12 +1,12 @@
|
||||||
import Account
|
import Account
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import Conversations
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Lists
|
import Lists
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
import Conversations
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
extension View {
|
||||||
|
|
|
@ -12,9 +12,9 @@ import Timeline
|
||||||
@main
|
@main
|
||||||
struct IceCubesApp: App {
|
struct IceCubesApp: App {
|
||||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
||||||
@StateObject private var currentInstance = CurrentInstance.shared
|
@StateObject private var currentInstance = CurrentInstance.shared
|
||||||
@StateObject private var currentAccount = CurrentAccount.shared
|
@StateObject private var currentAccount = CurrentAccount.shared
|
||||||
|
@ -23,18 +23,18 @@ struct IceCubesApp: App {
|
||||||
@StateObject private var quickLook = QuickLook()
|
@StateObject private var quickLook = QuickLook()
|
||||||
@StateObject private var theme = Theme.shared
|
@StateObject private var theme = Theme.shared
|
||||||
@StateObject private var sidebarRouterPath = RouterPath()
|
@StateObject private var sidebarRouterPath = RouterPath()
|
||||||
|
|
||||||
@State private var selectedTab: Tab = .timeline
|
@State private var selectedTab: Tab = .timeline
|
||||||
@State private var selectSidebarItem: Tab? = .timeline
|
@State private var selectSidebarItem: Tab? = .timeline
|
||||||
@State private var popToRootTab: Tab = .other
|
@State private var popToRootTab: Tab = .other
|
||||||
@State private var sideBarLoadedTabs: Set<Tab> = Set()
|
@State private var sideBarLoadedTabs: Set<Tab> = Set()
|
||||||
|
|
||||||
private let feedbackGenerator = UISelectionFeedbackGenerator()
|
private let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||||
|
|
||||||
private var availableTabs: [Tab] {
|
private var availableTabs: [Tab] {
|
||||||
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
appView
|
appView
|
||||||
|
@ -71,7 +71,7 @@ struct IceCubesApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var appView: some View {
|
private var appView: some View {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
@ -80,14 +80,14 @@ struct IceCubesApp: App {
|
||||||
tabBarView
|
tabBarView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func badgeFor(tab: Tab) -> Int {
|
private func badgeFor(tab: Tab) -> Int {
|
||||||
if tab == .notifications && selectedTab != tab {
|
if tab == .notifications && selectedTab != tab {
|
||||||
return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount
|
return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sidebarView: some View {
|
private var sidebarView: some View {
|
||||||
SideBarView(selectedTab: $selectedTab,
|
SideBarView(selectedTab: $selectedTab,
|
||||||
popToRootTab: $popToRootTab,
|
popToRootTab: $popToRootTab,
|
||||||
|
@ -116,7 +116,7 @@ struct IceCubesApp: App {
|
||||||
sideBarLoadedTabs.removeAll()
|
sideBarLoadedTabs.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tabBarView: some View {
|
private var tabBarView: some View {
|
||||||
TabView(selection: .init(get: {
|
TabView(selection: .init(get: {
|
||||||
selectedTab
|
selectedTab
|
||||||
|
@ -142,14 +142,14 @@ struct IceCubesApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setNewClientsInEnv(client: Client) {
|
private func setNewClientsInEnv(client: Client) {
|
||||||
currentAccount.setClient(client: client)
|
currentAccount.setClient(client: client)
|
||||||
currentInstance.setClient(client: client)
|
currentInstance.setClient(client: client)
|
||||||
userPreferences.setClient(client: client)
|
userPreferences.setClient(client: client)
|
||||||
watcher.setClient(client: client)
|
watcher.setClient(client: client)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleScenePhase(scenePhase: ScenePhase) {
|
private func handleScenePhase(scenePhase: ScenePhase) {
|
||||||
switch scenePhase {
|
switch scenePhase {
|
||||||
case .background:
|
case .background:
|
||||||
|
@ -166,16 +166,16 @@ struct IceCubesApp: App {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupRevenueCat() {
|
private func setupRevenueCat() {
|
||||||
Purchases.logLevel = .error
|
Purchases.logLevel = .error
|
||||||
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshPushSubs() {
|
private func refreshPushSubs() {
|
||||||
PushNotificationsService.shared.requestPushNotifications()
|
PushNotificationsService.shared.requestPushNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
@CommandsBuilder
|
@CommandsBuilder
|
||||||
private var appMenu: some Commands {
|
private var appMenu: some Commands {
|
||||||
CommandGroup(replacing: .newItem) {
|
CommandGroup(replacing: .newItem) {
|
||||||
|
@ -202,29 +202,29 @@ struct IceCubesApp: App {
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
let themeObserver = ThemeObserverViewController(nibName: nil, bundle: nil)
|
let themeObserver = ThemeObserverViewController(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
func application(_: UIApplication,
|
func application(_: UIApplication,
|
||||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||||
{
|
{
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication,
|
func application(_: UIApplication,
|
||||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
{
|
{
|
||||||
PushNotificationsService.shared.pushToken = deviceToken
|
PushNotificationsService.shared.pushToken = deviceToken
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
Task {
|
Task {
|
||||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
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)
|
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||||
if connectingSceneSession.role == .windowApplication {
|
if connectingSceneSession.role == .windowApplication {
|
||||||
configuration.delegateClass = SceneDelegate.self
|
configuration.delegateClass = SceneDelegate.self
|
||||||
|
@ -236,7 +236,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
class ThemeObserverViewController: UIViewController {
|
class ThemeObserverViewController: UIViewController {
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
print(traitCollection.userInterfaceStyle.rawValue)
|
print(traitCollection.userInterfaceStyle.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,12 @@ struct QuickLookPreview: UIViewControllerRepresentable {
|
||||||
|
|
||||||
class AppQLPreviewController: QLPreviewController {
|
class AppQLPreviewController: QLPreviewController {
|
||||||
private var closeButton: UIBarButtonItem {
|
private var closeButton: UIBarButtonItem {
|
||||||
.init(
|
.init(
|
||||||
title: NSLocalizedString("action.done", comment: ""),
|
title: NSLocalizedString("action.done", comment: ""),
|
||||||
style: .plain,
|
style: .plain,
|
||||||
target: self,
|
target: self,
|
||||||
action: #selector(onCloseButton)
|
action: #selector(onCloseButton)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
|
|
|
@ -100,7 +100,7 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var otherSections: some View {
|
private var otherSections: some View {
|
||||||
Section("settings.section.other") {
|
Section("settings.section.other") {
|
||||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||||
|
|
|
@ -166,6 +166,10 @@
|
||||||
"account.follow.follow" = "Folgen";
|
"account.follow.follow" = "Folgen";
|
||||||
"account.follow.following" = "Gefolgt";
|
"account.follow.following" = "Gefolgt";
|
||||||
"account.follow.requested" = "Angefragt";
|
"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.followers" = "Follower";
|
||||||
"account.following" = "Folgt";
|
"account.following" = "Folgt";
|
||||||
"account.list.create" = "Neue Liste erstellen";
|
"account.list.create" = "Neue Liste erstellen";
|
||||||
|
|
|
@ -169,6 +169,10 @@
|
||||||
"account.follow.follow" = "Follow";
|
"account.follow.follow" = "Follow";
|
||||||
"account.follow.following" = "Following";
|
"account.follow.following" = "Following";
|
||||||
"account.follow.requested" = "Requested";
|
"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.followers" = "Followers";
|
||||||
"account.following" = "Following";
|
"account.following" = "Following";
|
||||||
"account.list.create" = "Create a new list";
|
"account.list.create" = "Create a new list";
|
||||||
|
|
|
@ -166,6 +166,10 @@
|
||||||
"account.follow.follow" = "Seguir";
|
"account.follow.follow" = "Seguir";
|
||||||
"account.follow.following" = "Siguiendo";
|
"account.follow.following" = "Siguiendo";
|
||||||
"account.follow.requested" = "Solicitado";
|
"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.followers" = "Seguidores";
|
||||||
"account.following" = "Siguiendo";
|
"account.following" = "Siguiendo";
|
||||||
"account.list.create" = "Crear una lista nueva";
|
"account.list.create" = "Crear una lista nueva";
|
||||||
|
|
|
@ -166,6 +166,10 @@
|
||||||
"account.follow.follow" = "Segui";
|
"account.follow.follow" = "Segui";
|
||||||
"account.follow.following" = "Segui già";
|
"account.follow.following" = "Segui già";
|
||||||
"account.follow.requested" = "Richiesto";
|
"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.followers" = "Seguito da";
|
||||||
"account.following" = "Seguiti";
|
"account.following" = "Seguiti";
|
||||||
"account.list.create" = "Crea una nuova lista";
|
"account.list.create" = "Crea una nuova lista";
|
||||||
|
|
|
@ -152,6 +152,10 @@
|
||||||
"account.follow.follow" = "フォロー";
|
"account.follow.follow" = "フォロー";
|
||||||
"account.follow.following" = "フォローしている";
|
"account.follow.following" = "フォローしている";
|
||||||
"account.follow.requested" = "リクエストしました";
|
"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.followers" = "フォロワー";
|
||||||
"account.following" = "フォローしている";
|
"account.following" = "フォローしている";
|
||||||
"account.list.create" = "新しいリストを作成";
|
"account.list.create" = "新しいリストを作成";
|
||||||
|
|
|
@ -166,6 +166,10 @@
|
||||||
"account.follow.follow" = "Volg";
|
"account.follow.follow" = "Volg";
|
||||||
"account.follow.following" = "Volgend";
|
"account.follow.following" = "Volgend";
|
||||||
"account.follow.requested" = "Verzocht";
|
"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.followers" = "Volgers";
|
||||||
"account.following" = "Volgend";
|
"account.following" = "Volgend";
|
||||||
"account.list.create" = "Maak een nieuwe lijst";
|
"account.list.create" = "Maak een nieuwe lijst";
|
||||||
|
|
|
@ -167,6 +167,10 @@
|
||||||
"account.follow.follow" = "关注";
|
"account.follow.follow" = "关注";
|
||||||
"account.follow.following" = "正在关注";
|
"account.follow.following" = "正在关注";
|
||||||
"account.follow.requested" = "已申请";
|
"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.followers" = "粉丝";
|
||||||
"account.following" = "关注";
|
"account.following" = "关注";
|
||||||
"account.list.create" = "新建一个列表";
|
"account.list.create" = "新建一个列表";
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import AppAccount
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Env
|
import Env
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
import Models
|
import Models
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import AppAccount
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
@ -51,7 +51,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bestAttemptContent.title = notification.title
|
bestAttemptContent.title = notification.title
|
||||||
if AppAccountsManager.shared.availableAccounts.count > 1 {
|
if AppAccountsManager.shared.availableAccounts.count > 1 {
|
||||||
bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? ""
|
bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? ""
|
||||||
|
|
|
@ -10,6 +10,7 @@ struct AccountDetailHeaderView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var quickLook: QuickLook
|
@EnvironmentObject private var quickLook: QuickLook
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
|
||||||
@ObservedObject var viewModel: AccountDetailViewModel
|
@ObservedObject var viewModel: AccountDetailViewModel
|
||||||
|
@ -95,7 +96,11 @@ struct AccountDetailHeaderView: View {
|
||||||
makeCustomInfoLabel(title: "account.following", count: account.followingCount)
|
makeCustomInfoLabel(title: "account.following", count: account.followingCount)
|
||||||
}
|
}
|
||||||
NavigationLink(value: RouterDestinations.followers(id: account.id)) {
|
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)
|
}.offset(y: 20)
|
||||||
}
|
}
|
||||||
|
@ -136,11 +141,19 @@ struct AccountDetailHeaderView: View {
|
||||||
.offset(y: -40)
|
.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 {
|
VStack {
|
||||||
Text("\(count)")
|
Text("\(count)")
|
||||||
.font(.scaledHeadline)
|
.font(.scaledHeadline)
|
||||||
.foregroundColor(theme.tintColor)
|
.foregroundColor(theme.tintColor)
|
||||||
|
.overlay(alignment: .trailing) {
|
||||||
|
if needsBadge {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.red)
|
||||||
|
.frame(width: 9, height: 9)
|
||||||
|
.offset(x: 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
|
@ -10,9 +10,9 @@ public class AccountsListRowViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
@Published var account: Account
|
@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.account = account
|
||||||
self.relationShip = relationShip
|
self.relationShip = relationShip
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,13 @@ public struct AccountsListRow: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@StateObject var viewModel: AccountsListRowViewModel
|
@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)
|
_viewModel = StateObject(wrappedValue: viewModel)
|
||||||
|
self.isFollowRequest = isFollowRequest
|
||||||
|
self.requestUpdated = requestUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
@ -45,11 +49,17 @@ public struct AccountsListRow: View {
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
routerPath.handle(url: url)
|
routerPath.handle(url: url)
|
||||||
})
|
})
|
||||||
|
if isFollowRequest {
|
||||||
|
FollowRequestButtons(account: viewModel.account,
|
||||||
|
requestUpdated: requestUpdated)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
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,
|
FollowButton(viewModel: .init(accountId: viewModel.account.id,
|
||||||
relationship: viewModel.relationShip,
|
relationship: relationShip,
|
||||||
shouldDisplayNotify: false,
|
shouldDisplayNotify: false,
|
||||||
relationshipUpdated: { _ in }))
|
relationshipUpdated: { _ in }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import SwiftUI
|
||||||
public struct AccountsListView: View {
|
public struct AccountsListView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@StateObject private var viewModel: AccountsListViewModel
|
@StateObject private var viewModel: AccountsListViewModel
|
||||||
@State private var didAppear: Bool = false
|
@State private var didAppear: Bool = false
|
||||||
|
|
||||||
|
@ -26,11 +27,37 @@ public struct AccountsListView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
case let .display(accounts, relationships, nextPageState):
|
case let .display(accounts, relationships, nextPageState):
|
||||||
ForEach(accounts) { account in
|
if case .followers = viewModel.mode,
|
||||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
!currentAccount.followRequests.isEmpty
|
||||||
AccountsListRow(viewModel: .init(account: account,
|
{
|
||||||
relationShip: relationship))
|
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)
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ class AccountsListViewModel: ObservableObject {
|
||||||
maxId: nil))
|
maxId: nil))
|
||||||
case let .favoritedBy(statusId):
|
case let .favoritedBy(statusId):
|
||||||
(accounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
(accounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||||
maxId: nil))
|
maxId: nil))
|
||||||
}
|
}
|
||||||
nextPageId = link?.maxId
|
nextPageId = link?.maxId
|
||||||
relationships = try await client.get(endpoint:
|
relationships = try await client.get(endpoint:
|
||||||
|
@ -95,7 +95,7 @@ class AccountsListViewModel: ObservableObject {
|
||||||
maxId: nextPageId))
|
maxId: nextPageId))
|
||||||
case let .favoritedBy(statusId):
|
case let .favoritedBy(statusId):
|
||||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||||
maxId: nextPageId))
|
maxId: nextPageId))
|
||||||
}
|
}
|
||||||
accounts.append(contentsOf: newAccounts)
|
accounts.append(contentsOf: newAccounts)
|
||||||
let newRelationships: [Relationship] =
|
let newRelationships: [Relationship] =
|
||||||
|
|
|
@ -111,7 +111,7 @@ public struct FollowButton: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(viewModel.isUpdating)
|
.disabled(viewModel.isUpdating)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.toggleReboosts()
|
await viewModel.toggleReboosts()
|
||||||
|
|
|
@ -31,7 +31,8 @@ public struct AppAccount: Codable, Identifiable {
|
||||||
|
|
||||||
public init(server: String,
|
public init(server: String,
|
||||||
accountName: String?,
|
accountName: String?,
|
||||||
oauthToken: OauthToken? = nil) {
|
oauthToken: OauthToken? = nil)
|
||||||
|
{
|
||||||
self.server = server
|
self.server = server
|
||||||
self.accountName = accountName
|
self.accountName = accountName
|
||||||
self.oauthToken = oauthToken
|
self.oauthToken = oauthToken
|
||||||
|
|
|
@ -9,7 +9,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
@ObservedObject var routerPath: RouterPath
|
@ObservedObject var routerPath: RouterPath
|
||||||
|
|
||||||
@State private var accountsViewModel: [AppAccountViewModel] = []
|
@State private var accountsViewModel: [AppAccountViewModel] = []
|
||||||
|
|
||||||
let feedbackGenerator = UIImpactFeedbackGenerator()
|
let feedbackGenerator = UIImpactFeedbackGenerator()
|
||||||
|
|
||||||
private let accountCreationEnabled: Bool
|
private let accountCreationEnabled: Bool
|
||||||
|
@ -22,7 +22,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
self.routerPath = routerPath
|
self.routerPath = routerPath
|
||||||
self.accountCreationEnabled = accountCreationEnabled
|
self.accountCreationEnabled = accountCreationEnabled
|
||||||
self.avatarSize = avatarSize
|
self.avatarSize = avatarSize
|
||||||
|
|
||||||
feedbackGenerator.prepare()
|
feedbackGenerator.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,10 +54,18 @@ public struct AppAccountsSelectorView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var labelView: some View {
|
private var labelView: some View {
|
||||||
if let avatar = currentAccount.account?.avatar {
|
Group {
|
||||||
AvatarView(url: avatar, size: avatarSize)
|
if let avatar = currentAccount.account?.avatar {
|
||||||
} else {
|
AvatarView(url: avatar, size: avatarSize)
|
||||||
EmptyView()
|
} 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 {
|
} else {
|
||||||
appAccounts.currentAccount = viewModel.appAccount
|
appAccounts.currentAccount = viewModel.appAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
feedbackGenerator.impactOccurred(intensity: 0.7)
|
feedbackGenerator.impactOccurred(intensity: 0.7)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Network
|
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct ConversationDetailView: View {
|
public struct ConversationDetailView: View {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
|
@ -16,18 +16,18 @@ public struct ConversationDetailView: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
|
|
||||||
@StateObject private var viewModel: ConversationDetailViewModel
|
@StateObject private var viewModel: ConversationDetailViewModel
|
||||||
|
|
||||||
@FocusState private var isMessageFieldFocused: Bool
|
@FocusState private var isMessageFieldFocused: Bool
|
||||||
|
|
||||||
@State private var scrollProxy: ScrollViewProxy?
|
@State private var scrollProxy: ScrollViewProxy?
|
||||||
@State private var didAppear: Bool = false
|
@State private var didAppear: Bool = false
|
||||||
|
|
||||||
public init(conversation: Conversation) {
|
public init(conversation: Conversation) {
|
||||||
_viewModel = StateObject(wrappedValue: .init(conversation: conversation))
|
_viewModel = StateObject(wrappedValue: .init(conversation: conversation))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
@ -72,7 +72,8 @@ public struct ConversationDetailView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
if viewModel.conversation.accounts.count == 1,
|
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)
|
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||||
.font(.scaledHeadline)
|
.font(.scaledHeadline)
|
||||||
} else {
|
} else {
|
||||||
|
@ -92,7 +93,7 @@ public struct ConversationDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingView: some View {
|
private var loadingView: some View {
|
||||||
ForEach(Status.placeholders()) { message in
|
ForEach(Status.placeholders()) { message in
|
||||||
ConversationMessageView(message: message, conversation: viewModel.conversation)
|
ConversationMessageView(message: message, conversation: viewModel.conversation)
|
||||||
|
@ -100,14 +101,14 @@ public struct ConversationDetailView: View {
|
||||||
.shimmering()
|
.shimmering()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bottomAnchorView: some View {
|
private var bottomAnchorView: some View {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.clear)
|
.fill(Color.clear)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
.id(Constants.bottomAnchor)
|
.id(Constants.bottomAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var inputTextView: some View {
|
private var inputTextView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack(alignment: .bottom, spacing: 8) {
|
HStack(alignment: .bottom, spacing: 8) {
|
||||||
|
@ -117,7 +118,7 @@ public struct ConversationDetailView: View {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
.padding(.bottom, 6)
|
.padding(.bottom, 6)
|
||||||
|
|
||||||
TextField("conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical)
|
TextField("conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.focused($isMessageFieldFocused)
|
.focused($isMessageFieldFocused)
|
||||||
|
|
|
@ -8,18 +8,18 @@ class ConversationDetailViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
var conversation: Conversation
|
var conversation: Conversation
|
||||||
|
|
||||||
@Published var isLoadingMessages: Bool = true
|
@Published var isLoadingMessages: Bool = true
|
||||||
@Published var messages: [Status] = []
|
@Published var messages: [Status] = []
|
||||||
|
|
||||||
@Published var isSendingMessage: Bool = false
|
@Published var isSendingMessage: Bool = false
|
||||||
@Published var newMessageText: String = ""
|
@Published var newMessageText: String = ""
|
||||||
|
|
||||||
init(conversation: Conversation) {
|
init(conversation: Conversation) {
|
||||||
self.conversation = conversation
|
self.conversation = conversation
|
||||||
messages = [conversation.lastStatus]
|
messages = [conversation.lastStatus]
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchMessages() async {
|
func fetchMessages() async {
|
||||||
guard let client, let lastMessageId = messages.last?.id else { return }
|
guard let client, let lastMessageId = messages.last?.id else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -27,15 +27,13 @@ class ConversationDetailViewModel: ObservableObject {
|
||||||
isLoadingMessages = false
|
isLoadingMessages = false
|
||||||
messages.insert(contentsOf: context.ancestors, at: 0)
|
messages.insert(contentsOf: context.ancestors, at: 0)
|
||||||
messages.append(contentsOf: context.descendants)
|
messages.append(contentsOf: context.descendants)
|
||||||
} catch {
|
} catch {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func postMessage() async {
|
func postMessage() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
isSendingMessage = true
|
isSendingMessage = true
|
||||||
var finalText = conversation.accounts.map{ "@\($0.acct)" }.joined(separator: " ")
|
var finalText = conversation.accounts.map { "@\($0.acct)" }.joined(separator: " ")
|
||||||
finalText += " "
|
finalText += " "
|
||||||
finalText += newMessageText
|
finalText += newMessageText
|
||||||
let data = StatusData(status: finalText,
|
let data = StatusData(status: finalText,
|
||||||
|
@ -52,21 +50,24 @@ class ConversationDetailViewModel: ObservableObject {
|
||||||
isSendingMessage = false
|
isSendingMessage = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleEvent(event: any StreamEvent) {
|
func handleEvent(event: any StreamEvent) {
|
||||||
if let event = event as? StreamEventStatusUpdate,
|
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
|
messages[index] = event.status
|
||||||
} else if let event = event as? StreamEventDelete,
|
} 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)
|
messages.remove(at: index)
|
||||||
} else if let event = event as? StreamEventConversation,
|
} else if let event = event as? StreamEventConversation,
|
||||||
event.conversation.id == conversation.id {
|
event.conversation.id == conversation.id
|
||||||
self.conversation = event.conversation
|
{
|
||||||
|
conversation = event.conversation
|
||||||
appendNewStatus(status: conversation.lastStatus)
|
appendNewStatus(status: conversation.lastStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func appendNewStatus(status: Status) {
|
private func appendNewStatus(status: Status) {
|
||||||
if !messages.contains(where: { $0.id == status.id }) {
|
if !messages.contains(where: { $0.id == status.id }) {
|
||||||
messages.append(status)
|
messages.append(status)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Network
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ConversationMessageView: View {
|
struct ConversationMessageView: View {
|
||||||
@EnvironmentObject private var quickLook: QuickLook
|
@EnvironmentObject private var quickLook: QuickLook
|
||||||
|
@ -11,12 +11,12 @@ struct ConversationMessageView: View {
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let message: Status
|
let message: Status
|
||||||
let conversation: Conversation
|
let conversation: Conversation
|
||||||
|
|
||||||
@State private var isLiked: Bool = false
|
@State private var isLiked: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let isOwnMessage = message.account.id == currentAccount.account?.id
|
let isOwnMessage = message.account.id == currentAccount.account?.id
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -49,18 +49,18 @@ struct ConversationMessageView: View {
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
contextMenu
|
contextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isOwnMessage {
|
if !isOwnMessage {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(message.mediaAttachments) { media in
|
ForEach(message.mediaAttachments) { media in
|
||||||
makeMediaView(media)
|
makeMediaView(media)
|
||||||
.padding(.leading, isOwnMessage ? 24 : 0)
|
.padding(.leading, isOwnMessage ? 24 : 0)
|
||||||
.padding(.trailing, isOwnMessage ? 0 : 24)
|
.padding(.trailing, isOwnMessage ? 0 : 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.id == conversation.lastStatus.id {
|
if message.id == conversation.lastStatus.id {
|
||||||
HStack {
|
HStack {
|
||||||
if isOwnMessage {
|
if isOwnMessage {
|
||||||
|
@ -79,7 +79,7 @@ struct ConversationMessageView: View {
|
||||||
isLiked = message.favourited == true
|
isLiked = message.favourited == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var contextMenu: some View {
|
private var contextMenu: some View {
|
||||||
Button {
|
Button {
|
||||||
|
@ -104,7 +104,7 @@ struct ConversationMessageView: View {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLiked = status.favourited == true
|
isLiked = status.favourited == true
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(isLiked ? "status.action.unfavorite" : "status.action.favorite",
|
Label(isLiked ? "status.action.unfavorite" : "status.action.favorite",
|
||||||
|
@ -119,7 +119,7 @@ struct ConversationMessageView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeMediaView(_ attachement: MediaAttachment) -> some View {
|
private func makeMediaView(_ attachement: MediaAttachment) -> some View {
|
||||||
LazyImage(url: attachement.url) { state in
|
LazyImage(url: attachement.url) { state in
|
||||||
if let image = state.image {
|
if let image = state.image {
|
||||||
|
@ -144,7 +144,7 @@ struct ConversationMessageView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var likeView: some View {
|
private var likeView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -2,15 +2,16 @@ import UIKit
|
||||||
|
|
||||||
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||||
public var window: UIWindow?
|
public var window: UIWindow?
|
||||||
|
|
||||||
public var windowWidth: CGFloat {
|
public var windowWidth: CGFloat {
|
||||||
window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scene(_ scene: UIScene,
|
public func scene(_ scene: UIScene,
|
||||||
willConnectTo session: UISceneSession,
|
willConnectTo _: UISceneSession,
|
||||||
options connectionOptions: UIScene.ConnectionOptions) {
|
options _: UIScene.ConnectionOptions)
|
||||||
|
{
|
||||||
guard let windowScene = scene as? UIWindowScene else { return }
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
self.window = windowScene.keyWindow
|
window = windowScene.keyWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,9 @@ public struct EmojiTextApp: View {
|
||||||
.environment(\.layoutDirection, isRTL() ? .rightToLeft : .leftToRight)
|
.environment(\.layoutDirection, isRTL() ? .rightToLeft : .leftToRight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isRTL() -> Bool {
|
private func isRTL() -> Bool {
|
||||||
// Arabic, Hebrew, Persian, Urdu, Kurdish, Azeri, Dhivehi
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ public extension View {
|
||||||
|
|
||||||
public struct StatusEditorToolbarItem: ToolbarContent {
|
public struct StatusEditorToolbarItem: ToolbarContent {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
|
||||||
let visibility: Models.Visibility
|
let visibility: Models.Visibility
|
||||||
let feedbackGenerator = UISelectionFeedbackGenerator()
|
let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ public class CurrentAccount: ObservableObject {
|
||||||
@Published public private(set) var account: Account?
|
@Published public private(set) var account: Account?
|
||||||
@Published public private(set) var lists: [List] = []
|
@Published public private(set) var lists: [List] = []
|
||||||
@Published public private(set) var tags: [Tag] = []
|
@Published public private(set) var tags: [Tag] = []
|
||||||
|
@Published public private(set) var followRequests: [Account] = []
|
||||||
|
@Published public private(set) var isUpdating: Bool = false
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
group.addTask { await self.fetchCurrentAccount() }
|
group.addTask { await self.fetchCurrentAccount() }
|
||||||
group.addTask { await self.fetchLists() }
|
group.addTask { await self.fetchLists() }
|
||||||
group.addTask { await self.fetchFollowedTags() }
|
group.addTask { await self.fetchFollowedTags() }
|
||||||
|
group.addTask { await self.fetchFollowerRequests() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,4 +106,37 @@ public class CurrentAccount: ObservableObject {
|
||||||
return nil
|
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 {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,6 @@ public class RouterPath: ObservableObject {
|
||||||
// That is on the same host as the person that posted the tag,
|
// 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
|
// 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
|
// In those circumstances, hijack the link and goto the tags page instead
|
||||||
|
|
||||||
navigate(to: .hashTag(tag: tag, account: nil))
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
return .handled
|
return .handled
|
||||||
} else if let mention = status.mentions.first(where: { $0.url == url }) {
|
} else if let mention = status.mentions.first(where: { $0.url == url }) {
|
||||||
|
|
|
@ -4,11 +4,11 @@ public struct Poll: Codable, Equatable, Hashable {
|
||||||
public static func == (lhs: Poll, rhs: Poll) -> Bool {
|
public static func == (lhs: Poll, rhs: Poll) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Option: Identifiable, Codable {
|
public struct Option: Identifiable, Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case title, votesCount
|
case title, votesCount
|
||||||
|
|
|
@ -58,11 +58,11 @@ public struct Status: AnyStatus, Decodable, Identifiable, Equatable, Hashable {
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: Status, rhs: Status) -> Bool {
|
public static func == (lhs: Status, rhs: Status) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
@ -132,11 +132,11 @@ public struct ReblogStatus: AnyStatus, Decodable, Identifiable, Equatable, Hasha
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool {
|
public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ public class Client: ObservableObject, Equatable {
|
||||||
logResponseOnError(httpResponse: httpResponse, data: data)
|
logResponseOnError(httpResponse: httpResponse, data: data)
|
||||||
do {
|
do {
|
||||||
return try decoder.decode(Entity.self, from: data)
|
return try decoder.decode(Entity.self, from: data)
|
||||||
} catch let error {
|
} catch {
|
||||||
if let serverError = try? decoder.decode(ServerError.self, from: data) {
|
if let serverError = try? decoder.decode(ServerError.self, from: data) {
|
||||||
throw serverError
|
throw serverError
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ public enum Accounts: Endpoint {
|
||||||
case let .follow(_, notify, reblogs):
|
case let .follow(_, notify, reblogs):
|
||||||
return [
|
return [
|
||||||
.init(name: "notify", value: notify ? "true" : "false"),
|
.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):
|
case let .familiarFollowers(withAccount):
|
||||||
return [.init(name: "id[]", value: withAccount)]
|
return [.init(name: "id[]", value: withAccount)]
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct NotificationRowView: View {
|
struct NotificationRowView: View {
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
@ -19,6 +20,11 @@ struct NotificationRowView: View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
makeMainLabel(type: type)
|
makeMainLabel(type: type)
|
||||||
makeContent(type: type)
|
makeContent(type: type)
|
||||||
|
if type == .follow_request,
|
||||||
|
currentAccount.followRequests.map(\.id).contains(notification.account.id)
|
||||||
|
{
|
||||||
|
FollowRequestButtons(account: notification.account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
|
||||||
import Models
|
import Models
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
struct StatusEditorMediaContainer: Identifiable {
|
struct StatusEditorMediaContainer: Identifiable {
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import AVKit
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVKit
|
|
||||||
|
|
||||||
struct StatusEditorMediaView: View {
|
struct StatusEditorMediaView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@ -39,7 +39,7 @@ struct StatusEditorMediaView: View {
|
||||||
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
|
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeVideoAttachement(container: StatusEditorMediaContainer) -> some View {
|
private func makeVideoAttachement(container: StatusEditorMediaContainer) -> some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
placeholderView
|
placeholderView
|
||||||
|
@ -108,7 +108,8 @@ struct StatusEditorMediaView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
if mediaAttachement.url != nil,
|
if mediaAttachement.url != nil,
|
||||||
mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv {
|
mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv
|
||||||
|
{
|
||||||
Image(systemName: "play.fill")
|
Image(systemName: "play.fill")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.tint(.white)
|
.tint(.white)
|
||||||
|
@ -147,7 +148,7 @@ struct StatusEditorMediaView: View {
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var placeholderView: some View {
|
private var placeholderView: some View {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.foregroundColor(theme.secondaryBackgroundColor)
|
.foregroundColor(theme.secondaryBackgroundColor)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import PhotosUI
|
||||||
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import SwiftUI
|
|
||||||
import PhotosUI
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
|
@ -12,7 +12,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
case image = "public.image"
|
case image = "public.image"
|
||||||
case jpeg = "public.jpeg"
|
case jpeg = "public.jpeg"
|
||||||
case png = "public.png"
|
case png = "public.png"
|
||||||
|
|
||||||
case video = "public.video"
|
case video = "public.video"
|
||||||
case movie = "public.movie"
|
case movie = "public.movie"
|
||||||
case mp4 = "public.mpeg-4"
|
case mp4 = "public.mpeg-4"
|
||||||
|
@ -21,7 +21,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
static func types() -> [UTType] {
|
static func types() -> [UTType] {
|
||||||
[.url, .text, .plainText, .image, .jpeg, .png, .video, .mpeg4Movie, .gif, .movie]
|
[.url, .text, .plainText, .image, .jpeg, .png, .video, .mpeg4Movie, .gif, .movie]
|
||||||
}
|
}
|
||||||
|
|
||||||
var isVideo: Bool {
|
var isVideo: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .video, .movie, .mp4, .gif:
|
case .video, .movie, .mp4, .gif:
|
||||||
|
@ -53,12 +53,12 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getVideoTransferable(item: NSItemProvider) async -> MovieFileTranseferable? {
|
private func getVideoTransferable(item: NSItemProvider) async -> MovieFileTranseferable? {
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
_ = item.loadTransferable(type: MovieFileTranseferable.self) { result in
|
_ = item.loadTransferable(type: MovieFileTranseferable.self) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let success):
|
case let .success(success):
|
||||||
continuation.resume(with: .success(success))
|
continuation.resume(with: .success(success))
|
||||||
case .failure:
|
case .failure:
|
||||||
continuation.resume(with: .success(nil))
|
continuation.resume(with: .success(nil))
|
||||||
|
@ -70,39 +70,39 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
|
|
||||||
struct MovieFileTranseferable: Transferable {
|
struct MovieFileTranseferable: Transferable {
|
||||||
let url: URL
|
let url: URL
|
||||||
|
|
||||||
static var transferRepresentation: some TransferRepresentation {
|
static var transferRepresentation: some TransferRepresentation {
|
||||||
FileRepresentation(contentType: .movie) { movie in
|
FileRepresentation(contentType: .movie) { movie in
|
||||||
SentTransferredFile(movie.url)
|
SentTransferredFile(movie.url)
|
||||||
} importing: { received in
|
} importing: { received in
|
||||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
||||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||||
return Self.init(url: copy)
|
return Self(url: copy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageFileTranseferable: Transferable {
|
struct ImageFileTranseferable: Transferable {
|
||||||
let url: URL
|
let url: URL
|
||||||
|
|
||||||
lazy var data: Data? = try? Data(contentsOf: url)
|
lazy var data: Data? = try? Data(contentsOf: url)
|
||||||
lazy var compressedData: Data? = image?.jpegData(compressionQuality: 0.90)
|
lazy var compressedData: Data? = image?.jpegData(compressionQuality: 0.90)
|
||||||
lazy var image: UIImage? = UIImage(data: data ?? Data())
|
lazy var image: UIImage? = UIImage(data: data ?? Data())
|
||||||
|
|
||||||
static var transferRepresentation: some TransferRepresentation {
|
static var transferRepresentation: some TransferRepresentation {
|
||||||
FileRepresentation(contentType: .image) { image in
|
FileRepresentation(contentType: .image) { image in
|
||||||
SentTransferredFile(image.url)
|
SentTransferredFile(image.url)
|
||||||
} importing: { received in
|
} importing: { received in
|
||||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
||||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||||
return Self.init(url: copy)
|
return Self(url: copy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension URL {
|
public extension URL {
|
||||||
public func mimeType() -> String {
|
func mimeType() -> String {
|
||||||
if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType {
|
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
|
||||||
return mimeType
|
return mimeType
|
||||||
} else {
|
} else {
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
|
|
|
@ -96,10 +96,10 @@ public struct StatusEditorView: View {
|
||||||
.alert("Error while posting",
|
.alert("Error while posting",
|
||||||
isPresented: $viewModel.showPostingErrorAlert,
|
isPresented: $viewModel.showPostingErrorAlert,
|
||||||
actions: {
|
actions: {
|
||||||
Button("Ok") { }
|
Button("Ok") {}
|
||||||
}, message: {
|
}, message: {
|
||||||
Text(viewModel.postingError ?? "")
|
Text(viewModel.postingError ?? "")
|
||||||
})
|
})
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if preferences.isOpenAIEnabled {
|
if preferences.isOpenAIEnabled {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
|
|
||||||
private var urlLengthAdjustments: Int = 0
|
private var urlLengthAdjustments: Int = 0
|
||||||
private let maxLengthOfUrl = 23
|
private let maxLengthOfUrl = 23
|
||||||
|
|
||||||
private var spoilerTextCount: Int {
|
private var spoilerTextCount: Int {
|
||||||
spoilerOn ? spoilerText.utf16.count : 0
|
spoilerOn ? spoilerText.utf16.count : 0
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
@Published var embeddedStatus: Status?
|
@Published var embeddedStatus: Status?
|
||||||
|
|
||||||
@Published var customEmojis: [Emoji] = []
|
@Published var customEmojis: [Emoji] = []
|
||||||
|
|
||||||
@Published var postingError: String?
|
@Published var postingError: String?
|
||||||
@Published var showPostingErrorAlert: Bool = false
|
@Published var showPostingErrorAlert: Bool = false
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
isPosting = false
|
isPosting = false
|
||||||
return postStatus
|
return postStatus
|
||||||
} catch let error {
|
} catch {
|
||||||
if let error = error as? Models.ServerError {
|
if let error = error as? Models.ServerError {
|
||||||
postingError = error.error
|
postingError = error.error
|
||||||
showPostingErrorAlert = true
|
showPostingErrorAlert = true
|
||||||
|
@ -466,9 +466,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
mediaAttachment: nil,
|
mediaAttachment: nil,
|
||||||
error: error))
|
error: error))
|
||||||
}
|
}
|
||||||
|
|
||||||
if var imageFile = file as? ImageFileTranseferable,
|
if var imageFile = file as? ImageFileTranseferable,
|
||||||
let image = imageFile.image {
|
let image = imageFile.image
|
||||||
|
{
|
||||||
medias.append(.init(image: image,
|
medias.append(.init(image: image,
|
||||||
movieTransferable: nil,
|
movieTransferable: nil,
|
||||||
mediaAttachment: nil,
|
mediaAttachment: nil,
|
||||||
|
@ -480,7 +481,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
error: nil))
|
error: nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.mediasImages = medias
|
self?.mediasImages = medias
|
||||||
self?.processMediasToUpload()
|
self?.processMediasToUpload()
|
||||||
|
@ -511,22 +512,24 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
do {
|
do {
|
||||||
if let index = indexOf(container: newContainer) {
|
if let index = indexOf(container: newContainer) {
|
||||||
if let image = originalContainer.image,
|
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")
|
let uploadedMedia = try await uploadMedia(data: data, mimeType: "image/jpeg")
|
||||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||||
movieTransferable: nil,
|
movieTransferable: nil,
|
||||||
mediaAttachment: uploadedMedia,
|
mediaAttachment: uploadedMedia,
|
||||||
error: nil)
|
error: nil)
|
||||||
if let uploadedMedia, uploadedMedia.url == nil {
|
if let uploadedMedia, uploadedMedia.url == nil {
|
||||||
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
||||||
}
|
}
|
||||||
} else if let videoURL = originalContainer.movieTransferable?.url,
|
} 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())
|
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())
|
||||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||||
movieTransferable: originalContainer.movieTransferable,
|
movieTransferable: originalContainer.movieTransferable,
|
||||||
mediaAttachment: uploadedMedia,
|
mediaAttachment: uploadedMedia,
|
||||||
error: nil)
|
error: nil)
|
||||||
if let uploadedMedia, uploadedMedia.url == nil {
|
if let uploadedMedia, uploadedMedia.url == nil {
|
||||||
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
||||||
}
|
}
|
||||||
|
@ -542,18 +545,19 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scheduleAsyncMediaRefresh(mediaAttachement: MediaAttachment) {
|
private func scheduleAsyncMediaRefresh(mediaAttachement: MediaAttachment) {
|
||||||
Task {
|
Task {
|
||||||
repeat {
|
repeat {
|
||||||
if let client,
|
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 {
|
guard mediasImages[index].mediaAttachment?.url == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id,
|
let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id,
|
||||||
description: nil))
|
description: nil))
|
||||||
if newAttachement.url != nil {
|
if newAttachement.url != nil {
|
||||||
let oldContainer = mediasImages[index]
|
let oldContainer = mediasImages[index]
|
||||||
mediasImages[index] = .init(image: oldContainer.image,
|
mediasImages[index] = .init(image: oldContainer.image,
|
||||||
|
@ -561,10 +565,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
mediaAttachment: newAttachement,
|
mediaAttachment: newAttachement,
|
||||||
error: nil)
|
error: nil)
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
try? await Task.sleep(for: .seconds(5))
|
try? await Task.sleep(for: .seconds(5))
|
||||||
} while (!Task.isCancelled)
|
} while !Task.isCancelled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ public struct StatusMediaPreviewView: View {
|
||||||
}
|
}
|
||||||
return sceneDelegate.windowWidth
|
return sceneDelegate.windowWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
var appLayoutWidth: CGFloat {
|
var appLayoutWidth: CGFloat {
|
||||||
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0
|
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0
|
||||||
var sidebarWidth: CGFloat = 0
|
var sidebarWidth: CGFloat = 0
|
||||||
|
@ -39,7 +39,7 @@ public struct StatusMediaPreviewView: View {
|
||||||
}
|
}
|
||||||
return (.layoutPadding * 2) + avatarColumnWidth + sidebarWidth
|
return (.layoutPadding * 2) + avatarColumnWidth + sidebarWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imageMaxHeight: CGFloat {
|
private var imageMaxHeight: CGFloat {
|
||||||
if isNotifications {
|
if isNotifications {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
|
|
@ -75,9 +75,10 @@ struct StatusRowContextMenu: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("status.action.copy-text", systemImage: "doc.on.doc")
|
Label("status.action.copy-text", systemImage: "doc.on.doc")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier,
|
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier,
|
||||||
viewModel.status.language != lang {
|
viewModel.status.language != lang
|
||||||
|
{
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.translate(userLang: lang)
|
await viewModel.translate(userLang: lang)
|
||||||
|
|
|
@ -243,15 +243,15 @@ public struct StatusRowView: View {
|
||||||
})
|
})
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
makeTranslateView(status: status)
|
makeTranslateView(status: status)
|
||||||
|
|
||||||
if let poll = status.poll {
|
if let poll = status.poll {
|
||||||
StatusPollView(poll: poll, status: status)
|
StatusPollView(poll: poll, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
embedStatusView
|
embedStatusView
|
||||||
|
|
||||||
makeMediasView(status: status)
|
makeMediasView(status: status)
|
||||||
.accessibilityHidden(!viewModel.isFocused)
|
.accessibilityHidden(!viewModel.isFocused)
|
||||||
makeCardView(status: status)
|
makeCardView(status: status)
|
||||||
|
@ -314,7 +314,7 @@ public struct StatusRowView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
@ -360,12 +360,13 @@ public struct StatusRowView: View {
|
||||||
StatusCardView(card: card)
|
StatusCardView(card: card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var embedStatusView: some View {
|
private var embedStatusView: some View {
|
||||||
if !reasons.contains(.placeholder) {
|
if !reasons.contains(.placeholder) {
|
||||||
if !viewModel.isCompact, !viewModel.isEmbedLoading,
|
if !viewModel.isCompact, !viewModel.isEmbedLoading,
|
||||||
let embed = viewModel.embeddedStatus {
|
let embed = viewModel.embeddedStatus
|
||||||
|
{
|
||||||
StatusEmbeddedView(status: embed)
|
StatusEmbeddedView(status: embed)
|
||||||
} else if viewModel.isEmbedLoading, !viewModel.isCompact {
|
} else if viewModel.isEmbedLoading, !viewModel.isCompact {
|
||||||
StatusEmbeddedView(status: .placeholder())
|
StatusEmbeddedView(status: .placeholder())
|
||||||
|
|
Loading…
Reference in a new issue