Multi account sidebar + scaled font size on macOS + better iPad / macOS app UX

This commit is contained in:
Thomas Ricouard 2023-01-17 19:41:46 +01:00
parent bb72327f52
commit 4143e82fbc
34 changed files with 277 additions and 129 deletions

View file

@ -60,7 +60,6 @@ struct IceCubesApp: App {
Button("New post") { Button("New post") {
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub) sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub)
} }
.keyboardShortcut("n", modifiers: .command)
} }
} }
.onChange(of: scenePhase) { scenePhase in .onChange(of: scenePhase) { scenePhase in
@ -187,10 +186,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{ {
PushNotificationsService.shared.pushToken = deviceToken PushNotificationsService.shared.pushToken = deviceToken
#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
} }
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}

View file

@ -5,6 +5,7 @@ import Env
import SwiftUI import SwiftUI
struct SideBarView<Content: View>: View { struct SideBarView<Content: View>: View {
@EnvironmentObject private var appAccounts: AppAccountsManager
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var watcher: StreamWatcher
@ -41,14 +42,14 @@ struct SideBarView<Content: View>: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24) .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 { if let badge = badgeFor(tab: tab), badge > 0 {
ZStack { ZStack {
Circle() Circle()
.fill(.red) .fill(.red)
Text(String(badge)) Text(String(badge))
.foregroundColor(.white) .foregroundColor(.white)
.font(.footnote) .font(.caption)
} }
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.offset(x: 10, y: -10) .offset(x: 10, y: -10)
@ -71,11 +72,25 @@ struct SideBarView<Content: View>: View {
.keyboardShortcut("n", modifiers: .command) .keyboardShortcut("n", modifiers: .command)
} }
var body: some View { private func makeAccountButton(account: AppAccount) -> some View {
HStack(spacing: 0) { Button {
ScrollView { if account.id == appAccounts.currentAccount.id {
VStack(alignment: .center) { selectedTab = .profile
profileView } 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 ForEach(tabs) { tab in
Button { Button {
if tab == selectedTab { if tab == selectedTab {
@ -94,6 +109,22 @@ struct SideBarView<Content: View>: View {
} }
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear) .background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
} }
}
var body: some View {
HStack(spacing: 0) {
ScrollView {
VStack(alignment: .center) {
if appAccounts.availableAccounts.isEmpty {
tabsView
} else {
ForEach(appAccounts.availableAccounts) { account in
makeAccountButton(account: account)
if account.id == appAccounts.currentAccount.id {
tabsView
}
}
}
postButton postButton
.padding(.top, 12) .padding(.top, 12)
Spacer() Spacer()

View file

@ -118,7 +118,7 @@ struct AddAccountView: View {
.tint(theme.labelColor) .tint(theme.labelColor)
} else { } else {
Text("Sign in") Text("Sign in")
.font(.headline) .font(.scaledHeadline)
} }
Spacer() Spacer()
} }
@ -139,13 +139,13 @@ struct AddAccountView: View {
} label: { } label: {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(instance.name) Text(instance.name)
.font(.headline) .font(.scaledHeadline)
.foregroundColor(.primary) .foregroundColor(.primary)
Text(instance.info?.shortDescription ?? "") Text(instance.info?.shortDescription ?? "")
.font(.body) .font(.scaledBody)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("\(instance.users) users ⸱ \(instance.statuses) posts") Text("\(instance.users) users ⸱ \(instance.statuses) posts")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
@ -158,13 +158,13 @@ struct AddAccountView: View {
private var placeholderRow: some View { private var placeholderRow: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Loading...") Text("Loading...")
.font(.headline) .font(.scaledHeadline)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Loading, loading, loading ....") Text("Loading, loading, loading ....")
.font(.body) .font(.scaledBody)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("Loading ...") Text("Loading ...")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
.redacted(reason: .placeholder) .redacted(reason: .placeholder)

View file

@ -71,9 +71,9 @@ struct SupportAppView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Loading ...") Text("Loading ...")
.font(.subheadline) .font(.scaledSubheadline)
Text("Loading subtitle...") Text("Loading subtitle...")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
.padding(.vertical, 8) .padding(.vertical, 8)
@ -86,9 +86,9 @@ struct SupportAppView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(tip.title) Text(tip.title)
.font(.subheadline) .font(.scaledSubheadline)
Text(tip.subtitle) Text(tip.subtitle)
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
Spacer() Spacer()

View file

@ -89,13 +89,13 @@ struct AddRemoteTimelineView: View {
} label: { } label: {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(instance.name) Text(instance.name)
.font(.headline) .font(.scaledHeadline)
.foregroundColor(.primary) .foregroundColor(.primary)
Text(instance.info?.shortDescription ?? "") Text(instance.info?.shortDescription ?? "")
.font(.body) .font(.scaledBody)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("\(instance.users) users ⸱ \(instance.statuses) posts") Text("\(instance.users) users ⸱ \(instance.statuses) posts")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }

View file

@ -56,7 +56,7 @@ struct AccountDetailHeaderView: View {
if viewModel.relationship?.followedBy == true { if viewModel.relationship?.followedBy == true {
Text("Follows You") Text("Follows You")
.font(.footnote) .font(.scaledFootnote)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(4) .padding(4)
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
@ -109,9 +109,9 @@ struct AccountDetailHeaderView: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
.font(.headline) .font(.scaledHeadline)
Text("@\(account.acct)") Text("@\(account.acct)")
.font(.callout) .font(.scaledCallout)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
Spacer() Spacer()
@ -124,7 +124,7 @@ struct AccountDetailHeaderView: View {
} }
} }
EmojiTextApp(account.note.asMarkdown, emojis: account.emojis) EmojiTextApp(account.note.asMarkdown, emojis: account.emojis)
.font(.body) .font(.scaledBody)
.padding(.top, 8) .padding(.top, 8)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url) routerPath.handle(url: url)
@ -137,10 +137,10 @@ struct AccountDetailHeaderView: View {
private func makeCustomInfoLabel(title: String, count: Int) -> some View { private func makeCustomInfoLabel(title: String, count: Int) -> some View {
VStack { VStack {
Text("\(count)") Text("\(count)")
.font(.headline) .font(.scaledHeadline)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
Text(title) Text(title)
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }

View file

@ -77,16 +77,21 @@ public struct AccountDetailView: View {
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
} }
.onAppear { .onAppear {
Task {
guard reasons != .placeholder else { return } guard reasons != .placeholder else { return }
isCurrentUser = currentAccount.account?.id == viewModel.accountId isCurrentUser = currentAccount.account?.id == viewModel.accountId
viewModel.isCurrentUser = isCurrentUser viewModel.isCurrentUser = isCurrentUser
viewModel.client = client viewModel.client = client
Task {
await viewModel.fetchAccount() await viewModel.fetchAccount()
}
Task {
if viewModel.statuses.isEmpty { if viewModel.statuses.isEmpty {
await viewModel.fetchStatuses() await viewModel.fetchStatuses()
} }
} }
Task {
await viewModel.fetchFamilliarFollowers()
}
} }
.refreshable { .refreshable {
Task { Task {
@ -150,7 +155,7 @@ public struct AccountDetailView: View {
} label: { } label: {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("About") Text("About")
.font(.callout) .font(.scaledCallout)
Text("\(viewModel.fields.count) fields") Text("\(viewModel.fields.count) fields")
.font(.caption2) .font(.caption2)
} }
@ -167,7 +172,7 @@ public struct AccountDetailView: View {
} label: { } label: {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.callout) .font(.scaledCallout)
Text("\(tag.statusesCount) posts") Text("\(tag.statusesCount) posts")
.font(.caption2) .font(.caption2)
} }
@ -185,7 +190,7 @@ public struct AccountDetailView: View {
if !viewModel.familiarFollowers.isEmpty { if !viewModel.familiarFollowers.isEmpty {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Also followed by") Text("Also followed by")
.font(.headline) .font(.scaledHeadline)
.padding(.leading, .layoutPadding) .padding(.leading, .layoutPadding)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) { LazyHStack(spacing: 0) {
@ -211,7 +216,7 @@ public struct AccountDetailView: View {
ForEach(viewModel.fields) { field in ForEach(viewModel.fields) { field in
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(field.name) Text(field.name)
.font(.headline) .font(.scaledHeadline)
HStack { HStack {
if field.verifiedAt != nil { if field.verifiedAt != nil {
Image(systemName: "checkmark.seal") Image(systemName: "checkmark.seal")
@ -220,7 +225,7 @@ public struct AccountDetailView: View {
EmojiTextApp(field.value.asMarkdown, emojis: viewModel.account?.emojis ?? []) EmojiTextApp(field.value.asMarkdown, emojis: viewModel.account?.emojis ?? [])
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
} }
.font(.body) .font(.scaledBody)
} }
.listRowBackground(field.verifiedAt != nil ? Color.green.opacity(0.15) : theme.primaryBackgroundColor) .listRowBackground(field.verifiedAt != nil ? Color.green.opacity(0.15) : theme.primaryBackgroundColor)
} }
@ -273,7 +278,7 @@ public struct AccountDetailView: View {
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.padding(.horizontal, .layoutPadding) .padding(.horizontal, .layoutPadding)
.font(.headline) .font(.scaledHeadline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
} }
.contextMenu { .contextMenu {
@ -317,7 +322,7 @@ public struct AccountDetailView: View {
ForEach(viewModel.pinned) { status in ForEach(viewModel.pinned) { status in
VStack(alignment: .leading) { VStack(alignment: .leading) {
Label("Pinned post", systemImage: "pin.fill") Label("Pinned post", systemImage: "pin.fill")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.fontWeight(.semibold) .fontWeight(.semibold)
StatusRowView(viewModel: .init(status: status)) StatusRowView(viewModel: .init(status: status))
@ -336,7 +341,7 @@ public struct AccountDetailView: View {
switch viewModel.accountState { switch viewModel.accountState {
case let .data(account): case let .data(account):
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
.font(.headline) .font(.scaledHeadline)
default: default:
EmptyView() EmptyView()
} }

View file

@ -105,7 +105,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
let account: Account let account: Account
let featuredTags: [FeaturedTag] let featuredTags: [FeaturedTag]
let relationships: [Relationship] let relationships: [Relationship]
let familiarFollowers: [FamiliarAccounts]
} }
func fetchAccount() async { func fetchAccount() async {
@ -119,8 +118,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
featuredTags = data.featuredTags featuredTags = data.featuredTags
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt } featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
relationship = data.relationships.first relationship = data.relationships.first
familiarFollowers = data.familiarFollowers.first?.accounts ?? []
} catch { } catch {
if let account { if let account {
accountState = .data(account: 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 account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId)) async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
if client.isAuth && !isCurrentUser { if client.isAuth && !isCurrentUser {
async let relationships: [Relationship] = client.get(endpoint: Accounts.relationships(ids: [accountId])) 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, return try await .init(account: account,
featuredTags: featuredTags, featuredTags: featuredTags,
relationships: relationships, relationships: relationships)
familiarFollowers: familiarFollowers)
} }
return try await .init(account: account, return try await .init(account: account,
featuredTags: featuredTags, featuredTags: featuredTags,
relationships: [], relationships: [])
familiarFollowers: []) }
func fetchFamilliarFollowers() async {
let familiarFollowers: [FamiliarAccounts]? = try? await client?.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
} }
func fetchStatuses() async { func fetchStatuses() async {

View file

@ -34,13 +34,13 @@ public struct AccountsListRow: View {
AvatarView(url: viewModel.account.avatar, size: .status) AvatarView(url: viewModel.account.avatar, size: .status)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
EmojiTextApp(viewModel.account.safeDisplayName.asMarkdown, emojis: viewModel.account.emojis) EmojiTextApp(viewModel.account.safeDisplayName.asMarkdown, emojis: viewModel.account.emojis)
.font(.subheadline) .font(.scaledSubheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Text("@\(viewModel.account.acct)") Text("@\(viewModel.account.acct)")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
EmojiTextApp(viewModel.account.note.asMarkdown, emojis: viewModel.account.emojis) EmojiTextApp(viewModel.account.note.asMarkdown, emojis: viewModel.account.emojis)
.font(.footnote) .font(.scaledFootnote)
.lineLimit(3) .lineLimit(3)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url) routerPath.handle(url: url)

View file

@ -13,6 +13,30 @@ public struct AppAccountView: View {
} }
public var body: some 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 { HStack {
if let account = viewModel.account { if let account = viewModel.account {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
@ -28,7 +52,7 @@ public struct AppAccountView: View {
if let account = viewModel.account { if let account = viewModel.account {
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis) EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
Text("\(account.username)@\(viewModel.appAccount.server)") Text("\(account.username)@\(viewModel.appAccount.server)")
.font(.subheadline) .font(.scaledSubheadline)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
@ -36,11 +60,6 @@ public struct AppAccountView: View {
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.foregroundColor(.gray) .foregroundColor(.gray)
} }
.onAppear {
Task {
await viewModel.fetchAccount()
}
}
.onTapGesture { .onTapGesture {
if appAccounts.currentAccount.id == viewModel.appAccount.id, if appAccounts.currentAccount.id == viewModel.appAccount.id,
let account = viewModel.account let account = viewModel.account

View file

@ -6,6 +6,7 @@ import SwiftUI
public class AppAccountViewModel: ObservableObject { public class AppAccountViewModel: ObservableObject {
let appAccount: AppAccount let appAccount: AppAccount
let client: Client let client: Client
let isCompact: Bool
@Published var account: Account? @Published var account: Account?
@ -13,8 +14,9 @@ public class AppAccountViewModel: ObservableObject {
"@\(account?.acct ?? "...")@\(appAccount.server)" "@\(account?.acct ?? "...")@\(appAccount.server)"
} }
public init(appAccount: AppAccount) { public init(appAccount: AppAccount, isCompact: Bool = false) {
self.appAccount = appAccount self.appAccount = appAccount
self.isCompact = isCompact
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken) client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
} }

View file

@ -20,7 +20,7 @@ struct ConversationsListRow: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", ")) Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", "))
.font(.headline) .font(.scaledHeadline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
Spacer() Spacer()
@ -30,7 +30,7 @@ struct ConversationsListRow: View {
.frame(width: 10, height: 10) .frame(width: 10, height: 10)
} }
Text(conversation.lastStatus.createdAt.formatted) Text(conversation.lastStatus.createdAt.formatted)
.font(.footnote) .font(.scaledFootnote)
} }
Text(conversation.lastStatus.content.asRawText) Text(conversation.lastStatus.content.asRawText)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)

View file

@ -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
}
}
}

View file

@ -14,6 +14,9 @@ public struct AvatarView: View {
case .account: case .account:
return .init(width: 80, height: 80) return .init(width: 80, height: 80)
case .status: case .status:
if ProcessInfo.processInfo.isiOSAppOnMac {
return .init(width: 48, height: 48)
}
return .init(width: 40, height: 40) return .init(width: 40, height: 40)
case .embed: case .embed:
return .init(width: 34, height: 34) return .init(width: 34, height: 34)

View file

@ -18,10 +18,10 @@ public struct EmptyView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(maxHeight: 50) .frame(maxHeight: 50)
Text(title) Text(title)
.font(.title) .font(.scaledTitle)
.padding(.top, 16) .padding(.top, 16)
Text(message) Text(message)
.font(.subheadline) .font(.scaledSubheadline)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(.gray) .foregroundColor(.gray)
} }

View file

@ -20,10 +20,10 @@ public struct ErrorView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(maxHeight: 50) .frame(maxHeight: 50)
Text(title) Text(title)
.font(.title) .font(.scaledTitle)
.padding(.top, 16) .padding(.top, 16)
Text(message) Text(message)
.font(.subheadline) .font(.scaledSubheadline)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(.gray) .foregroundColor(.gray)
Button { Button {

View file

@ -15,9 +15,9 @@ public struct TagRowView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.headline) .font(.scaledHeadline)
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants") Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
Spacer() Spacer()

View file

@ -35,7 +35,7 @@ public struct ListEditView: View {
emojis: account.emojis) emojis: account.emojis)
Text("@\(account.acct)") Text("@\(account.acct)")
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.footnote) .font(.scaledFootnote)
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)

View file

@ -75,6 +75,16 @@ public struct OpenAIClient {
public let object: String public let object: String
public let model: String public let model: String
public let choices: [Choice] 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() {} public init() {}

View file

@ -56,18 +56,18 @@ struct NotificationRowView: View {
append: { append: {
Text(" ") + Text(" ") +
Text(type.label()) Text(type.label())
.font(.subheadline) .font(.scaledSubheadline)
.fontWeight(.regular) + .fontWeight(.regular) +
Text("") Text("")
.font(.footnote) .font(.scaledFootnote)
.fontWeight(.regular) .fontWeight(.regular)
.foregroundColor(.gray) + .foregroundColor(.gray) +
Text(notification.createdAt.formatted) Text(notification.createdAt.formatted)
.font(.footnote) .font(.scaledFootnote)
.fontWeight(.regular) .fontWeight(.regular)
.foregroundColor(.gray) .foregroundColor(.gray)
}) })
.font(.subheadline) .font(.scaledSubheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Spacer() Spacer()
} }
@ -93,14 +93,14 @@ struct NotificationRowView: View {
} else { } else {
Group { Group {
Text("@\(notification.account.acct)") Text("@\(notification.account.acct)")
.font(.callout) .font(.scaledCallout)
.foregroundColor(.gray) .foregroundColor(.gray)
if type == .follow { if type == .follow {
EmojiTextApp(notification.account.note.asMarkdown, EmojiTextApp(notification.account.note.asMarkdown,
emojis: notification.account.emojis) emojis: notification.account.emojis)
.lineLimit(3) .lineLimit(3)
.font(.callout) .font(.scaledCallout)
.foregroundColor(.gray) .foregroundColor(.gray)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url) routerPath.handle(url: url)

View file

@ -23,7 +23,8 @@ public struct NotificationsListView: View {
.padding(.top, 16) .padding(.top, 16)
.frame(maxWidth: .maxColumnWidth) .frame(maxWidth: .maxColumnWidth)
} }
.padding(.horizontal, .layoutPadding) .padding(.leading, .layoutPadding + 12)
.padding(.trailing, .layoutPadding)
.padding(.top, .layoutPadding) .padding(.top, .layoutPadding)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
} }

View file

@ -158,7 +158,7 @@ struct StatusEditorAccessoryView: View {
private var characterCountView: some View { private var characterCountView: some View {
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)") Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.callout) .font(.scaledCallout)
} }
private var availableLanguages: [(String, String?, String?)] { private var availableLanguages: [(String, String?, String?)] {

View file

@ -34,10 +34,10 @@ struct StatusEditorAutoCompleteView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
EmojiTextApp(account.safeDisplayName.asMarkdown, EmojiTextApp(account.safeDisplayName.asMarkdown,
emojis: account.emojis) emojis: account.emojis)
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
Text("@\(account.acct)") Text("@\(account.acct)")
.font(.caption) .font(.scaledCaption)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
} }
} }
@ -51,7 +51,7 @@ struct StatusEditorAutoCompleteView: View {
viewModel.selectHashtagSuggestion(tag: tag) viewModel.selectHashtagSuggestion(tag: tag)
} label: { } label: {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.caption) .font(.scaledCaption)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
} }
} }

View file

@ -164,7 +164,7 @@ public struct StatusEditorView: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
privacyMenu privacyMenu
Text("@\(account.acct)") Text("@\(account.acct)")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
Spacer() Spacer()
@ -188,7 +188,7 @@ public struct StatusEditorView: View {
Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName) Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName)
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
} }
.font(.footnote) .font(.scaledFootnote)
.padding(4) .padding(4)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)

View file

@ -358,12 +358,8 @@ public class StatusEditorViewModel: ObservableObject {
do { do {
let client = OpenAIClient() let client = OpenAIClient()
let response = try await client.request(prompt) let response = try await client.request(prompt)
if var text = response.choices.first?.text {
text.removeFirst()
text.removeFirst()
backupStatusText = statusText backupStatusText = statusText
replaceTextWith(text: text) replaceTextWith(text: response.trimmedText)
}
} catch {} } catch {}
} }

View file

@ -36,14 +36,14 @@ public struct StatusEmbeddedView: View {
AvatarView(url: account.avatar, size: .embed) AvatarView(url: account.avatar, size: .embed)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: account.emojis) EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: account.emojis)
.font(.footnote) .font(.scaledFootnote)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {
Text("@\(account.acct)") + Text("@\(account.acct)") +
Text("") + Text("") +
Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted) Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted)
} }
.font(.caption) .font(.scaledCaption)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }

View file

@ -21,18 +21,38 @@ class VideoPlayerViewModel: ObservableObject {
} }
} }
func pause() {
player?.pause()
}
func play() {
player?.play()
}
deinit { deinit {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: self.player) NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: self.player)
} }
} }
struct VideoPlayerView: View { struct VideoPlayerView: View {
@Environment(\.scenePhase) private var scenePhase
@StateObject var viewModel: VideoPlayerViewModel @StateObject var viewModel: VideoPlayerViewModel
var body: some View { var body: some View {
VStack { VStack {
VideoPlayer(player: viewModel.player) VideoPlayer(player: viewModel.player)
}.onAppear { }.onAppear {
viewModel.preparePlayer() viewModel.preparePlayer()
} }
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
case .background, .inactive:
viewModel.pause()
case .active:
viewModel.play()
default:
break
}
})
} }
} }

View file

@ -47,7 +47,7 @@ public struct StatusPollView: View {
if !viewModel.votes.isEmpty || viewModel.poll.expired { if !viewModel.votes.isEmpty || viewModel.poll.expired {
Spacer() Spacer()
Text("\(percentForOption(option: option)) %") Text("\(percentForOption(option: option)) %")
.font(.subheadline) .font(.scaledSubheadline)
.frame(width: 40) .frame(width: 40)
} }
} }
@ -74,7 +74,7 @@ public struct StatusPollView: View {
Text(viewModel.poll.expiresAt.asDate, style: .timer) Text(viewModel.poll.expiresAt.asDate, style: .timer)
} }
} }
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
@ -120,7 +120,7 @@ public struct StatusPollView: View {
} }
Text(option.title) Text(option.title)
.foregroundColor(.white) .foregroundColor(.white)
.font(.body) .font(.scaledBody)
} }
.padding(.leading, 12) .padding(.leading, 12)
} }

View file

@ -80,7 +80,7 @@ struct StatusActionsView: View {
.foregroundColor(action.tintColor(viewModel: viewModel, theme: theme)) .foregroundColor(action.tintColor(viewModel: viewModel, theme: theme))
if let count = action.count(viewModel: viewModel, theme: theme) { if let count = action.count(viewModel: viewModel, theme: theme) {
Text("\(count)") Text("\(count)")
.font(.footnote) .font(.scaledFootnote)
} }
} }
} }
@ -114,14 +114,14 @@ struct StatusActionsView: View {
} }
} }
} }
.font(.caption) .font(.scaledCaption)
.foregroundColor(.gray) .foregroundColor(.gray)
if viewModel.favouritesCount > 0 { if viewModel.favouritesCount > 0 {
Divider() Divider()
NavigationLink(value: RouterDestinations.favouritedBy(id: viewModel.status.id)) { NavigationLink(value: RouterDestinations.favouritedBy(id: viewModel.status.id)) {
Text("\(viewModel.favouritesCount) favorites") Text("\(viewModel.favouritesCount) favorites")
.font(.callout) .font(.scaledCallout)
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
@ -130,7 +130,7 @@ struct StatusActionsView: View {
Divider() Divider()
NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) { NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) {
Text("\(viewModel.reblogsCount) boosts") Text("\(viewModel.reblogsCount) boosts")
.font(.callout) .font(.scaledCallout)
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }

View file

@ -33,16 +33,16 @@ public struct StatusCardView: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(title) Text(title)
.font(.headline) .font(.scaledHeadline)
.lineLimit(3) .lineLimit(3)
if let description = card.description, !description.isEmpty { if let description = card.description, !description.isEmpty {
Text(description) Text(description)
.font(.body) .font(.scaledBody)
.foregroundColor(.gray) .foregroundColor(.gray)
.lineLimit(3) .lineLimit(3)
} }
Text(card.url.host() ?? card.url.absoluteString) Text(card.url.host() ?? card.url.absoluteString)
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
.lineLimit(1) .lineLimit(1)
} }

View file

@ -174,14 +174,14 @@ public struct StatusMediaPreviewView: View {
content: { image in content: { image in
image image
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fit)
.frame(maxHeight: imageMaxHeight) .frame(maxHeight: isNotifications ? imageMaxHeight : nil)
.cornerRadius(4) .cornerRadius(4)
}, },
placeholder: { placeholder: {
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(Color.gray) .fill(Color.gray)
.frame(maxHeight: imageMaxHeight) .frame(maxHeight: isNotifications ? imageMaxHeight : nil)
.shimmering() .shimmering()
} }
) )
@ -213,11 +213,11 @@ public struct StatusMediaPreviewView: View {
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(Color.gray) .fill(Color.gray)
.frame(maxHeight: imageMaxHeight) .frame(maxHeight: imageMaxHeight)
.frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) .frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
.shimmering() .shimmering()
} }
} }
.frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width) .frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
.frame(height: imageMaxHeight) .frame(height: imageMaxHeight)
if sensitive { if sensitive {
cornerSensitiveButton cornerSensitiveButton
@ -228,7 +228,7 @@ public struct StatusMediaPreviewView: View {
isAltAlertDisplayed = true isAltAlertDisplayed = true
} label: { } label: {
Text("ALT") Text("ALT")
.font(.footnote) .font(.scaledFootnote)
} }
.padding(4) .padding(4)
.background(.thinMaterial) .background(.thinMaterial)
@ -243,7 +243,7 @@ public struct StatusMediaPreviewView: View {
} }
} }
} }
.frame(width: isNotifications ? imageMaxHeight : nil) .frame(maxWidth: isNotifications ? imageMaxHeight : nil)
.frame(height: imageMaxHeight) .frame(height: imageMaxHeight)
} }
.onTapGesture { .onTapGesture {

View file

@ -106,7 +106,7 @@ public struct StatusRowView: View {
Text("You boosted") Text("You boosted")
} }
} }
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.fontWeight(.semibold) .fontWeight(.semibold)
.onTapGesture { .onTapGesture {
@ -131,7 +131,7 @@ public struct StatusRowView: View {
Text("Replied to") Text("Replied to")
Text(mention.username) Text(mention.username)
} }
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.fontWeight(.semibold) .fontWeight(.semibold)
.onTapGesture { .onTapGesture {
@ -179,7 +179,7 @@ public struct StatusRowView: View {
Group { Group {
if !status.spoilerText.isEmpty { if !status.spoilerText.isEmpty {
EmojiTextApp(status.spoilerText.asMarkdown, emojis: status.emojis) EmojiTextApp(status.spoilerText.asMarkdown, emojis: status.emojis)
.font(.body) .font(.scaledBody)
Button { Button {
withAnimation { withAnimation {
viewModel.displaySpoiler.toggle() viewModel.displaySpoiler.toggle()
@ -192,7 +192,7 @@ public struct StatusRowView: View {
if !viewModel.displaySpoiler { if !viewModel.displaySpoiler {
HStack { HStack {
EmojiTextApp(status.content.asMarkdown, emojis: status.emojis) EmojiTextApp(status.content.asMarkdown, emojis: status.emojis)
.font(.body) .font(.scaledBody)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routerPath.handleStatus(status: status, url: url) routerPath.handleStatus(status: status, url: url)
}) })
@ -249,7 +249,7 @@ public struct StatusRowView: View {
} }
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis) EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis)
.font(.headline) .font(.scaledHeadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {
Text("@\(status.account.acct)") + Text("@\(status.account.acct)") +
@ -258,7 +258,7 @@ public struct StatusRowView: View {
Text("") + Text("") +
Text(Image(systemName: viewModel.status.visibility.iconName)) Text(Image(systemName: viewModel.status.visibility.iconName))
} }
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }

View file

@ -60,6 +60,7 @@ public class StatusRowViewModel: ObservableObject {
} }
func navigateToDetail(routerPath: RouterPath) { func navigateToDetail(routerPath: RouterPath) {
guard !isFocused else { return }
if isRemote, let url = status.reblog?.url ?? status.url { if isRemote, let url = status.reblog?.url ?? status.url {
routerPath.navigate(to: .remoteStatusDetail(url: url)) routerPath.navigate(to: .remoteStatusDetail(url: url))
} else { } else {

View file

@ -141,9 +141,9 @@ public struct TimelineView: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.headline) .font(.scaledHeadline)
Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants") Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants")
.font(.footnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
Spacer() Spacer()