SwiftFormat

This commit is contained in:
Thomas Ricouard 2023-03-13 13:38:28 +01:00
parent f1267620be
commit 6c307aba63
58 changed files with 313 additions and 299 deletions

View file

@ -3,13 +3,13 @@ import AppAccount
import Conversations import Conversations
import DesignSystem import DesignSystem
import Env import Env
import Explore
import LinkPresentation import LinkPresentation
import Lists import Lists
import Models import Models
import Status import Status
import SwiftUI import SwiftUI
import Timeline import Timeline
import Explore
@MainActor @MainActor
extension View { extension View {

View file

@ -112,7 +112,8 @@ struct IceCubesApp: App {
SideBarView(selectedTab: $selectedTab, SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab, popToRootTab: $popToRootTab,
tabs: availableTabs, tabs: availableTabs,
routerPath: sidebarRouterPath) { routerPath: sidebarRouterPath)
{
GeometryReader { _ in GeometryReader { _ in
HStack(spacing: 0) { HStack(spacing: 0) {
ZStack { ZStack {
@ -165,12 +166,12 @@ struct IceCubesApp: App {
popToRootTab = selectedTab popToRootTab = selectedTab
} }
} }
HapticManager.shared.fireHaptic(of: .tabSelection) HapticManager.shared.fireHaptic(of: .tabSelection)
SoundEffectManager.shared.playSound(of: .tabSelection) SoundEffectManager.shared.playSound(of: .tabSelection)
selectedTab = newTab selectedTab = newTab
DispatchQueue.main.async { DispatchQueue.main.async {
if selectedTab == .notifications, if selectedTab == .notifications,
let token = appAccountsManager.currentAccount.oauthToken let token = appAccountsManager.currentAccount.oauthToken
@ -179,7 +180,7 @@ struct IceCubesApp: App {
watcher.unreadNotificationsCount = 0 watcher.unreadNotificationsCount = 0
} }
} }
})) { })) {
ForEach(availableTabs) { tab in ForEach(availableTabs) { tab in
tab.makeContentView(popToRootTab: $popToRootTab) tab.makeContentView(popToRootTab: $popToRootTab)

View file

@ -39,11 +39,11 @@ struct AboutView: View {
.cornerRadius(4) .cornerRadius(4)
Spacer() Spacer()
} }
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) { Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
Label("settings.support.privacy-policy", systemImage: "lock") Label("settings.support.privacy-policy", systemImage: "lock")
} }
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) { Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
Label("settings.support.terms-of-use", systemImage: "checkmark.shield") Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
} }
@ -51,7 +51,7 @@ struct AboutView: View {
Text("\(versionNumber)©2023 Thomas Ricouard") Text("\(versionNumber)©2023 Thomas Ricouard")
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
Section { Section {
Text(""" Text("""
[EmojiText](https://github.com/divadretlaw/EmojiText) [EmojiText](https://github.com/divadretlaw/EmojiText)

View file

@ -74,7 +74,7 @@ struct AccountSettingsView: View {
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
Section { Section {
Button(role: .destructive) { Button(role: .destructive) {
if let token = appAccount.oauthToken { if let token = appAccount.oauthToken {

View file

@ -28,19 +28,17 @@ struct AddAccountView: View {
@State private var oauthURL: URL? @State private var oauthURL: URL?
private let instanceNamePublisher = PassthroughSubject<String, Never>() private let instanceNamePublisher = PassthroughSubject<String, Never>()
private var sanitizedName: String { private var sanitizedName: String {
get { var name = instanceName
var name = instanceName .replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "")
.replacingOccurrences(of: "https://", with: "")
if name.contains("@") {
if name.contains("@") { let parts = name.components(separatedBy: "@")
let parts = name.components(separatedBy: "@") name = parts[parts.count - 1] // [@]username@server.address.com
name = parts[parts.count-1] // [@]username@server.address.com
}
return name
} }
return name
} }
@FocusState private var isInstanceURLFieldFocused: Bool @FocusState private var isInstanceURLFieldFocused: Bool
@ -94,8 +92,8 @@ struct AddAccountView: View {
.onChange(of: instanceName) { newValue in .onChange(of: instanceName) { newValue in
instanceNamePublisher.send(newValue) instanceNamePublisher.send(newValue)
} }
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in .onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in
//let newValue = newValue // let newValue = newValue
// .replacingOccurrences(of: "http://", with: "") // .replacingOccurrences(of: "http://", with: "")
// .replacingOccurrences(of: "https://", with: "") // .replacingOccurrences(of: "https://", with: "")
let client = Client(server: sanitizedName) let client = Client(server: sanitizedName)
@ -106,7 +104,7 @@ struct AddAccountView: View {
let instance: Instance = try await client.get(endpoint: Instances.instance) let instance: Instance = try await client.get(endpoint: Instances.instance)
withAnimation { withAnimation {
self.instance = instance self.instance = instance
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
} }
instanceFetchError = nil instanceFetchError = nil
} else { } else {

View file

@ -48,14 +48,14 @@ struct ContentSettingsView: View {
Text("settings.content.expand-spoilers") Text("settings.content.expand-spoilers")
} }
.disabled(userPreferences.useInstanceContentSettings) .disabled(userPreferences.useInstanceContentSettings)
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) { Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
Text(media.description).tag(media) Text(media.description).tag(media)
} }
} }
.disabled(userPreferences.useInstanceContentSettings) .disabled(userPreferences.useInstanceContentSettings)
Toggle(isOn: $userPreferences.collapseLongPosts) { Toggle(isOn: $userPreferences.collapseLongPosts) {
Text("settings.content.collapse-long-posts") Text("settings.content.collapse-long-posts")
} }

View file

@ -1,35 +1,35 @@
import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network import Network
import Status import Status
import SwiftUI import SwiftUI
import Combine
class DisplaySettingsLocalColors: ObservableObject { class DisplaySettingsLocalColors: ObservableObject {
@Published var tintColor = Theme.shared.tintColor @Published var tintColor = Theme.shared.tintColor
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor @Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor @Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
@Published var labelColor = Theme.shared.labelColor @Published var labelColor = Theme.shared.labelColor
private var subscriptions = Set<AnyCancellable>() private var subscriptions = Set<AnyCancellable>()
init() { init() {
$tintColor $tintColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.tintColor = newColor } ) .sink(receiveValue: { newColor in Theme.shared.tintColor = newColor })
.store(in: &subscriptions) .store(in: &subscriptions)
$primaryBackgroundColor $primaryBackgroundColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor } ) .sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor })
.store(in: &subscriptions) .store(in: &subscriptions)
$secondaryBackgroundColor $secondaryBackgroundColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor } ) .sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor })
.store(in: &subscriptions) .store(in: &subscriptions)
$labelColor $labelColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.labelColor = newColor } ) .sink(receiveValue: { newColor in Theme.shared.labelColor = newColor })
.store(in: &subscriptions) .store(in: &subscriptions)
} }
} }
@ -40,15 +40,15 @@ struct DisplaySettingsView: View {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var userPreferences: UserPreferences @EnvironmentObject private var userPreferences: UserPreferences
@StateObject private var localColors = DisplaySettingsLocalColors() @StateObject private var localColors = DisplaySettingsLocalColors()
@State private var isFontSelectorPresented = false @State private var isFontSelectorPresented = false
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"), private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
client: Client(server: ""), client: Client(server: ""),
routerPath: RouterPath()) // translate from latin button routerPath: RouterPath()) // translate from latin button
var body: some View { var body: some View {
Form { Form {
exampleSection exampleSection
@ -62,14 +62,14 @@ struct DisplaySettingsView: View {
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor) .background(theme.secondaryBackgroundColor)
} }
private var exampleSection: some View { private var exampleSection: some View {
Section("settings.display.example-toot") { Section("settings.display.example-toot") {
StatusRowView(viewModel: { previewStatusViewModel }) StatusRowView(viewModel: { previewStatusViewModel })
.allowsHitTesting(false) .allowsHitTesting(false)
} }
} }
private var themeSection: some View { private var themeSection: some View {
Section { Section {
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme) Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
@ -97,7 +97,7 @@ struct DisplaySettingsView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var fontSection: some View { private var fontSection: some View {
Section("settings.display.section.font") { Section("settings.display.section.font") {
Picker("settings.display.font", selection: .init(get: { () -> FontState in Picker("settings.display.font", selection: .init(get: { () -> FontState in
@ -140,7 +140,7 @@ struct DisplaySettingsView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var layoutSection: some View { private var layoutSection: some View {
Section("settings.display.section.display") { Section("settings.display.section.display") {
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) { Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
@ -169,7 +169,7 @@ struct DisplaySettingsView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
@ViewBuilder @ViewBuilder
private var platformsSection: some View { private var platformsSection: some View {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
@ -186,7 +186,7 @@ struct DisplaySettingsView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
} }
private var resetSection: some View { private var resetSection: some View {
Section { Section {
Button { Button {
@ -195,12 +195,12 @@ struct DisplaySettingsView: View {
theme.avatarShape = .rounded theme.avatarShape = .rounded
theme.avatarPosition = .top theme.avatarPosition = .top
theme.statusActionsDisplay = .full theme.statusActionsDisplay = .full
localColors.tintColor = theme.tintColor localColors.tintColor = theme.tintColor
localColors.primaryBackgroundColor = theme.primaryBackgroundColor localColors.primaryBackgroundColor = theme.primaryBackgroundColor
localColors.secondaryBackgroundColor = theme.secondaryBackgroundColor localColors.secondaryBackgroundColor = theme.secondaryBackgroundColor
localColors.labelColor = theme.labelColor localColors.labelColor = theme.labelColor
} label: { } label: {
Text("settings.display.restore") Text("settings.display.restore")
} }

View file

@ -48,7 +48,7 @@ struct SupportAppView: View {
} }
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
@State private var loadingProducts: Bool = false @State private var loadingProducts: Bool = false
@ -86,7 +86,7 @@ struct SupportAppView: View {
refreshUserInfo() refreshUserInfo()
} }
} }
private func purchase(product: StoreProduct) async { private func purchase(product: StoreProduct) async {
if !isProcessingPurchase { if !isProcessingPurchase {
isProcessingPurchase = true isProcessingPurchase = true
@ -101,23 +101,23 @@ struct SupportAppView: View {
isProcessingPurchase = false isProcessingPurchase = false
} }
} }
private func fetchStoreProducts() { private func fetchStoreProducts() {
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId }) self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
self.products = products.filter{ $0.productIdentifier != Tip.supporter.productId}.sorted(by: { $0.price < $1.price }) self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price })
withAnimation { withAnimation {
loadingProducts = false loadingProducts = false
} }
} }
} }
private func refreshUserInfo() { private func refreshUserInfo() {
Purchases.shared.getCustomerInfo { info, _ in Purchases.shared.getCustomerInfo { info, _ in
self.customerInfo = info self.customerInfo = info
} }
} }
private func makePurchaseButton(product: StoreProduct) -> some View { private func makePurchaseButton(product: StoreProduct) -> some View {
Button { Button {
Task { Task {
@ -133,7 +133,7 @@ struct SupportAppView: View {
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
private var aboutSection: some View { private var aboutSection: some View {
Section { Section {
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
@ -152,7 +152,7 @@ struct SupportAppView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var subscriptionSection: some View { private var subscriptionSection: some View {
Section { Section {
if loadingProducts { if loadingProducts {
@ -163,14 +163,14 @@ struct SupportAppView: View {
Text(Image(systemName: "checkmark.seal.fill")) Text(Image(systemName: "checkmark.seal.fill"))
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
.baselineOffset(-1) + .baselineOffset(-1) +
Text("settings.support.supporter.subscribed") Text("settings.support.supporter.subscribed")
.font(.scaledSubheadline) .font(.scaledSubheadline)
} else { } else {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(Image(systemName: "checkmark.seal.fill")) Text(Image(systemName: "checkmark.seal.fill"))
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
.baselineOffset(-1) + .baselineOffset(-1) +
Text(Tip.supporter.title) Text(Tip.supporter.title)
.font(.scaledSubheadline) .font(.scaledSubheadline)
Text(Tip.supporter.subtitle) Text(Tip.supporter.subtitle)
.font(.scaledFootnote) .font(.scaledFootnote)
@ -179,7 +179,6 @@ struct SupportAppView: View {
Spacer() Spacer()
makePurchaseButton(product: subscription) makePurchaseButton(product: subscription)
} }
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }
@ -190,7 +189,7 @@ struct SupportAppView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var tipsSection: some View { private var tipsSection: some View {
Section { Section {
if loadingProducts { if loadingProducts {
@ -215,7 +214,7 @@ struct SupportAppView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private var restorePurchase: some View { private var restorePurchase: some View {
Section { Section {
HStack { HStack {
@ -234,7 +233,7 @@ struct SupportAppView: View {
} }
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
} }
private var linksSection: some View { private var linksSection: some View {
Section { Section {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
@ -254,7 +253,7 @@ struct SupportAppView: View {
} }
.listRowBackground(theme.secondaryBackgroundColor) .listRowBackground(theme.secondaryBackgroundColor)
} }
private var loadingPlaceholder: some View { private var loadingPlaceholder: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {

View file

@ -1,9 +1,9 @@
import Account import Account
import DesignSystem
import Explore import Explore
import Foundation import Foundation
import Status import Status
import SwiftUI import SwiftUI
import DesignSystem
enum Tab: Int, Identifiable, Hashable { enum Tab: Int, Identifiable, Hashable {
case timeline, notifications, mentions, explore, messages, settings, other case timeline, notifications, mentions, explore, messages, settings, other

View file

@ -54,7 +54,8 @@ class ShareViewController: UIViewController {
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose, NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
object: nil, object: nil,
queue: nil) { _ in queue: nil)
{ _ in
self.close() self.close()
} }
} }

View file

@ -1,15 +1,15 @@
import SwiftUI
import Env import Env
import Network import Network
import SwiftUI
public struct AccountDetailContextMenu: View { public struct AccountDetailContextMenu: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@ObservedObject var viewModel: AccountDetailViewModel @ObservedObject var viewModel: AccountDetailViewModel
public var body: some View { public var body: some View {
if let account = viewModel.account { if let account = viewModel.account {
Section(account.acct) { Section(account.acct) {

View file

@ -51,7 +51,8 @@ public struct AccountDetailView: View {
Picker("", selection: $viewModel.selectedTab) { Picker("", selection: $viewModel.selectedTab) {
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs, ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
id: \.self) { tab in id: \.self)
{ tab in
Image(systemName: tab.iconName) Image(systemName: tab.iconName)
.tag(tab) .tag(tab)
} }
@ -300,7 +301,7 @@ public struct AccountDetailView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Menu { Menu {
AccountDetailContextMenu(viewModel: viewModel) AccountDetailContextMenu(viewModel: viewModel)
if !viewModel.isCurrentUser { if !viewModel.isCurrentUser {
Button { Button {
isEditingRelationshipNote = true isEditingRelationshipNote = true
@ -308,7 +309,7 @@ public struct AccountDetailView: View {
Label("account.relation.note.edit", systemImage: "pencil") Label("account.relation.note.edit", systemImage: "pencil")
} }
} }
if isCurrentUser { if isCurrentUser {
Button { Button {
isEditingAccount = true isEditingAccount = true
@ -329,10 +330,10 @@ public struct AccountDetailView: View {
} label: { } label: {
Label("settings.push.navigation-title", systemImage: "bell") Label("settings.push.navigation-title", systemImage: "bell")
} }
if let account = viewModel.account { if let account = viewModel.account {
Divider() Divider()
Button { Button {
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") { if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
openURL(url) openURL(url)
@ -343,7 +344,7 @@ public struct AccountDetailView: View {
Divider() Divider()
} }
Button { Button {
routerPath.presentedSheet = .settings routerPath.presentedSheet = .settings
} label: { } label: {

View file

@ -137,9 +137,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
featuredTags: featuredTags, featuredTags: featuredTags,
relationships: relationships) relationships: relationships)
} catch { } catch {
return try await .init(account: account, return try await .init(account: account,
featuredTags: [], featuredTags: [],
relationships: relationships) relationships: relationships)
} }
} }
return try await .init(account: account, return try await .init(account: account,

View file

@ -26,9 +26,9 @@ public struct AccountsListRow: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@StateObject var viewModel: AccountsListRowViewModel @StateObject var viewModel: AccountsListRowViewModel
@State private var isEditingRelationshipNote: Bool = false @State private var isEditingRelationshipNote: Bool = false
let isFollowRequest: Bool let isFollowRequest: Bool
let requestUpdated: (() -> Void)? let requestUpdated: (() -> Void)?
@ -89,7 +89,7 @@ public struct AccountsListRow: View {
AccountDetailHeaderView(viewModel: .init(account: viewModel.account), AccountDetailHeaderView(viewModel: .init(account: viewModel.account),
account: viewModel.account, account: viewModel.account,
scrollViewProxy: nil) scrollViewProxy: nil)
.applyAccountDetailsRowStyle(theme: theme) .applyAccountDetailsRowStyle(theme: theme)
} }
.listStyle(.plain) .listStyle(.plain)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
@ -98,6 +98,5 @@ public struct AccountsListRow: View {
.environmentObject(currentAccount) .environmentObject(currentAccount)
.environmentObject(client) .environmentObject(client)
} }
} }
} }

View file

@ -23,9 +23,9 @@ struct EditFilterView: View {
@FocusState private var isTitleFocused: Bool @FocusState private var isTitleFocused: Bool
private var data: ServerFilterData { private var data: ServerFilterData {
var expiresIn: String? = nil; var expiresIn: String?
// we add 50 seconds, otherwise we immediately show 6d for a 7d filter (6d, 23h, 59s) // we add 50 seconds, otherwise we immediately show 6d for a 7d filter (6d, 23h, 59s)
switch(expirySelection){ switch expirySelection {
case .infinite: case .infinite:
expiresIn = "" // need to send an empty value in order for the server to clear this field in the filter expiresIn = "" // need to send an empty value in order for the server to clear this field in the filter
case .custom: case .custom:
@ -33,11 +33,11 @@ struct EditFilterView: View {
default: default:
expiresIn = String(expirySelection.rawValue + 50) expiresIn = String(expirySelection.rawValue + 50)
} }
return ServerFilterData(title: title, return ServerFilterData(title: title,
context: contexts, context: contexts,
filterAction: filterAction, filterAction: filterAction,
expiresIn: expiresIn) expiresIn: expiresIn)
} }
private var canSave: Bool { private var canSave: Bool {
@ -53,7 +53,7 @@ struct EditFilterView: View {
_expiresAt = .init(initialValue: filter?.expiresAt?.asDate) _expiresAt = .init(initialValue: filter?.expiresAt?.asDate)
_expirySelection = .init(initialValue: filter?.expiresAt == nil ? .infinite : .custom) _expirySelection = .init(initialValue: filter?.expiresAt == nil ? .infinite : .custom)
} }
var body: some View { var body: some View {
Form { Form {
titleSection titleSection
@ -95,15 +95,14 @@ struct EditFilterView: View {
} }
if expirySelection != .infinite { if expirySelection != .infinite {
DatePicker("filter.edit.expiry.date-time", DatePicker("filter.edit.expiry.date-time",
selection: Binding<Date>(get: {self.expiresAt ?? Date()}, set: {self.expiresAt = $0}), selection: Binding<Date>(get: { self.expiresAt ?? Date() }, set: { self.expiresAt = $0 }),
displayedComponents: [.date, .hourAndMinute] displayedComponents: [.date, .hourAndMinute])
) .disabled(expirySelection != .custom)
.disabled(expirySelection != .custom)
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
@ViewBuilder @ViewBuilder
private var titleSection: some View { private var titleSection: some View {
Section("filter.edit.title") { Section("filter.edit.title") {
@ -116,7 +115,7 @@ struct EditFilterView: View {
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
if filter == nil, !title.isEmpty { if filter == nil, !title.isEmpty {
Section { Section {
Button { Button {

View file

@ -7,7 +7,7 @@ public struct AppAccountView: View {
@EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var appAccounts: AppAccountsManager @EnvironmentObject private var appAccounts: AppAccountsManager
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@StateObject var viewModel: AppAccountViewModel @StateObject var viewModel: AppAccountViewModel
public init(viewModel: AppAccountViewModel) { public init(viewModel: AppAccountViewModel) {
@ -51,7 +51,8 @@ public struct AppAccountView: View {
.offset(x: 5, y: -5) .offset(x: 5, y: -5)
} else if viewModel.showBadge, } else if viewModel.showBadge,
let token = viewModel.appAccount.oauthToken, let token = viewModel.appAccount.oauthToken,
preferences.getNotificationsCount(for: token) > 0 { preferences.getNotificationsCount(for: token) > 0
{
let notificationsCount = preferences.getNotificationsCount(for: token) let notificationsCount = preferences.getNotificationsCount(for: token)
ZStack { ZStack {
Circle() Circle()

View file

@ -23,7 +23,7 @@ public struct AppAccountsSelectorView: View {
.map { preferences.getNotificationsCount(for: $0) } .map { preferences.getNotificationsCount(for: $0) }
.reduce(0, +) > 0 .reduce(0, +) > 0
} }
private var preferredHeight: CGFloat { private var preferredHeight: CGFloat {
var baseHeight: CGFloat = 220 var baseHeight: CGFloat = 220
baseHeight += CGFloat(60 * accountsViewModel.count) baseHeight += CGFloat(60 * accountsViewModel.count)
@ -48,9 +48,9 @@ public struct AppAccountsSelectorView: View {
} }
.sheet(isPresented: $isPresented, content: { .sheet(isPresented: $isPresented, content: {
accountsView.presentationDetents([.height(preferredHeight), .large]) accountsView.presentationDetents([.height(preferredHeight), .large])
.onAppear { .onAppear {
refreshAccounts() refreshAccounts()
} }
}) })
.onChange(of: currentAccount.account?.id) { _ in .onChange(of: currentAccount.account?.id) { _ in
refreshAccounts() refreshAccounts()
@ -88,7 +88,7 @@ public struct AppAccountsSelectorView: View {
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
if accountCreationEnabled { if accountCreationEnabled {
Section { Section {
Button { Button {
@ -121,7 +121,7 @@ public struct AppAccountsSelectorView: View {
} }
} }
} }
private var settingsButton: some View { private var settingsButton: some View {
Button { Button {
isPresented = false isPresented = false

View file

@ -177,7 +177,8 @@ struct ConversationMessageView: View {
let width = mediaWidth(proxy: proxy) let width = mediaWidth(proxy: proxy)
if let url = attachement.url { if let url = attachement.url {
LazyImage(request: makeImageRequest(for: url, LazyImage(request: makeImageRequest(for: url,
size: .init(width: width, height: 200))) { state in size: .init(width: width, height: 200)))
{ state in
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()

View file

@ -47,7 +47,8 @@ public struct ConversationsListView: View {
} else if viewModel.isError { } else if viewModel.isError {
ErrorView(title: "conversations.error.title", ErrorView(title: "conversations.error.title",
message: "conversations.error.message", message: "conversations.error.message",
buttonTitle: "conversations.error.button") { buttonTitle: "conversations.error.button")
{
Task { Task {
await viewModel.fetchConversations() await viewModel.fetchConversations()
} }

View file

@ -44,15 +44,15 @@ public extension Font {
static var scaledHeadline: Font { static var scaledHeadline: Font {
customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold) customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold)
} }
static var scaledHeadlineFont: UIFont { static var scaledHeadlineFont: UIFont {
customUIFont(size: userScaledFontSize(baseSize: headline)) customUIFont(size: userScaledFontSize(baseSize: headline))
} }
static var scaledBodyFocused: Font { static var scaledBodyFocused: Font {
customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body) customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body)
} }
static var scaledBodyFocusedFont: UIFont { static var scaledBodyFocusedFont: UIFont {
customUIFont(size: userScaledFontSize(baseSize: body + 2)) customUIFont(size: userScaledFontSize(baseSize: body + 2))
} }
@ -109,11 +109,13 @@ public extension UIFont {
} }
return UIFont(descriptor: descriptor, size: pointSize) return UIFont(descriptor: descriptor, size: pointSize)
} }
var emojiSize: CGFloat { var emojiSize: CGFloat {
self.pointSize pointSize
} }
var emojiBaselineOffset: CGFloat { var emojiBaselineOffset: CGFloat {
// Center emoji with capital letter size of font // Center emoji with capital letter size of font
-(self.emojiSize - self.capHeight) / 2 -(emojiSize - capHeight) / 2
} }
} }

View file

@ -7,15 +7,12 @@ import SwiftUI
// images named in lower case are Apple's symbols // images named in lower case are Apple's symbols
// images inamed in CamelCase are custom // images inamed in CamelCase are custom
extension Label where Title == Text, Icon == Image { public extension Label where Title == Text, Icon == Image {
init(_ title: LocalizedStringKey, imageNamed: String) {
public init (_ title: LocalizedStringKey, imageNamed: String) {
if imageNamed.lowercased() == imageNamed { if imageNamed.lowercased() == imageNamed {
self.init(title, systemImage: imageNamed) self.init(title, systemImage: imageNamed)
} } else {
else {
self.init(title, image: imageNamed) self.init(title, image: imageNamed)
} }
} }
} }

View file

@ -41,7 +41,7 @@ public extension EnvironmentValues {
get { self[IsInCaptureMode.self] } get { self[IsInCaptureMode.self] }
set { self[IsInCaptureMode.self] = newValue } set { self[IsInCaptureMode.self] = newValue }
} }
var isSupporter: Bool { var isSupporter: Bool {
get { self[IsSupporter.self] } get { self[IsSupporter.self] }
set { self[IsSupporter.self] = newValue } set { self[IsSupporter.self] = newValue }

View file

@ -36,7 +36,7 @@ public enum Duration: Int, CaseIterable {
return "enum.durations.custom" return "enum.durations.custom"
} }
} }
public static func mutingDurations() -> [Duration] { public static func mutingDurations() -> [Duration] {
return Self.allCases.filter { $0 != .custom } return Self.allCases.filter { $0 != .custom }
} }
@ -44,7 +44,7 @@ public enum Duration: Int, CaseIterable {
public static func filterDurations() -> [Duration] { public static func filterDurations() -> [Duration] {
return [.infinite, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .sevenDays, .custom] return [.infinite, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .sevenDays, .custom]
} }
public static func pollDurations() -> [Duration] { public static func pollDurations() -> [Duration] {
return [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays] return [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays]
} }

View file

@ -70,7 +70,6 @@ public class RouterPath: ObservableObject {
@Published public var path: [RouterDestination] = [] @Published public var path: [RouterDestination] = []
@Published public var presentedSheet: SheetDestination? @Published public var presentedSheet: SheetDestination?
public init() {} public init() {}
public func navigate(to: RouterDestination) { public func navigate(to: RouterDestination) {

View file

@ -1,6 +1,6 @@
import AVKit
import CoreHaptics import CoreHaptics
import UIKit import UIKit
import AVKit
public class SoundEffectManager { public class SoundEffectManager {
public static let shared: SoundEffectManager = .init() public static let shared: SoundEffectManager = .init()
@ -13,7 +13,7 @@ public class SoundEffectManager {
} }
private let userPreferences = UserPreferences.shared private let userPreferences = UserPreferences.shared
private var currentPlayer: AVAudioPlayer? private var currentPlayer: AVAudioPlayer?
private init() {} private init() {}

View file

@ -1,18 +1,18 @@
import Foundation import Foundation
import SwiftUI
import Models import Models
import Network import Network
import SwiftUI
@MainActor @MainActor
public protocol StatusDataControlling: ObservableObject { public protocol StatusDataControlling: ObservableObject {
var isReblogged: Bool { get set } var isReblogged: Bool { get set }
var isBookmarked: Bool { get set } var isBookmarked: Bool { get set }
var isFavorited: Bool { get set } var isFavorited: Bool { get set }
var favoritesCount: Int { get set } var favoritesCount: Int { get set }
var reblogsCount: Int { get set } var reblogsCount: Int { get set }
var repliesCount: Int { get set } var repliesCount: Int { get set }
func toggleBookmark(remoteStatus: String?) async func toggleBookmark(remoteStatus: String?) async
func toggleReblog(remoteStatus: String?) async func toggleReblog(remoteStatus: String?) async
func toggleFavorite(remoteStatus: String?) async func toggleFavorite(remoteStatus: String?) async
@ -21,14 +21,14 @@ public protocol StatusDataControlling: ObservableObject {
@MainActor @MainActor
public final class StatusDataControllerProvider { public final class StatusDataControllerProvider {
public static let shared = StatusDataControllerProvider() public static let shared = StatusDataControllerProvider()
private var cache: NSMutableDictionary = [:] private var cache: NSMutableDictionary = [:]
private struct CacheKey: Hashable { private struct CacheKey: Hashable {
let statusId: String let statusId: String
let client: Client let client: Client
} }
public func dataController(for status: any AnyStatus, client: Client) -> StatusDataController { public func dataController(for status: any AnyStatus, client: Client) -> StatusDataController {
let key = CacheKey(statusId: status.id, client: client) let key = CacheKey(statusId: status.id, client: client)
if let controller = cache[key] as? StatusDataController { if let controller = cache[key] as? StatusDataController {
@ -38,7 +38,7 @@ public final class StatusDataControllerProvider {
cache[key] = controller cache[key] = controller
return controller return controller
} }
public func updateDataControllers(for statuses: [Status], client: Client) { public func updateDataControllers(for statuses: [Status], client: Client) {
for status in statuses { for status in statuses {
let realStatus: AnyStatus = status.reblog ?? status let realStatus: AnyStatus = status.reblog ?? status
@ -52,42 +52,42 @@ public final class StatusDataControllerProvider {
public final class StatusDataController: StatusDataControlling { public final class StatusDataController: StatusDataControlling {
private let status: AnyStatus private let status: AnyStatus
private let client: Client private let client: Client
public var isReblogged: Bool public var isReblogged: Bool
public var isBookmarked: Bool public var isBookmarked: Bool
public var isFavorited: Bool public var isFavorited: Bool
public var favoritesCount: Int public var favoritesCount: Int
public var reblogsCount: Int public var reblogsCount: Int
public var repliesCount: Int public var repliesCount: Int
init(status: AnyStatus, client: Client) { init(status: AnyStatus, client: Client) {
self.status = status self.status = status
self.client = client self.client = client
self.isReblogged = status.reblogged == true isReblogged = status.reblogged == true
self.isBookmarked = status.bookmarked == true isBookmarked = status.bookmarked == true
self.isFavorited = status.favourited == true isFavorited = status.favourited == true
self.reblogsCount = status.reblogsCount reblogsCount = status.reblogsCount
self.repliesCount = status.repliesCount repliesCount = status.repliesCount
self.favoritesCount = status.favouritesCount favoritesCount = status.favouritesCount
} }
public func updateFrom(status: AnyStatus, publishUpdate: Bool) { public func updateFrom(status: AnyStatus, publishUpdate: Bool) {
self.isReblogged = status.reblogged == true isReblogged = status.reblogged == true
self.isBookmarked = status.bookmarked == true isBookmarked = status.bookmarked == true
self.isFavorited = status.favourited == true isFavorited = status.favourited == true
self.reblogsCount = status.reblogsCount reblogsCount = status.reblogsCount
self.repliesCount = status.repliesCount repliesCount = status.repliesCount
self.favoritesCount = status.favouritesCount favoritesCount = status.favouritesCount
if publishUpdate { if publishUpdate {
objectWillChange.send() objectWillChange.send()
} }
} }
public func toggleFavorite(remoteStatus: String?) async { public func toggleFavorite(remoteStatus: String?) async {
guard client.isAuth else { return } guard client.isAuth else { return }
isFavorited.toggle() isFavorited.toggle()
@ -104,8 +104,7 @@ public final class StatusDataController: StatusDataControlling {
objectWillChange.send() objectWillChange.send()
} }
} }
public func toggleReblog(remoteStatus: String?) async { public func toggleReblog(remoteStatus: String?) async {
guard client.isAuth else { return } guard client.isAuth else { return }
isReblogged.toggle() isReblogged.toggle()
@ -122,7 +121,7 @@ public final class StatusDataController: StatusDataControlling {
objectWillChange.send() objectWillChange.send()
} }
} }
public func toggleBookmark(remoteStatus: String?) async { public func toggleBookmark(remoteStatus: String?) async {
guard client.isAuth else { return } guard client.isAuth else { return }
isBookmarked.toggle() isBookmarked.toggle()

View file

@ -7,7 +7,7 @@ public class StatusEmbedCache {
public static let shared = StatusEmbedCache() public static let shared = StatusEmbedCache()
private var cache: [URL: Status] = [:] private var cache: [URL: Status] = [:]
public var badStatusesURLs = Set<URL>() public var badStatusesURLs = Set<URL>()
private init() {} private init() {}

View file

@ -49,7 +49,7 @@ public class UserPreferences: ObservableObject {
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText @AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
@AppStorage("requested_review") public var requestedReview = false @AppStorage("requested_review") public var requestedReview = false
@AppStorage("collapse-long-posts") public var collapseLongPosts = true @AppStorage("collapse-long-posts") public var collapseLongPosts = true
public enum SwipeActionsIconStyle: String, CaseIterable { public enum SwipeActionsIconStyle: String, CaseIterable {
@ -70,7 +70,7 @@ public class UserPreferences: ObservableObject {
// Main actor-isolated static property 'allCases' cannot be used to // Main actor-isolated static property 'allCases' cannot be used to
// satisfy nonisolated protocol requirement // satisfy nonisolated protocol requirement
// //
nonisolated public static var allCases: [Self] { public nonisolated static var allCases: [Self] {
[.iconWithText, .iconOnly] [.iconWithText, .iconOnly]
} }
} }

View file

@ -127,12 +127,13 @@ public struct ExploreView: View {
private var suggestedAccountsSection: some View { private var suggestedAccountsSection: some View {
Section("explore.section.suggested-users") { Section("explore.section.suggested-users") {
ForEach(viewModel.suggestedAccounts ForEach(viewModel.suggestedAccounts
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in .prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count))
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) { { account in
AccountsListRow(viewModel: .init(account: account, relationShip: relationship)) if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
.listRowBackground(theme.primaryBackgroundColor) AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
} .listRowBackground(theme.primaryBackgroundColor)
} }
}
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) { NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
Text("see-more") Text("see-more")
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
@ -144,11 +145,12 @@ public struct ExploreView: View {
private var trendingTagsSection: some View { private var trendingTagsSection: some View {
Section("explore.section.trending.tags") { Section("explore.section.trending.tags") {
ForEach(viewModel.trendingTags ForEach(viewModel.trendingTags
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in .prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
TagRowView(tag: tag) { tag in
.listRowBackground(theme.primaryBackgroundColor) TagRowView(tag: tag)
.padding(.vertical, 4) .listRowBackground(theme.primaryBackgroundColor)
} .padding(.vertical, 4)
}
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) { NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
Text("see-more") Text("see-more")
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
@ -160,11 +162,12 @@ public struct ExploreView: View {
private var trendingPostsSection: some View { private var trendingPostsSection: some View {
Section("explore.section.trending.posts") { Section("explore.section.trending.posts") {
ForEach(viewModel.trendingStatuses ForEach(viewModel.trendingStatuses
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) { status in
.listRowBackground(theme.primaryBackgroundColor) StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
.padding(.vertical, 8) .listRowBackground(theme.primaryBackgroundColor)
} .padding(.vertical, 8)
}
NavigationLink(value: RouterDestination.trendingTimeline) { NavigationLink(value: RouterDestination.trendingTimeline) {
Text("see-more") Text("see-more")
@ -177,11 +180,12 @@ public struct ExploreView: View {
private var trendingLinksSection: some View { private var trendingLinksSection: some View {
Section("explore.section.trending.links") { Section("explore.section.trending.links") {
ForEach(viewModel.trendingLinks ForEach(viewModel.trendingLinks
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in .prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
StatusRowCardView(card: card) { card in
.listRowBackground(theme.primaryBackgroundColor) StatusRowCardView(card: card)
.padding(.vertical, 8) .listRowBackground(theme.primaryBackgroundColor)
} .padding(.vertical, 8)
}
NavigationLink { NavigationLink {
List { List {
ForEach(viewModel.trendingLinks) { card in ForEach(viewModel.trendingLinks) { card in

View file

@ -1,16 +1,16 @@
import SwiftUI
import Models
import DesignSystem import DesignSystem
import Models
import SwiftUI
public struct TagsListView: View { public struct TagsListView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
let tags: [Tag] let tags: [Tag]
public init(tags: [Tag]) { public init(tags: [Tag]) {
self.tags = tags self.tags = tags
} }
public var body: some View { public var body: some View {
List { List {
ForEach(tags) { tag in ForEach(tags) { tag in

View file

@ -2,6 +2,7 @@ import Foundation
public struct ServerError: Decodable, Error { public struct ServerError: Decodable, Error {
public let error: String? public let error: String?
public var httpCode: Int
} }
extension ServerError: Sendable {} extension ServerError: Sendable {}

View file

@ -22,14 +22,14 @@ public struct ServerFilter: Codable, Identifiable, Hashable, Sendable {
public let context: [Context] public let context: [Context]
public let expiresIn: Int? public let expiresIn: Int?
public let expiresAt: ServerDate? public let expiresAt: ServerDate?
public func hasExpiry() -> Bool { public func hasExpiry() -> Bool {
return expiresAt != nil return expiresAt != nil
} }
public func isExpired() -> Bool { public func isExpired() -> Bool {
if let expiresAtDate = expiresAt?.asDate { if let expiresAtDate = expiresAt?.asDate {
return expiresAtDate < Date() return expiresAtDate < Date()
} else { } else {
return false return false
} }

View file

@ -103,7 +103,6 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
public let sensitive: Bool public let sensitive: Bool
public let language: String? public let language: String?
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
self.id = id self.id = id
self.content = content self.content = content
@ -277,5 +276,3 @@ extension Status: Sendable {}
// Every property in ReblogStatus is immutable. // Every property in ReblogStatus is immutable.
extension ReblogStatus: Sendable {} extension ReblogStatus: Sendable {}

View file

@ -1,8 +1,8 @@
import Combine import Combine
import Foundation import Foundation
import Models import Models
import SwiftUI
import os import os
import SwiftUI
public final class Client: ObservableObject, Equatable, Identifiable, Hashable { public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
public static func == (lhs: Client, rhs: Client) -> Bool { public static func == (lhs: Client, rhs: Client) -> Bool {
@ -61,7 +61,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) { public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
self.server = server self.server = server
self.version = version self.version = version
self.critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server])) critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
urlSession = URLSession.shared urlSession = URLSession.shared
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
} }
@ -141,7 +141,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
linkHandler = .init(rawLink: link) linkHandler = .init(rawLink: link)
} }
logResponseOnError(httpResponse: httpResponse, data: data) logResponseOnError(httpResponse: httpResponse, data: data)
return (try decoder.decode(Entity.self, from: data), linkHandler) return try (decoder.decode(Entity.self, from: data), linkHandler)
} }
public func post<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { public func post<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity {
@ -184,7 +184,10 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
do { do {
return try decoder.decode(Entity.self, from: data) return try decoder.decode(Entity.self, from: data)
} catch { } catch {
if let serverError = try? decoder.decode(ServerError.self, from: data) { if var serverError = try? decoder.decode(ServerError.self, from: data) {
if let httpResponse = httpResponse as? HTTPURLResponse {
serverError.httpCode = httpResponse.statusCode
}
throw serverError throw serverError
} }
throw error throw error

View file

@ -13,7 +13,7 @@ public enum Oauth: Endpoint {
return "oauth/token" return "oauth/token"
} }
} }
public var jsonValue: Encodable? { public var jsonValue: Encodable? {
switch self { switch self {
case let .token(code, clientId, clientSecret): case let .token(code, clientId, clientSecret):
@ -22,7 +22,7 @@ public enum Oauth: Endpoint {
return nil return nil
} }
} }
public struct TokenData: Encodable { public struct TokenData: Encodable {
public let grantType = "authorization_code" public let grantType = "authorization_code"
public let clientId: String public let clientId: String

View file

@ -30,28 +30,28 @@ public struct OpenAIClient {
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder return decoder
} }
public struct ChatRequest: OpenAIRequest { public struct ChatRequest: OpenAIRequest {
public struct Message: Encodable { public struct Message: Encodable {
public let role = "user" public let role = "user"
public let content: String public let content: String
} }
let model = "gpt-3.5-turbo" let model = "gpt-3.5-turbo"
let messages: [Message] let messages: [Message]
let temperature: CGFloat let temperature: CGFloat
var path: String { var path: String {
"chat/completions" "chat/completions"
} }
public init(content: String, temperature: CGFloat) { public init(content: String, temperature: CGFloat) {
self.messages = [.init(content: content)] messages = [.init(content: content)]
self.temperature = temperature self.temperature = temperature
} }
} }
public enum Prompt { public enum Prompt {
case correct(input: String) case correct(input: String)
case shorten(input: String) case shorten(input: String)
@ -81,7 +81,7 @@ public struct OpenAIClient {
public let role: String public let role: String
public let content: String public let content: String
} }
public let message: Message? public let message: Message?
} }

View file

@ -47,23 +47,23 @@ extension Models.Notification.NotificationType {
func icon(isPrivate: Bool) -> Image { func icon(isPrivate: Bool) -> Image {
if isPrivate { if isPrivate {
return Image(systemName:"tray.fill") return Image(systemName: "tray.fill")
} }
switch self { switch self {
case .status: case .status:
return Image(systemName:"pencil") return Image(systemName: "pencil")
case .mention: case .mention:
return Image(systemName:"at") return Image(systemName: "at")
case .reblog: case .reblog:
return Image("Rocket.Fill") return Image("Rocket.Fill")
case .follow, .follow_request: case .follow, .follow_request:
return Image(systemName:"person.fill.badge.plus") return Image(systemName: "person.fill.badge.plus")
case .favourite: case .favourite:
return Image(systemName:"star.fill") return Image(systemName: "star.fill")
case .poll: case .poll:
return Image(systemName:"chart.bar.fill") return Image(systemName: "chart.bar.fill")
case .update: case .update:
return Image(systemName:"pencil.line") return Image(systemName: "pencil.line")
} }
} }

View file

@ -145,7 +145,8 @@ public struct NotificationsListView: View {
case .error: case .error:
ErrorView(title: "notifications.error.title", ErrorView(title: "notifications.error.title",
message: "notifications.error.message", message: "notifications.error.message",
buttonTitle: "action.retry") { buttonTitle: "action.retry")
{
Task { Task {
await viewModel.fetchNotifications() await viewModel.fetchNotifications()
} }

View file

@ -155,7 +155,8 @@ public struct StatusDetailView: View {
private var errorView: some View { private var errorView: some View {
ErrorView(title: "status.error.title", ErrorView(title: "status.error.title",
message: "status.error.message", message: "status.error.message",
buttonTitle: "action.retry") { buttonTitle: "action.retry")
{
Task { Task {
await viewModel.fetch() await viewModel.fetch()
} }

View file

@ -1,8 +1,8 @@
import Env
import Foundation import Foundation
import Models import Models
import Network import Network
import SwiftUI import SwiftUI
import Env
@MainActor @MainActor
class StatusDetailViewModel: ObservableObject { class StatusDetailViewModel: ObservableObject {
@ -79,9 +79,9 @@ class StatusDetailViewModel: ObservableObject {
var statuses = data.context.ancestors var statuses = data.context.ancestors
statuses.append(data.status) statuses.append(data.status)
statuses.append(contentsOf: data.context.descendants) statuses.append(contentsOf: data.context.descendants)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if animate { if animate {
withAnimation { withAnimation {
isLoadingContext = false isLoadingContext = false

View file

@ -51,14 +51,14 @@ struct StatusEditorAccessoryView: View {
matching: .any(of: [.images, .videos])) matching: .any(of: [.images, .videos]))
.fileImporter(isPresented: $isFileImporterPresented, .fileImporter(isPresented: $isFileImporterPresented,
allowedContentTypes: [.image, .video], allowedContentTypes: [.image, .video],
allowsMultipleSelection: true) { result in allowsMultipleSelection: true)
{ result in
if let urls = try? result.get() { if let urls = try? result.get() {
viewModel.processURLs(urls: urls) viewModel.processURLs(urls: urls)
} }
} }
.accessibilityLabel("accessibility.editor.button.attach-photo") .accessibilityLabel("accessibility.editor.button.attach-photo")
.disabled(viewModel.showPoll) .disabled(viewModel.showPoll)
Button { Button {
withAnimation { withAnimation {

View file

@ -1,86 +1,87 @@
import AVFoundation
import Foundation import Foundation
import UIKit import UIKit
import AVFoundation
actor StatusEditorCompressor { actor StatusEditorCompressor {
enum CompressorError: Error { enum CompressorError: Error {
case noData case noData
} }
func compressImageFrom(url: URL) async -> Data? { func compressImageFrom(url: URL) async -> Data? {
return await withCheckedContinuation{ continuation in return await withCheckedContinuation { continuation in
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
continuation.resume(returning: nil) continuation.resume(returning: nil)
return return
} }
let maxPixelSize: Int let maxPixelSize: Int
if Bundle.main.bundlePath.hasSuffix(".appex") { if Bundle.main.bundlePath.hasSuffix(".appex") {
maxPixelSize = 1536 maxPixelSize = 1536
} else { } else {
maxPixelSize = 4096 maxPixelSize = 4096
} }
let downsampleOptions = [ let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
] as [CFString : Any] as CFDictionary ] as [CFString: Any] as CFDictionary
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
continuation.resume(returning: nil) continuation.resume(returning: nil)
return return
} }
let data = NSMutableData() let data = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else { guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
continuation.resume(returning: nil) continuation.resume(returning: nil)
return return
} }
let isPNG: Bool = { let isPNG: Bool = {
guard let utType = cgImage.utType else { return false } guard let utType = cgImage.utType else { return false }
return (utType as String) == UTType.png.identifier return (utType as String) == UTType.png.identifier
}() }()
let destinationProperties = [ let destinationProperties = [
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75 kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75,
] as CFDictionary ] as CFDictionary
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties) CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
CGImageDestinationFinalize(imageDestination) CGImageDestinationFinalize(imageDestination)
continuation.resume(returning: data as Data) continuation.resume(returning: data as Data)
} }
} }
func compressImageForUpload(_ image: UIImage) async throws -> Data { func compressImageForUpload(_ image: UIImage) async throws -> Data {
var image = image var image = image
if image.size.height > 5000 || image.size.width > 5000 { if image.size.height > 5000 || image.size.width > 5000 {
image = image.resized(to: .init(width: image.size.width / 4, image = image.resized(to: .init(width: image.size.width / 4,
height: image.size.height / 4)) height: image.size.height / 4))
} }
guard var imageData = image.jpegData(compressionQuality: 0.8) else { guard var imageData = image.jpegData(compressionQuality: 0.8) else {
throw CompressorError.noData throw CompressorError.noData
} }
let maxSize: Int = 10 * 1024 * 1024 let maxSize = 10 * 1024 * 1024
if imageData.count > maxSize { if imageData.count > maxSize {
while imageData.count > maxSize { while imageData.count > maxSize {
guard let compressedImage = UIImage(data: imageData), guard let compressedImage = UIImage(data: imageData),
let compressedData = compressedImage.jpegData(compressionQuality: 0.8) else { let compressedData = compressedImage.jpegData(compressionQuality: 0.8)
else {
throw CompressorError.noData throw CompressorError.noData
} }
imageData = compressedData imageData = compressedData
} }
} }
return imageData return imageData
} }
func compressVideo(_ url: URL) async -> URL? { func compressVideo(_ url: URL) async -> URL? {
await withCheckedContinuation { continuation in await withCheckedContinuation { continuation in
let urlAsset = AVURLAsset(url: url, options: nil) let urlAsset = AVURLAsset(url: url, options: nil)
@ -97,5 +98,4 @@ actor StatusEditorCompressor {
} }
} }
} }
} }

View file

@ -31,11 +31,11 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
// Main actor-isolated static property 'allCases' cannot be used to // Main actor-isolated static property 'allCases' cannot be used to
// satisfy nonisolated protocol requirement // satisfy nonisolated protocol requirement
// //
nonisolated public static var allCases: [StatusEditorUTTypeSupported] { public nonisolated static var allCases: [StatusEditorUTTypeSupported] {
[.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video, [.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video,
.movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage, .adobeRawImage] .movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage, .adobeRawImage]
} }
static func types() -> [UTType] { static func types() -> [UTType] {
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie] [.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
} }

View file

@ -170,7 +170,7 @@ public struct StatusEditorView: View {
@ViewBuilder @ViewBuilder
private var languageConfirmationDialog: some View { private var languageConfirmationDialog: some View {
if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages, if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages,
let detectedLong = Locale.current.localizedString(forLanguageCode: detected), let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
let selectedLong = Locale.current.localizedString(forLanguageCode: selected) let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
{ {

View file

@ -20,6 +20,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
} }
} }
} }
var theme: Theme? var theme: Theme?
var preferences: UserPreferences? var preferences: UserPreferences?
var languageConfirmationDialogLanguages: (detected: String, selected: String)? var languageConfirmationDialogLanguages: (detected: String, selected: String)?
@ -69,7 +70,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
var statusTextCharacterLength: Int { var statusTextCharacterLength: Int {
urlLengthAdjustments - statusText.string.utf16.count - spoilerTextCount urlLengthAdjustments - statusText.string.utf16.count - spoilerTextCount
} }
private var itemsProvider: [NSItemProvider]? private var itemsProvider: [NSItemProvider]?
@Published var backupStatusText: NSAttributedString? @Published var backupStatusText: NSAttributedString?
@ -135,7 +136,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
} }
private var mentionString: String? private var mentionString: String?
private var uploadTask: Task<Void, Never>? private var uploadTask: Task<Void, Never>?
private var suggestedTask: Task<Void, Never>? private var suggestedTask: Task<Void, Never>?
@ -363,11 +364,11 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
} }
// MARK: - Shar sheet / Item provider // MARK: - Shar sheet / Item provider
func processURLs(urls: [URL]) { func processURLs(urls: [URL]) {
isMediasLoading = true isMediasLoading = true
let items = urls.filter { $0.startAccessingSecurityScopedResource() } let items = urls.filter { $0.startAccessingSecurityScopedResource() }
.compactMap { NSItemProvider(contentsOf: $0) } .compactMap { NSItemProvider(contentsOf: $0) }
processItemsProvider(items: items) processItemsProvider(items: items)
} }
@ -391,7 +392,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
error: nil)) error: nil))
} else if let content = content as? ImageFileTranseferable, } else if let content = content as? ImageFileTranseferable,
let compressedData = await compressor.compressImageFrom(url: content.url), let compressedData = await compressor.compressImageFrom(url: content.url),
let image = UIImage(data: compressedData) { let image = UIImage(data: compressedData)
{
mediasImages.append(.init(image: image, mediasImages.append(.init(image: image,
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
@ -616,7 +618,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
} }
} else if let videoURL = originalContainer.movieTransferable?.url, } else if let videoURL = originalContainer.movieTransferable?.url,
let compressedVideoURL = await compressor.compressVideo(videoURL), let compressedVideoURL = await compressor.compressVideo(videoURL),
let data = try? Data(contentsOf: compressedVideoURL) { let data = try? Data(contentsOf: compressedVideoURL)
{
let uploadedMedia = try await uploadMedia(data: data, mimeType: compressedVideoURL.mimeType()) let uploadedMedia = try await uploadMedia(data: data, mimeType: compressedVideoURL.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,

View file

@ -7,22 +7,23 @@ import SwiftUI
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher { public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ObservedObject private var fetcher: Fetcher @ObservedObject private var fetcher: Fetcher
private let isRemote: Bool private let isRemote: Bool
private let routerPath: RouterPath private let routerPath: RouterPath
private let client: Client private let client: Client
public init(fetcher: Fetcher, public init(fetcher: Fetcher,
client: Client, client: Client,
routerPath: RouterPath, routerPath: RouterPath,
isRemote: Bool = false) { isRemote: Bool = false)
{
self.fetcher = fetcher self.fetcher = fetcher
self.isRemote = isRemote self.isRemote = isRemote
self.client = client self.client = client
self.routerPath = routerPath self.routerPath = routerPath
} }
public var body: some View { public var body: some View {
switch fetcher.statusesState { switch fetcher.statusesState {
case .loading: case .loading:
@ -33,29 +34,30 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
case .error: case .error:
ErrorView(title: "status.error.title", ErrorView(title: "status.error.title",
message: "status.error.loading.message", message: "status.error.loading.message",
buttonTitle: "action.retry") { buttonTitle: "action.retry")
{
Task { Task {
await fetcher.fetchNewestStatuses() await fetcher.fetchNewestStatuses()
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
case let .display(statuses, nextPageState): case let .display(statuses, nextPageState):
ForEach(statuses, id: \.viewId) { status in ForEach(statuses, id: \.viewId) { status in
StatusRowView(viewModel: { StatusRowViewModel(status: status, StatusRowView(viewModel: { StatusRowViewModel(status: status,
client: client, client: client,
routerPath: routerPath, routerPath: routerPath,
isRemote: isRemote) isRemote: isRemote)
}) })
.id(status.id) .id(status.id)
.onAppear { .onAppear {
fetcher.statusDidAppear(status: status) fetcher.statusDidAppear(status: status)
} }
.onDisappear { .onDisappear {
fetcher.statusDidDisappear(status: status) fetcher.statusDidDisappear(status: status)
} }
} }
switch nextPageState { switch nextPageState {
case .hasNextPage: case .hasNextPage:
@ -72,7 +74,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
} }
} }
} }
private var loadingRow: some View { private var loadingRow: some View {
HStack { HStack {
Spacer() Spacer()

View file

@ -22,7 +22,8 @@ class VideoPlayerViewModel: ObservableObject {
} }
guard let player else { return } guard let player else { return }
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem, queue: .main) { [weak self] _ in object: player.currentItem, queue: .main)
{ [weak self] _ in
if autoPlay { if autoPlay {
self?.player?.seek(to: CMTime.zero) self?.player?.seek(to: CMTime.zero)
self?.player?.play() self?.player?.play()

View file

@ -10,20 +10,20 @@ public struct StatusRowView: View {
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact: Bool @Environment(\.isCompact) private var isCompact: Bool
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@StateObject var viewModel: StatusRowViewModel @StateObject var viewModel: StatusRowViewModel
// StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen. // StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen.
public init(viewModel: @escaping () -> StatusRowViewModel) { public init(viewModel: @escaping () -> StatusRowViewModel) {
_viewModel = StateObject(wrappedValue: viewModel()) _viewModel = StateObject(wrappedValue: viewModel())
} }
var contextMenu: some View { var contextMenu: some View {
StatusRowContextMenu(viewModel: viewModel) StatusRowContextMenu(viewModel: viewModel)
} }
public var body: some View { public var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if viewModel.isFiltered, let filter = viewModel.filter { if viewModel.isFiltered, let filter = viewModel.filter {
@ -151,7 +151,7 @@ public struct StatusRowView: View {
client: viewModel.client) client: viewModel.client)
) )
} }
@ViewBuilder @ViewBuilder
private var accesibilityActions: some View { private var accesibilityActions: some View {
// Add the individual mentions as accessibility actions // Add the individual mentions as accessibility actions
@ -160,20 +160,20 @@ public struct StatusRowView: View {
viewModel.routerPath.navigate(to: .accountDetail(id: mention.id)) viewModel.routerPath.navigate(to: .accountDetail(id: mention.id))
} }
} }
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") { Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
withAnimation { withAnimation {
viewModel.displaySpoiler.toggle() viewModel.displaySpoiler.toggle()
} }
} }
Button("@\(viewModel.status.account.username)") { Button("@\(viewModel.status.account.username)") {
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id)) viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
} }
contextMenu contextMenu
} }
private func makeFilterView(filter: Filter) -> some View { private func makeFilterView(filter: Filter) -> some View {
HStack { HStack {
Text("status.filter.filtered-by-\(filter.title)") Text("status.filter.filtered-by-\(filter.title)")
@ -186,7 +186,7 @@ public struct StatusRowView: View {
} }
} }
} }
private var remoteContentLoadingView: some View { private var remoteContentLoadingView: some View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
VStack { VStack {

View file

@ -1,10 +1,10 @@
import Combine import Combine
import DesignSystem
import Env import Env
import Models import Models
import NaturalLanguage import NaturalLanguage
import Network import Network
import SwiftUI import SwiftUI
import DesignSystem
@MainActor @MainActor
public class StatusRowViewModel: ObservableObject { public class StatusRowViewModel: ObservableObject {
@ -39,25 +39,26 @@ public class StatusRowViewModel: ObservableObject {
recalcCollapse() recalcCollapse()
} }
} }
// number of lines to show, nil means show the whole post // number of lines to show, nil means show the whole post
@Published var lineLimit: Int? = nil @Published var lineLimit: Int? = nil
// post length determining if the post should be collapsed // post length determining if the post should be collapsed
let collapseThresholdLength : Int = 750 let collapseThresholdLength: Int = 750
// number of text lines to show on a collpased post // number of text lines to show on a collpased post
let collapsedLines: Int = 8 let collapsedLines: Int = 8
// user preference, set in init // user preference, set in init
var collapseLongPosts: Bool = false var collapseLongPosts: Bool = false
private func recalcCollapse() { private func recalcCollapse() {
let hasContentWarning = !status.spoilerText.asRawText.isEmpty let hasContentWarning = !status.spoilerText.asRawText.isEmpty
let showCollapseButton = collapseLongPosts && isCollapsed && !hasContentWarning let showCollapseButton = collapseLongPosts && isCollapsed && !hasContentWarning
&& finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength && finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength
let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil
if newlineLimit != lineLimit { if newlineLimit != lineLimit {
lineLimit = newlineLimit lineLimit = newlineLimit
} }
} }
private let theme = Theme.shared private let theme = Theme.shared
private let userMentionned: Bool private let userMentionned: Bool
@ -94,7 +95,7 @@ public class StatusRowViewModel: ObservableObject {
textDisabled: Bool = false) textDisabled: Bool = false)
{ {
self.status = status self.status = status
self.finalStatus = status.reblog ?? status finalStatus = status.reblog ?? status
self.client = client self.client = client
self.routerPath = routerPath self.routerPath = routerPath
self.isFocused = isFocused self.isFocused = isFocused
@ -112,13 +113,12 @@ public class StatusRowViewModel: ObservableObject {
displaySpoiler = !finalStatus.spoilerText.asRawText.isEmpty displaySpoiler = !finalStatus.spoilerText.asRawText.isEmpty
} }
if status.mentions.first(where: { $0.id == CurrentAccount.shared.account?.id }) != nil { if status.mentions.first(where: { $0.id == CurrentAccount.shared.account?.id }) != nil {
userMentionned = true userMentionned = true
} else { } else {
userMentionned = false userMentionned = false
} }
isFiltered = filter != nil isFiltered = filter != nil
if let url = embededStatusURL(), if let url = embededStatusURL(),
@ -127,7 +127,7 @@ public class StatusRowViewModel: ObservableObject {
isEmbedLoading = false isEmbedLoading = false
embeddedStatus = embed embeddedStatus = embed
} }
collapseLongPosts = UserPreferences.shared.collapseLongPosts collapseLongPosts = UserPreferences.shared.collapseLongPosts
recalcCollapse() recalcCollapse()
} }
@ -187,7 +187,8 @@ public class StatusRowViewModel: ObservableObject {
if !content.statusesURLs.isEmpty, if !content.statusesURLs.isEmpty,
let url = content.statusesURLs.first, let url = content.statusesURLs.first,
!StatusEmbedCache.shared.badStatusesURLs.contains(url), !StatusEmbedCache.shared.badStatusesURLs.contains(url),
client.hasConnection(with: url) { client.hasConnection(with: url)
{
return url return url
} }
return nil return nil
@ -202,7 +203,7 @@ public class StatusRowViewModel: ObservableObject {
} }
return return
} }
if let embed = StatusEmbedCache.shared.get(url: url) { if let embed = StatusEmbedCache.shared.get(url: url) {
isEmbedLoading = false isEmbedLoading = false
embeddedStatus = embed embeddedStatus = embed
@ -224,8 +225,7 @@ public class StatusRowViewModel: ObservableObject {
} }
if let embed { if let embed {
StatusEmbedCache.shared.set(url: url, status: embed) StatusEmbedCache.shared.set(url: url, status: embed)
} } else {
else {
StatusEmbedCache.shared.badStatusesURLs.insert(url) StatusEmbedCache.shared.badStatusesURLs.insert(url)
} }
withAnimation { withAnimation {

View file

@ -24,7 +24,7 @@ struct StatusRowActionsView: View {
// Main actor-isolated static property 'allCases' cannot be used to // Main actor-isolated static property 'allCases' cannot be used to
// satisfy nonisolated protocol requirement // satisfy nonisolated protocol requirement
// //
nonisolated public static var allCases: [StatusRowActionsView.Action] { public nonisolated static var allCases: [StatusRowActionsView.Action] {
[.respond, .boost, .favorite, .bookmark, .share] [.respond, .boost, .favorite, .bookmark, .share]
} }
@ -99,7 +99,8 @@ struct StatusRowActionsView: View {
{ {
ShareLink(item: url, ShareLink(item: url,
subject: Text(viewModel.finalStatus.account.safeDisplayName), subject: Text(viewModel.finalStatus.account.safeDisplayName),
message: Text(viewModel.finalStatus.content.asRawText)) { message: Text(viewModel.finalStatus.content.asRawText))
{
action.image(dataController: statusDataController) action.image(dataController: statusDataController)
} }
.buttonStyle(.statusAction()) .buttonStyle(.statusAction())
@ -142,7 +143,8 @@ struct StatusRowActionsView: View {
(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id)) (viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id))
if let count = action.count(dataController: statusDataController, if let count = action.count(dataController: statusDataController,
viewModel: viewModel, viewModel: viewModel,
theme: theme), !viewModel.isRemote { theme: theme), !viewModel.isRemote
{
Text("\(count)") Text("\(count)")
.foregroundColor(Color(UIColor.secondaryLabel)) .foregroundColor(Color(UIColor.secondaryLabel))
.font(.scaledFootnote) .font(.scaledFootnote)
@ -150,7 +152,6 @@ struct StatusRowActionsView: View {
} }
} }
} }
private func handleAction(action: Action) { private func handleAction(action: Action) {
Task { Task {

View file

@ -71,7 +71,8 @@ struct StatusRowContextMenu: View {
{ {
ShareLink(item: url, ShareLink(item: url,
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName), subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText)) { message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText))
{
Label("status.action.share", systemImage: "square.and.arrow.up") Label("status.action.share", systemImage: "square.and.arrow.up")
} }

View file

@ -5,7 +5,7 @@ import SwiftUI
struct StatusRowDetailView: View { struct StatusRowDetailView: View {
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
@EnvironmentObject private var statusDataController: StatusDataController @EnvironmentObject private var statusDataController: StatusDataController
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel

View file

@ -124,7 +124,8 @@ public struct StatusRowMediaPreviewView: View {
} }
} }
.alert("status.editor.media.image-description", .alert("status.editor.media.image-description",
isPresented: $isAltAlertDisplayed) { isPresented: $isAltAlertDisplayed)
{
Button("alert.button.ok", action: {}) Button("alert.button.ok", action: {})
} message: { } message: {
Text(altTextDisplayed ?? "") Text(altTextDisplayed ?? "")

View file

@ -109,9 +109,9 @@ struct StatusRowSwipeView: View {
isBookmarked: statusDataController.isBookmarked, isBookmarked: statusDataController.isBookmarked,
privateBoost: privateBoost), privateBoost: privateBoost),
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged, imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
isFavorited: statusDataController.isFavorited, isFavorited: statusDataController.isFavorited,
isBookmarked: statusDataController.isBookmarked, isBookmarked: statusDataController.isBookmarked,
privateBoost: privateBoost)) privateBoost: privateBoost))
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.environment(\.symbolVariants, .none) .environment(\.symbolVariants, .none)
case .iconWithText: case .iconWithText:
@ -120,9 +120,9 @@ struct StatusRowSwipeView: View {
isBookmarked: statusDataController.isBookmarked, isBookmarked: statusDataController.isBookmarked,
privateBoost: privateBoost), privateBoost: privateBoost),
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged, imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
isFavorited: statusDataController.isFavorited, isFavorited: statusDataController.isFavorited,
isBookmarked: statusDataController.isBookmarked, isBookmarked: statusDataController.isBookmarked,
privateBoost: privateBoost)) privateBoost: privateBoost))
.labelStyle(.titleAndIcon) .labelStyle(.titleAndIcon)
.environment(\.symbolVariants, .none) .environment(\.symbolVariants, .none)
} }

View file

@ -7,7 +7,7 @@ struct StatusRowTextView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {

View file

@ -34,7 +34,7 @@ public actor TimelineCache {
try await engine.removeAllData() try await engine.removeAllData()
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) } let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
let dataAndKeys = try zip(itemKeys, statuses) let dataAndKeys = try zip(itemKeys, statuses)
.map { (key: $0, data: try encoder.encode($1)) } .map { try (key: $0, data: encoder.encode($1)) }
try await engine.write(dataAndKeys) try await engine.write(dataAndKeys)
} catch {} } catch {}
} }

View file

@ -9,7 +9,7 @@ actor TimelineDatasource {
} }
func get() -> [Status] { func get() -> [Status] {
statuses.filter{ $0.filtered?.first?.filter.filterAction != .hide } statuses.filter { $0.filtered?.first?.filter.filterAction != .hide }
} }
func reset() { func reset() {

View file

@ -162,7 +162,7 @@ extension TimelineViewModel: StatusesFetcher {
} }
await fetchNewestStatuses() await fetchNewestStatuses()
} }
func fetchNewestStatuses() async { func fetchNewestStatuses() async {
guard let client else { return } guard let client else { return }
do { do {