From 4143e82fbc9ed65eb3937c80dc691ed3dc450ed9 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 17 Jan 2023 19:41:46 +0100 Subject: [PATCH] Multi account sidebar + scaled font size on macOS + better iPad / macOS app UX --- IceCubesApp/App/IceCubesApp.swift | 11 +-- IceCubesApp/App/SideBarView.swift | 67 ++++++++++++++----- .../App/Tabs/Settings/AddAccountsView.swift | 14 ++-- .../App/Tabs/Settings/SupportAppView.swift | 8 +-- .../Tabs/Timeline/AddRemoteTimelineVIew.swift | 6 +- .../Account/AccountDetailHeaderView.swift | 12 ++-- .../Sources/Account/AccountDetailView.swift | 29 ++++---- .../Account/AccountDetailViewModel.swift | 17 +++-- .../AccountsList/AccountsListRow.swift | 6 +- .../Sources/AppAccount/AppAccountView.swift | 31 +++++++-- .../AppAccount/AppAccountViewModel.swift | 4 +- .../List/ConversationsListRow.swift | 4 +- .../Sources/DesignSystem/Font.swift | 60 +++++++++++++++++ .../DesignSystem/Views/AvatarView.swift | 3 + .../DesignSystem/Views/EmptyView.swift | 4 +- .../DesignSystem/Views/ErrorView.swift | 4 +- .../DesignSystem/Views/TagRowView.swift | 4 +- .../Sources/Lists/Edit/ListEditView.swift | 2 +- .../Sources/Network/OpenAIClient.swift | 10 +++ .../Notifications/NotificationRowView.swift | 12 ++-- .../Notifications/NotificationsListView.swift | 3 +- .../StatusEditorAccessoryView.swift | 2 +- .../StatusEditorAutoCompleteView.swift | 6 +- .../Status/Editor/StatusEditorView.swift | 4 +- .../Status/Editor/StatusEditorViewModel.swift | 8 +-- .../Status/Embed/StatusEmbededView.swift | 4 +- .../Status/Media/VideoPlayerView.swift | 20 ++++++ .../Sources/Status/Poll/StatusPollView.swift | 6 +- .../Status/Row/StatusActionsView.swift | 8 +-- .../Sources/Status/Row/StatusCardView.swift | 6 +- .../Status/Row/StatusMediaPreviewView.swift | 14 ++-- .../Sources/Status/Row/StatusRowView.swift | 12 ++-- .../Status/Row/StatusRowViewModel.swift | 1 + .../Sources/Timeline/TimelineView.swift | 4 +- 34 files changed, 277 insertions(+), 129 deletions(-) create mode 100644 Packages/DesignSystem/Sources/DesignSystem/Font.swift diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index a244019c..13a8e8ad 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -60,7 +60,6 @@ struct IceCubesApp: App { Button("New post") { sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub) } - .keyboardShortcut("n", modifiers: .command) } } .onChange(of: scenePhase) { scenePhase in @@ -187,10 +186,12 @@ class AppDelegate: NSObject, UIApplicationDelegate { didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { PushNotificationsService.shared.pushToken = deviceToken - Task { - await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) - await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts) - } + #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) {} diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 2d861783..fe2b5546 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -5,6 +5,7 @@ import Env import SwiftUI struct SideBarView: View { + @EnvironmentObject private var appAccounts: AppAccountsManager @EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var theme: Theme @EnvironmentObject private var watcher: StreamWatcher @@ -41,14 +42,14 @@ struct SideBarView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24, height: 24) - .foregroundColor(tab == selectedTab ? theme.tintColor : .gray) + .foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor) if let badge = badgeFor(tab: tab), badge > 0 { ZStack { Circle() .fill(.red) Text(String(badge)) .foregroundColor(.white) - .font(.footnote) + .font(.caption) } .frame(width: 20, height: 20) .offset(x: 10, y: -10) @@ -71,28 +72,58 @@ struct SideBarView: View { .keyboardShortcut("n", modifiers: .command) } + private func makeAccountButton(account: AppAccount) -> some View { + Button { + if account.id == appAccounts.currentAccount.id { + selectedTab = .profile + } else { + withAnimation { + appAccounts.currentAccount = account + } + } + } label: { + AppAccountView(viewModel: .init(appAccount: account, isCompact: true)) + } + .frame(width: .sidebarWidth, height: 50) + .padding(.vertical, 8) + .background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ? + theme.secondaryBackgroundColor : .clear) + } + + private var tabsView: some View { + ForEach(tabs) { tab in + Button { + if tab == selectedTab { + popToRootTab = .other + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + popToRootTab = tab + } + } + selectedTab = tab + if tab == .notifications { + watcher.unreadNotificationsCount = 0 + userPreferences.pushNotificationsCount = 0 + } + } label: { + makeIconForTab(tab: tab) + } + .background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear) + } + } + var body: some View { HStack(spacing: 0) { ScrollView { VStack(alignment: .center) { - profileView - ForEach(tabs) { tab in - Button { - if tab == selectedTab { - popToRootTab = .other - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - popToRootTab = tab - } + if appAccounts.availableAccounts.isEmpty { + tabsView + } else { + ForEach(appAccounts.availableAccounts) { account in + makeAccountButton(account: account) + if account.id == appAccounts.currentAccount.id { + tabsView } - selectedTab = tab - if tab == .notifications { - watcher.unreadNotificationsCount = 0 - userPreferences.pushNotificationsCount = 0 - } - } label: { - makeIconForTab(tab: tab) } - .background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear) } postButton .padding(.top, 12) diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index b1743ecc..ef92cfc2 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -118,7 +118,7 @@ struct AddAccountView: View { .tint(theme.labelColor) } else { Text("Sign in") - .font(.headline) + .font(.scaledHeadline) } Spacer() } @@ -139,13 +139,13 @@ struct AddAccountView: View { } label: { VStack(alignment: .leading, spacing: 4) { Text(instance.name) - .font(.headline) + .font(.scaledHeadline) .foregroundColor(.primary) Text(instance.info?.shortDescription ?? "") - .font(.body) + .font(.scaledBody) .foregroundColor(.gray) Text("\(instance.users) users ⸱ \(instance.statuses) posts") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } } @@ -158,13 +158,13 @@ struct AddAccountView: View { private var placeholderRow: some View { VStack(alignment: .leading, spacing: 4) { Text("Loading...") - .font(.headline) + .font(.scaledHeadline) .foregroundColor(.primary) Text("Loading, loading, loading ....") - .font(.body) + .font(.scaledBody) .foregroundColor(.gray) Text("Loading ...") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } .redacted(reason: .placeholder) diff --git a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift index 36cccc11..e109115e 100644 --- a/IceCubesApp/App/Tabs/Settings/SupportAppView.swift +++ b/IceCubesApp/App/Tabs/Settings/SupportAppView.swift @@ -71,9 +71,9 @@ struct SupportAppView: View { HStack { VStack(alignment: .leading) { Text("Loading ...") - .font(.subheadline) + .font(.scaledSubheadline) Text("Loading subtitle...") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } .padding(.vertical, 8) @@ -86,9 +86,9 @@ struct SupportAppView: View { HStack { VStack(alignment: .leading) { Text(tip.title) - .font(.subheadline) + .font(.scaledSubheadline) Text(tip.subtitle) - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } Spacer() diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift index c1660580..455f191d 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineVIew.swift @@ -89,13 +89,13 @@ struct AddRemoteTimelineView: View { } label: { VStack(alignment: .leading, spacing: 4) { Text(instance.name) - .font(.headline) + .font(.scaledHeadline) .foregroundColor(.primary) Text(instance.info?.shortDescription ?? "") - .font(.body) + .font(.scaledBody) .foregroundColor(.gray) Text("\(instance.users) users ⸱ \(instance.statuses) posts") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } } diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index a58bb1b5..163783c2 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -56,7 +56,7 @@ struct AccountDetailHeaderView: View { if viewModel.relationship?.followedBy == true { Text("Follows You") - .font(.footnote) + .font(.scaledFootnote) .fontWeight(.semibold) .padding(4) .background(.ultraThinMaterial) @@ -109,9 +109,9 @@ struct AccountDetailHeaderView: View { HStack { VStack(alignment: .leading, spacing: 0) { EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) - .font(.headline) + .font(.scaledHeadline) Text("@\(account.acct)") - .font(.callout) + .font(.scaledCallout) .foregroundColor(.gray) } Spacer() @@ -124,7 +124,7 @@ struct AccountDetailHeaderView: View { } } EmojiTextApp(account.note.asMarkdown, emojis: account.emojis) - .font(.body) + .font(.scaledBody) .padding(.top, 8) .environment(\.openURL, OpenURLAction { url in routerPath.handle(url: url) @@ -137,10 +137,10 @@ struct AccountDetailHeaderView: View { private func makeCustomInfoLabel(title: String, count: Int) -> some View { VStack { Text("\(count)") - .font(.headline) + .font(.scaledHeadline) .foregroundColor(theme.tintColor) Text(title) - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index cd1f53cb..9648dbc7 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -77,16 +77,21 @@ public struct AccountDetailView: View { .background(theme.primaryBackgroundColor) } .onAppear { + guard reasons != .placeholder else { return } + isCurrentUser = currentAccount.account?.id == viewModel.accountId + viewModel.isCurrentUser = isCurrentUser + viewModel.client = client Task { - guard reasons != .placeholder else { return } - isCurrentUser = currentAccount.account?.id == viewModel.accountId - viewModel.isCurrentUser = isCurrentUser - viewModel.client = client await viewModel.fetchAccount() + } + Task { if viewModel.statuses.isEmpty { await viewModel.fetchStatuses() } } + Task { + await viewModel.fetchFamilliarFollowers() + } } .refreshable { Task { @@ -150,7 +155,7 @@ public struct AccountDetailView: View { } label: { VStack(alignment: .leading, spacing: 0) { Text("About") - .font(.callout) + .font(.scaledCallout) Text("\(viewModel.fields.count) fields") .font(.caption2) } @@ -167,7 +172,7 @@ public struct AccountDetailView: View { } label: { VStack(alignment: .leading, spacing: 0) { Text("#\(tag.name)") - .font(.callout) + .font(.scaledCallout) Text("\(tag.statusesCount) posts") .font(.caption2) } @@ -185,7 +190,7 @@ public struct AccountDetailView: View { if !viewModel.familiarFollowers.isEmpty { VStack(alignment: .leading, spacing: 2) { Text("Also followed by") - .font(.headline) + .font(.scaledHeadline) .padding(.leading, .layoutPadding) ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 0) { @@ -211,7 +216,7 @@ public struct AccountDetailView: View { ForEach(viewModel.fields) { field in VStack(alignment: .leading, spacing: 2) { Text(field.name) - .font(.headline) + .font(.scaledHeadline) HStack { if field.verifiedAt != nil { Image(systemName: "checkmark.seal") @@ -220,7 +225,7 @@ public struct AccountDetailView: View { EmojiTextApp(field.value.asMarkdown, emojis: viewModel.account?.emojis ?? []) .foregroundColor(theme.tintColor) } - .font(.body) + .font(.scaledBody) } .listRowBackground(field.verifiedAt != nil ? Color.green.opacity(0.15) : theme.primaryBackgroundColor) } @@ -273,7 +278,7 @@ public struct AccountDetailView: View { } .padding(.vertical, 8) .padding(.horizontal, .layoutPadding) - .font(.headline) + .font(.scaledHeadline) .foregroundColor(theme.labelColor) } .contextMenu { @@ -317,7 +322,7 @@ public struct AccountDetailView: View { ForEach(viewModel.pinned) { status in VStack(alignment: .leading) { Label("Pinned post", systemImage: "pin.fill") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) .fontWeight(.semibold) StatusRowView(viewModel: .init(status: status)) @@ -336,7 +341,7 @@ public struct AccountDetailView: View { switch viewModel.accountState { case let .data(account): EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) - .font(.headline) + .font(.scaledHeadline) default: EmptyView() } diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index b3055c23..4898def9 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -105,7 +105,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { let account: Account let featuredTags: [FeaturedTag] let relationships: [Relationship] - let familiarFollowers: [FamiliarAccounts] } func fetchAccount() async { @@ -119,8 +118,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { featuredTags = data.featuredTags featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt } relationship = data.relationships.first - familiarFollowers = data.familiarFollowers.first?.accounts ?? [] - } catch { if let account { accountState = .data(account: account) @@ -130,21 +127,23 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } - func fetchAccountData(accountId: String, client: Client) async throws -> AccountData { + private func fetchAccountData(accountId: String, client: Client) async throws -> AccountData { async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId)) async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId)) if client.isAuth && !isCurrentUser { async let relationships: [Relationship] = client.get(endpoint: Accounts.relationships(ids: [accountId])) - async let familiarFollowers: [FamiliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId)) return try await .init(account: account, featuredTags: featuredTags, - relationships: relationships, - familiarFollowers: familiarFollowers) + relationships: relationships) } return try await .init(account: account, featuredTags: featuredTags, - relationships: [], - familiarFollowers: []) + relationships: []) + } + + func fetchFamilliarFollowers() async { + let familiarFollowers: [FamiliarAccounts]? = try? await client?.get(endpoint: Accounts.familiarFollowers(withAccount: accountId)) + self.familiarFollowers = familiarFollowers?.first?.accounts ?? [] } func fetchStatuses() async { diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index 45abde55..df9f0011 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -34,13 +34,13 @@ public struct AccountsListRow: View { AvatarView(url: viewModel.account.avatar, size: .status) VStack(alignment: .leading, spacing: 2) { EmojiTextApp(viewModel.account.safeDisplayName.asMarkdown, emojis: viewModel.account.emojis) - .font(.subheadline) + .font(.scaledSubheadline) .fontWeight(.semibold) Text("@\(viewModel.account.acct)") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) EmojiTextApp(viewModel.account.note.asMarkdown, emojis: viewModel.account.emojis) - .font(.footnote) + .font(.scaledFootnote) .lineLimit(3) .environment(\.openURL, OpenURLAction { url in routerPath.handle(url: url) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift index 4eb2e269..db1d75ee 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift @@ -13,6 +13,30 @@ public struct AppAccountView: View { } public var body: some View { + Group { + if viewModel.isCompact { + compactView + } else { + fullView + } + } + .onAppear { + Task { + await viewModel.fetchAccount() + } + } + } + + @ViewBuilder + private var compactView: some View { + HStack { + if let account = viewModel.account { + AvatarView(url: account.avatar) + } + } + } + + private var fullView: some View { HStack { if let account = viewModel.account { ZStack(alignment: .topTrailing) { @@ -28,7 +52,7 @@ public struct AppAccountView: View { if let account = viewModel.account { EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) Text("\(account.username)@\(viewModel.appAccount.server)") - .font(.subheadline) + .font(.scaledSubheadline) .foregroundColor(.gray) } } @@ -36,11 +60,6 @@ public struct AppAccountView: View { Image(systemName: "chevron.right") .foregroundColor(.gray) } - .onAppear { - Task { - await viewModel.fetchAccount() - } - } .onTapGesture { if appAccounts.currentAccount.id == viewModel.appAccount.id, let account = viewModel.account diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index 373fb9f3..21d093d1 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -6,6 +6,7 @@ import SwiftUI public class AppAccountViewModel: ObservableObject { let appAccount: AppAccount let client: Client + let isCompact: Bool @Published var account: Account? @@ -13,8 +14,9 @@ public class AppAccountViewModel: ObservableObject { "@\(account?.acct ?? "...")@\(appAccount.server)" } - public init(appAccount: AppAccount) { + public init(appAccount: AppAccount, isCompact: Bool = false) { self.appAccount = appAccount + self.isCompact = isCompact client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) } diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift index 7d545321..2d47b75b 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift @@ -20,7 +20,7 @@ struct ConversationsListRow: View { VStack(alignment: .leading, spacing: 4) { HStack { Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", ")) - .font(.headline) + .font(.scaledHeadline) .foregroundColor(theme.labelColor) .multilineTextAlignment(.leading) Spacer() @@ -30,7 +30,7 @@ struct ConversationsListRow: View { .frame(width: 10, height: 10) } Text(conversation.lastStatus.createdAt.formatted) - .font(.footnote) + .font(.scaledFootnote) } Text(conversation.lastStatus.content.asRawText) .multilineTextAlignment(.leading) diff --git a/Packages/DesignSystem/Sources/DesignSystem/Font.swift b/Packages/DesignSystem/Sources/DesignSystem/Font.swift new file mode 100644 index 00000000..b3d10637 --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/Font.swift @@ -0,0 +1,60 @@ +import SwiftUI + +extension Font { + public static var scaledTitle: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 28)) + } else { + return .title + } + } + + public static var scaledHeadline: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 19), weight: .semibold) + } else { + return .headline + } + } + + public static var scaledBody: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 19)) + } else { + return .body + } + } + + public static var scaledCallout: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 17)) + } else { + return .callout + } + } + + public static var scaledSubheadline: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 16)) + } else { + return .subheadline + } + } + + + public static var scaledFootnote: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 15)) + } else { + return .footnote + } + } + + public static var scaledCaption: Font { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .system(size: UIFontMetrics.default.scaledValue(for: 14)) + } else { + return .caption + } + } +} diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift index 12c9272e..a521863c 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -14,6 +14,9 @@ public struct AvatarView: View { case .account: return .init(width: 80, height: 80) case .status: + if ProcessInfo.processInfo.isiOSAppOnMac { + return .init(width: 48, height: 48) + } return .init(width: 40, height: 40) case .embed: return .init(width: 34, height: 34) diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift index b3294b27..8f78a863 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/EmptyView.swift @@ -18,10 +18,10 @@ public struct EmptyView: View { .aspectRatio(contentMode: .fit) .frame(maxHeight: 50) Text(title) - .font(.title) + .font(.scaledTitle) .padding(.top, 16) Text(message) - .font(.subheadline) + .font(.scaledSubheadline) .multilineTextAlignment(.center) .foregroundColor(.gray) } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift index cbe4292a..fe809c1a 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ErrorView.swift @@ -20,10 +20,10 @@ public struct ErrorView: View { .aspectRatio(contentMode: .fit) .frame(maxHeight: 50) Text(title) - .font(.title) + .font(.scaledTitle) .padding(.top, 16) Text(message) - .font(.subheadline) + .font(.scaledSubheadline) .multilineTextAlignment(.center) .foregroundColor(.gray) Button { diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift index 2db7627e..18b4ed33 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift @@ -15,9 +15,9 @@ public struct TagRowView: View { HStack { VStack(alignment: .leading) { Text("#\(tag.name)") - .font(.headline) + .font(.scaledHeadline) Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } Spacer() diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift index 49050a50..aa3b1d8f 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift @@ -35,7 +35,7 @@ public struct ListEditView: View { emojis: account.emojis) Text("@\(account.acct)") .foregroundColor(.gray) - .font(.footnote) + .font(.scaledFootnote) } } .listRowBackground(theme.primaryBackgroundColor) diff --git a/Packages/Network/Sources/Network/OpenAIClient.swift b/Packages/Network/Sources/Network/OpenAIClient.swift index cf31222b..638cb089 100644 --- a/Packages/Network/Sources/Network/OpenAIClient.swift +++ b/Packages/Network/Sources/Network/OpenAIClient.swift @@ -75,6 +75,16 @@ public struct OpenAIClient { public let object: String public let model: String public let choices: [Choice] + + public var trimmedText: String { + guard var text = choices.first?.text else { + return "" + } + while text.first?.isNewline == true || text.first?.isWhitespace == true { + text.removeFirst() + } + return text + } } public init() {} diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index ea91610c..5177f32e 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -56,18 +56,18 @@ struct NotificationRowView: View { append: { Text(" ") + Text(type.label()) - .font(.subheadline) + .font(.scaledSubheadline) .fontWeight(.regular) + Text(" ⸱ ") - .font(.footnote) + .font(.scaledFootnote) .fontWeight(.regular) .foregroundColor(.gray) + Text(notification.createdAt.formatted) - .font(.footnote) + .font(.scaledFootnote) .fontWeight(.regular) .foregroundColor(.gray) }) - .font(.subheadline) + .font(.scaledSubheadline) .fontWeight(.semibold) Spacer() } @@ -93,14 +93,14 @@ struct NotificationRowView: View { } else { Group { Text("@\(notification.account.acct)") - .font(.callout) + .font(.scaledCallout) .foregroundColor(.gray) if type == .follow { EmojiTextApp(notification.account.note.asMarkdown, emojis: notification.account.emojis) .lineLimit(3) - .font(.callout) + .font(.scaledCallout) .foregroundColor(.gray) .environment(\.openURL, OpenURLAction { url in routerPath.handle(url: url) diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index a4ac8b01..79ae1eec 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -23,7 +23,8 @@ public struct NotificationsListView: View { .padding(.top, 16) .frame(maxWidth: .maxColumnWidth) } - .padding(.horizontal, .layoutPadding) + .padding(.leading, .layoutPadding + 12) + .padding(.trailing, .layoutPadding) .padding(.top, .layoutPadding) .background(theme.primaryBackgroundColor) } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 5ae13489..cd1f4c42 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -158,7 +158,7 @@ struct StatusEditorAccessoryView: View { private var characterCountView: some View { Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)") .foregroundColor(.gray) - .font(.callout) + .font(.scaledCallout) } private var availableLanguages: [(String, String?, String?)] { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift index 155bcd6a..db787a69 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift @@ -34,10 +34,10 @@ struct StatusEditorAutoCompleteView: View { VStack(alignment: .leading) { EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(theme.labelColor) Text("@\(account.acct)") - .font(.caption) + .font(.scaledCaption) .foregroundColor(theme.tintColor) } } @@ -51,7 +51,7 @@ struct StatusEditorAutoCompleteView: View { viewModel.selectHashtagSuggestion(tag: tag) } label: { Text("#\(tag.name)") - .font(.caption) + .font(.scaledCaption) .foregroundColor(theme.tintColor) } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 6d7d3a93..24eda040 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -164,7 +164,7 @@ public struct StatusEditorView: View { VStack(alignment: .leading, spacing: 4) { privacyMenu Text("@\(account.acct)") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } Spacer() @@ -188,7 +188,7 @@ public struct StatusEditorView: View { Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName) Image(systemName: "chevron.down") } - .font(.footnote) + .font(.scaledFootnote) .padding(4) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index aa44f171..f53b1681 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -358,12 +358,8 @@ public class StatusEditorViewModel: ObservableObject { do { let client = OpenAIClient() let response = try await client.request(prompt) - if var text = response.choices.first?.text { - text.removeFirst() - text.removeFirst() - backupStatusText = statusText - replaceTextWith(text: text) - } + backupStatusText = statusText + replaceTextWith(text: response.trimmedText) } catch {} } diff --git a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift index f4814ff8..61206677 100644 --- a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift +++ b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift @@ -36,14 +36,14 @@ public struct StatusEmbeddedView: View { AvatarView(url: account.avatar, size: .embed) VStack(alignment: .leading, spacing: 0) { EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: account.emojis) - .font(.footnote) + .font(.scaledFootnote) .fontWeight(.semibold) Group { Text("@\(account.acct)") + Text(" ⸱ ") + Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted) } - .font(.caption) + .font(.scaledCaption) .foregroundColor(.gray) } } diff --git a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift index e2608864..ea4a9a46 100644 --- a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift +++ b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift @@ -20,6 +20,14 @@ class VideoPlayerViewModel: ObservableObject { self?.player?.play() } } + + func pause() { + player?.pause() + } + + func play() { + player?.play() + } deinit { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: self.player) @@ -27,12 +35,24 @@ class VideoPlayerViewModel: ObservableObject { } struct VideoPlayerView: View { + @Environment(\.scenePhase) private var scenePhase @StateObject var viewModel: VideoPlayerViewModel + var body: some View { VStack { VideoPlayer(player: viewModel.player) }.onAppear { viewModel.preparePlayer() } + .onChange(of: scenePhase, perform: { scenePhase in + switch scenePhase { + case .background, .inactive: + viewModel.pause() + case .active: + viewModel.play() + default: + break + } + }) } } diff --git a/Packages/Status/Sources/Status/Poll/StatusPollView.swift b/Packages/Status/Sources/Status/Poll/StatusPollView.swift index 068dd1cd..bc307b66 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollView.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollView.swift @@ -47,7 +47,7 @@ public struct StatusPollView: View { if !viewModel.votes.isEmpty || viewModel.poll.expired { Spacer() Text("\(percentForOption(option: option)) %") - .font(.subheadline) + .font(.scaledSubheadline) .frame(width: 40) } } @@ -74,7 +74,7 @@ public struct StatusPollView: View { Text(viewModel.poll.expiresAt.asDate, style: .timer) } } - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } @@ -120,7 +120,7 @@ public struct StatusPollView: View { } Text(option.title) .foregroundColor(.white) - .font(.body) + .font(.scaledBody) } .padding(.leading, 12) } diff --git a/Packages/Status/Sources/Status/Row/StatusActionsView.swift b/Packages/Status/Sources/Status/Row/StatusActionsView.swift index b6c95695..8d07731a 100644 --- a/Packages/Status/Sources/Status/Row/StatusActionsView.swift +++ b/Packages/Status/Sources/Status/Row/StatusActionsView.swift @@ -80,7 +80,7 @@ struct StatusActionsView: View { .foregroundColor(action.tintColor(viewModel: viewModel, theme: theme)) if let count = action.count(viewModel: viewModel, theme: theme) { Text("\(count)") - .font(.footnote) + .font(.scaledFootnote) } } } @@ -114,14 +114,14 @@ struct StatusActionsView: View { } } } - .font(.caption) + .font(.scaledCaption) .foregroundColor(.gray) if viewModel.favouritesCount > 0 { Divider() NavigationLink(value: RouterDestinations.favouritedBy(id: viewModel.status.id)) { Text("\(viewModel.favouritesCount) favorites") - .font(.callout) + .font(.scaledCallout) Spacer() Image(systemName: "chevron.right") } @@ -130,7 +130,7 @@ struct StatusActionsView: View { Divider() NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) { Text("\(viewModel.reblogsCount) boosts") - .font(.callout) + .font(.scaledCallout) Spacer() Image(systemName: "chevron.right") } diff --git a/Packages/Status/Sources/Status/Row/StatusCardView.swift b/Packages/Status/Sources/Status/Row/StatusCardView.swift index 2825951d..7d7628e8 100644 --- a/Packages/Status/Sources/Status/Row/StatusCardView.swift +++ b/Packages/Status/Sources/Status/Row/StatusCardView.swift @@ -33,16 +33,16 @@ public struct StatusCardView: View { HStack { VStack(alignment: .leading, spacing: 6) { Text(title) - .font(.headline) + .font(.scaledHeadline) .lineLimit(3) if let description = card.description, !description.isEmpty { Text(description) - .font(.body) + .font(.scaledBody) .foregroundColor(.gray) .lineLimit(3) } Text(card.url.host() ?? card.url.absoluteString) - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(theme.tintColor) .lineLimit(1) } diff --git a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift index 8bf073b2..e4cbb975 100644 --- a/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift +++ b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift @@ -174,14 +174,14 @@ public struct StatusMediaPreviewView: View { content: { image in image .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxHeight: imageMaxHeight) + .aspectRatio(contentMode: .fit) + .frame(maxHeight: isNotifications ? imageMaxHeight : nil) .cornerRadius(4) }, placeholder: { RoundedRectangle(cornerRadius: 4) .fill(Color.gray) - .frame(maxHeight: imageMaxHeight) + .frame(maxHeight: isNotifications ? imageMaxHeight : nil) .shimmering() } ) @@ -213,11 +213,11 @@ public struct StatusMediaPreviewView: View { RoundedRectangle(cornerRadius: 4) .fill(Color.gray) .frame(maxHeight: imageMaxHeight) - .frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) + .frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) .shimmering() } } - .frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) + .frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) .frame(height: imageMaxHeight) if sensitive { cornerSensitiveButton @@ -228,7 +228,7 @@ public struct StatusMediaPreviewView: View { isAltAlertDisplayed = true } label: { Text("ALT") - .font(.footnote) + .font(.scaledFootnote) } .padding(4) .background(.thinMaterial) @@ -243,7 +243,7 @@ public struct StatusMediaPreviewView: View { } } } - .frame(width: isNotifications ? imageMaxHeight : nil) + .frame(maxWidth: isNotifications ? imageMaxHeight : nil) .frame(height: imageMaxHeight) } .onTapGesture { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index d9ec2a47..a3d94cb7 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -106,7 +106,7 @@ public struct StatusRowView: View { Text("You boosted") } } - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) .fontWeight(.semibold) .onTapGesture { @@ -131,7 +131,7 @@ public struct StatusRowView: View { Text("Replied to") Text(mention.username) } - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) .fontWeight(.semibold) .onTapGesture { @@ -179,7 +179,7 @@ public struct StatusRowView: View { Group { if !status.spoilerText.isEmpty { EmojiTextApp(status.spoilerText.asMarkdown, emojis: status.emojis) - .font(.body) + .font(.scaledBody) Button { withAnimation { viewModel.displaySpoiler.toggle() @@ -192,7 +192,7 @@ public struct StatusRowView: View { if !viewModel.displaySpoiler { HStack { EmojiTextApp(status.content.asMarkdown, emojis: status.emojis) - .font(.body) + .font(.scaledBody) .environment(\.openURL, OpenURLAction { url in routerPath.handleStatus(status: status, url: url) }) @@ -249,7 +249,7 @@ public struct StatusRowView: View { } VStack(alignment: .leading, spacing: 0) { EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis) - .font(.headline) + .font(.scaledHeadline) .fontWeight(.semibold) Group { Text("@\(status.account.acct)") + @@ -258,7 +258,7 @@ public struct StatusRowView: View { Text(" ⸱ ") + Text(Image(systemName: viewModel.status.visibility.iconName)) } - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index c0ab9912..32b1b96d 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -60,6 +60,7 @@ public class StatusRowViewModel: ObservableObject { } func navigateToDetail(routerPath: RouterPath) { + guard !isFocused else { return } if isRemote, let url = status.reblog?.url ?? status.url { routerPath.navigate(to: .remoteStatusDetail(url: url)) } else { diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index d7701dc8..68c999c1 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -141,9 +141,9 @@ public struct TimelineView: View { HStack { VStack(alignment: .leading, spacing: 4) { Text("#\(tag.name)") - .font(.headline) + .font(.scaledHeadline) Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants") - .font(.footnote) + .font(.scaledFootnote) .foregroundColor(.gray) } Spacer()