mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 07:36:48 +00:00
SwiftFormat
This commit is contained in:
parent
f1267620be
commit
6c307aba63
58 changed files with 313 additions and 299 deletions
|
@ -3,13 +3,13 @@ import AppAccount
|
|||
import Conversations
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Explore
|
||||
import LinkPresentation
|
||||
import Lists
|
||||
import Models
|
||||
import Status
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import Explore
|
||||
|
||||
@MainActor
|
||||
extension View {
|
||||
|
|
|
@ -112,7 +112,8 @@ struct IceCubesApp: App {
|
|||
SideBarView(selectedTab: $selectedTab,
|
||||
popToRootTab: $popToRootTab,
|
||||
tabs: availableTabs,
|
||||
routerPath: sidebarRouterPath) {
|
||||
routerPath: sidebarRouterPath)
|
||||
{
|
||||
GeometryReader { _ in
|
||||
HStack(spacing: 0) {
|
||||
ZStack {
|
||||
|
@ -165,12 +166,12 @@ struct IceCubesApp: App {
|
|||
popToRootTab = selectedTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HapticManager.shared.fireHaptic(of: .tabSelection)
|
||||
SoundEffectManager.shared.playSound(of: .tabSelection)
|
||||
|
||||
|
||||
selectedTab = newTab
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if selectedTab == .notifications,
|
||||
let token = appAccountsManager.currentAccount.oauthToken
|
||||
|
@ -179,7 +180,7 @@ struct IceCubesApp: App {
|
|||
watcher.unreadNotificationsCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
})) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab.makeContentView(popToRootTab: $popToRootTab)
|
||||
|
|
|
@ -39,11 +39,11 @@ struct AboutView: View {
|
|||
.cornerRadius(4)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||
}
|
||||
|
||||
|
||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
|
||||
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ struct AboutView: View {
|
|||
Text("\(versionNumber)©2023 Thomas Ricouard")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
Section {
|
||||
Text("""
|
||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||
|
|
|
@ -74,7 +74,7 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
if let token = appAccount.oauthToken {
|
||||
|
|
|
@ -28,19 +28,17 @@ struct AddAccountView: View {
|
|||
@State private var oauthURL: URL?
|
||||
|
||||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
|
||||
private var sanitizedName: String {
|
||||
get {
|
||||
var name = instanceName
|
||||
.replacingOccurrences(of: "http://", with: "")
|
||||
.replacingOccurrences(of: "https://", with: "")
|
||||
|
||||
if name.contains("@") {
|
||||
let parts = name.components(separatedBy: "@")
|
||||
name = parts[parts.count-1] // [@]username@server.address.com
|
||||
}
|
||||
return name
|
||||
var name = instanceName
|
||||
.replacingOccurrences(of: "http://", with: "")
|
||||
.replacingOccurrences(of: "https://", with: "")
|
||||
|
||||
if name.contains("@") {
|
||||
let parts = name.components(separatedBy: "@")
|
||||
name = parts[parts.count - 1] // [@]username@server.address.com
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||
|
@ -94,8 +92,8 @@ struct AddAccountView: View {
|
|||
.onChange(of: instanceName) { newValue in
|
||||
instanceNamePublisher.send(newValue)
|
||||
}
|
||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||
//let newValue = newValue
|
||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in
|
||||
// let newValue = newValue
|
||||
// .replacingOccurrences(of: "http://", with: "")
|
||||
// .replacingOccurrences(of: "https://", with: "")
|
||||
let client = Client(server: sanitizedName)
|
||||
|
@ -106,7 +104,7 @@ struct AddAccountView: View {
|
|||
let instance: Instance = try await client.get(endpoint: Instances.instance)
|
||||
withAnimation {
|
||||
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
|
||||
} else {
|
||||
|
|
|
@ -48,14 +48,14 @@ struct ContentSettingsView: View {
|
|||
Text("settings.content.expand-spoilers")
|
||||
}
|
||||
.disabled(userPreferences.useInstanceContentSettings)
|
||||
|
||||
|
||||
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
|
||||
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
|
||||
Text(media.description).tag(media)
|
||||
}
|
||||
}
|
||||
.disabled(userPreferences.useInstanceContentSettings)
|
||||
|
||||
|
||||
Toggle(isOn: $userPreferences.collapseLongPosts) {
|
||||
Text("settings.content.collapse-long-posts")
|
||||
}
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import Combine
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import Status
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class DisplaySettingsLocalColors: ObservableObject {
|
||||
@Published var tintColor = Theme.shared.tintColor
|
||||
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
|
||||
@Published var labelColor = Theme.shared.labelColor
|
||||
|
||||
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
|
||||
init() {
|
||||
$tintColor
|
||||
.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)
|
||||
$primaryBackgroundColor
|
||||
.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)
|
||||
$secondaryBackgroundColor
|
||||
.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)
|
||||
$labelColor
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -40,15 +40,15 @@ struct DisplaySettingsView: View {
|
|||
@Environment(\.colorScheme) private var colorScheme
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var userPreferences: UserPreferences
|
||||
|
||||
|
||||
@StateObject private var localColors = DisplaySettingsLocalColors()
|
||||
|
||||
@State private var isFontSelectorPresented = false
|
||||
|
||||
|
||||
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
||||
client: Client(server: ""),
|
||||
routerPath: RouterPath()) // translate from latin button
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
exampleSection
|
||||
|
@ -62,14 +62,14 @@ struct DisplaySettingsView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var exampleSection: some View {
|
||||
Section("settings.display.example-toot") {
|
||||
StatusRowView(viewModel: { previewStatusViewModel })
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var themeSection: some View {
|
||||
Section {
|
||||
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
|
||||
|
@ -97,7 +97,7 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var fontSection: some View {
|
||||
Section("settings.display.section.font") {
|
||||
Picker("settings.display.font", selection: .init(get: { () -> FontState in
|
||||
|
@ -140,7 +140,7 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var layoutSection: some View {
|
||||
Section("settings.display.section.display") {
|
||||
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
|
||||
|
@ -169,7 +169,7 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var platformsSection: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
|
@ -186,7 +186,7 @@ struct DisplaySettingsView: View {
|
|||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var resetSection: some View {
|
||||
Section {
|
||||
Button {
|
||||
|
@ -195,12 +195,12 @@ struct DisplaySettingsView: View {
|
|||
theme.avatarShape = .rounded
|
||||
theme.avatarPosition = .top
|
||||
theme.statusActionsDisplay = .full
|
||||
|
||||
|
||||
localColors.tintColor = theme.tintColor
|
||||
localColors.primaryBackgroundColor = theme.primaryBackgroundColor
|
||||
localColors.secondaryBackgroundColor = theme.secondaryBackgroundColor
|
||||
localColors.labelColor = theme.labelColor
|
||||
|
||||
|
||||
} label: {
|
||||
Text("settings.display.restore")
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
@State private var loadingProducts: Bool = false
|
||||
|
@ -86,7 +86,7 @@ struct SupportAppView: View {
|
|||
refreshUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func purchase(product: StoreProduct) async {
|
||||
if !isProcessingPurchase {
|
||||
isProcessingPurchase = true
|
||||
|
@ -101,23 +101,23 @@ struct SupportAppView: View {
|
|||
isProcessingPurchase = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func fetchStoreProducts() {
|
||||
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
|
||||
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 {
|
||||
loadingProducts = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func refreshUserInfo() {
|
||||
Purchases.shared.getCustomerInfo { info, _ in
|
||||
self.customerInfo = info
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func makePurchaseButton(product: StoreProduct) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
|
@ -133,7 +133,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
|
||||
private var aboutSection: some View {
|
||||
Section {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
|
@ -152,7 +152,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var subscriptionSection: some View {
|
||||
Section {
|
||||
if loadingProducts {
|
||||
|
@ -163,14 +163,14 @@ struct SupportAppView: View {
|
|||
Text(Image(systemName: "checkmark.seal.fill"))
|
||||
.foregroundColor(theme.tintColor)
|
||||
.baselineOffset(-1) +
|
||||
Text("settings.support.supporter.subscribed")
|
||||
Text("settings.support.supporter.subscribed")
|
||||
.font(.scaledSubheadline)
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text(Image(systemName: "checkmark.seal.fill"))
|
||||
.foregroundColor(theme.tintColor)
|
||||
.baselineOffset(-1) +
|
||||
Text(Tip.supporter.title)
|
||||
Text(Tip.supporter.title)
|
||||
.font(.scaledSubheadline)
|
||||
Text(Tip.supporter.subtitle)
|
||||
.font(.scaledFootnote)
|
||||
|
@ -179,7 +179,6 @@ struct SupportAppView: View {
|
|||
Spacer()
|
||||
makePurchaseButton(product: subscription)
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
@ -190,7 +189,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var tipsSection: some View {
|
||||
Section {
|
||||
if loadingProducts {
|
||||
|
@ -215,7 +214,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var restorePurchase: some View {
|
||||
Section {
|
||||
HStack {
|
||||
|
@ -234,7 +233,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var linksSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
@ -254,7 +253,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private var loadingPlaceholder: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Account
|
||||
import DesignSystem
|
||||
import Explore
|
||||
import Foundation
|
||||
import Status
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
enum Tab: Int, Identifiable, Hashable {
|
||||
case timeline, notifications, mentions, explore, messages, settings, other
|
||||
|
|
|
@ -54,7 +54,8 @@ class ShareViewController: UIViewController {
|
|||
|
||||
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
|
||||
object: nil,
|
||||
queue: nil) { _ in
|
||||
queue: nil)
|
||||
{ _ in
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
public struct AccountDetailContextMenu: View {
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
||||
|
||||
@ObservedObject var viewModel: AccountDetailViewModel
|
||||
|
||||
|
||||
public var body: some View {
|
||||
if let account = viewModel.account {
|
||||
Section(account.acct) {
|
||||
|
|
|
@ -51,7 +51,8 @@ public struct AccountDetailView: View {
|
|||
|
||||
Picker("", selection: $viewModel.selectedTab) {
|
||||
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
|
||||
id: \.self) { tab in
|
||||
id: \.self)
|
||||
{ tab in
|
||||
Image(systemName: tab.iconName)
|
||||
.tag(tab)
|
||||
}
|
||||
|
@ -300,7 +301,7 @@ public struct AccountDetailView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
AccountDetailContextMenu(viewModel: viewModel)
|
||||
|
||||
|
||||
if !viewModel.isCurrentUser {
|
||||
Button {
|
||||
isEditingRelationshipNote = true
|
||||
|
@ -308,7 +309,7 @@ public struct AccountDetailView: View {
|
|||
Label("account.relation.note.edit", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if isCurrentUser {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
|
@ -329,10 +330,10 @@ public struct AccountDetailView: View {
|
|||
} label: {
|
||||
Label("settings.push.navigation-title", systemImage: "bell")
|
||||
}
|
||||
|
||||
|
||||
if let account = viewModel.account {
|
||||
Divider()
|
||||
|
||||
|
||||
Button {
|
||||
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
|
||||
openURL(url)
|
||||
|
@ -343,7 +344,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .settings
|
||||
} label: {
|
||||
|
|
|
@ -137,9 +137,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
|||
featuredTags: featuredTags,
|
||||
relationships: relationships)
|
||||
} catch {
|
||||
return try await .init(account: account,
|
||||
featuredTags: [],
|
||||
relationships: relationships)
|
||||
return try await .init(account: account,
|
||||
featuredTags: [],
|
||||
relationships: relationships)
|
||||
}
|
||||
}
|
||||
return try await .init(account: account,
|
||||
|
|
|
@ -26,9 +26,9 @@ public struct AccountsListRow: View {
|
|||
@EnvironmentObject private var client: Client
|
||||
|
||||
@StateObject var viewModel: AccountsListRowViewModel
|
||||
|
||||
|
||||
@State private var isEditingRelationshipNote: Bool = false
|
||||
|
||||
|
||||
let isFollowRequest: Bool
|
||||
let requestUpdated: (() -> Void)?
|
||||
|
||||
|
@ -89,7 +89,7 @@ public struct AccountsListRow: View {
|
|||
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),
|
||||
account: viewModel.account,
|
||||
scrollViewProxy: nil)
|
||||
.applyAccountDetailsRowStyle(theme: theme)
|
||||
.applyAccountDetailsRowStyle(theme: theme)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
|
@ -98,6 +98,5 @@ public struct AccountsListRow: View {
|
|||
.environmentObject(currentAccount)
|
||||
.environmentObject(client)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ struct EditFilterView: View {
|
|||
@FocusState private var isTitleFocused: Bool
|
||||
|
||||
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)
|
||||
switch(expirySelection){
|
||||
switch expirySelection {
|
||||
case .infinite:
|
||||
expiresIn = "" // need to send an empty value in order for the server to clear this field in the filter
|
||||
case .custom:
|
||||
|
@ -33,11 +33,11 @@ struct EditFilterView: View {
|
|||
default:
|
||||
expiresIn = String(expirySelection.rawValue + 50)
|
||||
}
|
||||
|
||||
|
||||
return ServerFilterData(title: title,
|
||||
context: contexts,
|
||||
filterAction: filterAction,
|
||||
expiresIn: expiresIn)
|
||||
context: contexts,
|
||||
filterAction: filterAction,
|
||||
expiresIn: expiresIn)
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
|
@ -53,7 +53,7 @@ struct EditFilterView: View {
|
|||
_expiresAt = .init(initialValue: filter?.expiresAt?.asDate)
|
||||
_expirySelection = .init(initialValue: filter?.expiresAt == nil ? .infinite : .custom)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
titleSection
|
||||
|
@ -95,15 +95,14 @@ struct EditFilterView: View {
|
|||
}
|
||||
if expirySelection != .infinite {
|
||||
DatePicker("filter.edit.expiry.date-time",
|
||||
selection: Binding<Date>(get: {self.expiresAt ?? Date()}, set: {self.expiresAt = $0}),
|
||||
displayedComponents: [.date, .hourAndMinute]
|
||||
)
|
||||
.disabled(expirySelection != .custom)
|
||||
selection: Binding<Date>(get: { self.expiresAt ?? Date() }, set: { self.expiresAt = $0 }),
|
||||
displayedComponents: [.date, .hourAndMinute])
|
||||
.disabled(expirySelection != .custom)
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var titleSection: some View {
|
||||
Section("filter.edit.title") {
|
||||
|
@ -116,7 +115,7 @@ struct EditFilterView: View {
|
|||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
if filter == nil, !title.isEmpty {
|
||||
Section {
|
||||
Button {
|
||||
|
|
|
@ -7,7 +7,7 @@ public struct AppAccountView: View {
|
|||
@EnvironmentObject private var routerPath: RouterPath
|
||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
||||
|
||||
@StateObject var viewModel: AppAccountViewModel
|
||||
|
||||
public init(viewModel: AppAccountViewModel) {
|
||||
|
@ -51,7 +51,8 @@ public struct AppAccountView: View {
|
|||
.offset(x: 5, y: -5)
|
||||
} else if viewModel.showBadge,
|
||||
let token = viewModel.appAccount.oauthToken,
|
||||
preferences.getNotificationsCount(for: token) > 0 {
|
||||
preferences.getNotificationsCount(for: token) > 0
|
||||
{
|
||||
let notificationsCount = preferences.getNotificationsCount(for: token)
|
||||
ZStack {
|
||||
Circle()
|
||||
|
|
|
@ -23,7 +23,7 @@ public struct AppAccountsSelectorView: View {
|
|||
.map { preferences.getNotificationsCount(for: $0) }
|
||||
.reduce(0, +) > 0
|
||||
}
|
||||
|
||||
|
||||
private var preferredHeight: CGFloat {
|
||||
var baseHeight: CGFloat = 220
|
||||
baseHeight += CGFloat(60 * accountsViewModel.count)
|
||||
|
@ -48,9 +48,9 @@ public struct AppAccountsSelectorView: View {
|
|||
}
|
||||
.sheet(isPresented: $isPresented, content: {
|
||||
accountsView.presentationDetents([.height(preferredHeight), .large])
|
||||
.onAppear {
|
||||
refreshAccounts()
|
||||
}
|
||||
.onAppear {
|
||||
refreshAccounts()
|
||||
}
|
||||
})
|
||||
.onChange(of: currentAccount.account?.id) { _ in
|
||||
refreshAccounts()
|
||||
|
@ -88,7 +88,7 @@ public struct AppAccountsSelectorView: View {
|
|||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
if accountCreationEnabled {
|
||||
Section {
|
||||
Button {
|
||||
|
@ -121,7 +121,7 @@ public struct AppAccountsSelectorView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var settingsButton: some View {
|
||||
Button {
|
||||
isPresented = false
|
||||
|
|
|
@ -177,7 +177,8 @@ struct ConversationMessageView: View {
|
|||
let width = mediaWidth(proxy: proxy)
|
||||
if let url = attachement.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 {
|
||||
image
|
||||
.resizable()
|
||||
|
|
|
@ -47,7 +47,8 @@ public struct ConversationsListView: View {
|
|||
} else if viewModel.isError {
|
||||
ErrorView(title: "conversations.error.title",
|
||||
message: "conversations.error.message",
|
||||
buttonTitle: "conversations.error.button") {
|
||||
buttonTitle: "conversations.error.button")
|
||||
{
|
||||
Task {
|
||||
await viewModel.fetchConversations()
|
||||
}
|
||||
|
|
|
@ -44,15 +44,15 @@ public extension Font {
|
|||
static var scaledHeadline: Font {
|
||||
customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold)
|
||||
}
|
||||
|
||||
|
||||
static var scaledHeadlineFont: UIFont {
|
||||
customUIFont(size: userScaledFontSize(baseSize: headline))
|
||||
}
|
||||
|
||||
|
||||
static var scaledBodyFocused: Font {
|
||||
customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body)
|
||||
}
|
||||
|
||||
|
||||
static var scaledBodyFocusedFont: UIFont {
|
||||
customUIFont(size: userScaledFontSize(baseSize: body + 2))
|
||||
}
|
||||
|
@ -109,11 +109,13 @@ public extension UIFont {
|
|||
}
|
||||
return UIFont(descriptor: descriptor, size: pointSize)
|
||||
}
|
||||
|
||||
var emojiSize: CGFloat {
|
||||
self.pointSize
|
||||
pointSize
|
||||
}
|
||||
|
||||
var emojiBaselineOffset: CGFloat {
|
||||
// Center emoji with capital letter size of font
|
||||
-(self.emojiSize - self.capHeight) / 2
|
||||
-(emojiSize - capHeight) / 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,12 @@ import SwiftUI
|
|||
// images named in lower case are Apple's symbols
|
||||
// images inamed in CamelCase are custom
|
||||
|
||||
extension Label where Title == Text, Icon == Image {
|
||||
|
||||
public init (_ title: LocalizedStringKey, imageNamed: String) {
|
||||
public extension Label where Title == Text, Icon == Image {
|
||||
init(_ title: LocalizedStringKey, imageNamed: String) {
|
||||
if imageNamed.lowercased() == imageNamed {
|
||||
self.init(title, systemImage: imageNamed)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.init(title, image: imageNamed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ public extension EnvironmentValues {
|
|||
get { self[IsInCaptureMode.self] }
|
||||
set { self[IsInCaptureMode.self] = newValue }
|
||||
}
|
||||
|
||||
|
||||
var isSupporter: Bool {
|
||||
get { self[IsSupporter.self] }
|
||||
set { self[IsSupporter.self] = newValue }
|
||||
|
|
|
@ -36,7 +36,7 @@ public enum Duration: Int, CaseIterable {
|
|||
return "enum.durations.custom"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static func mutingDurations() -> [Duration] {
|
||||
return Self.allCases.filter { $0 != .custom }
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ public enum Duration: Int, CaseIterable {
|
|||
public static func filterDurations() -> [Duration] {
|
||||
return [.infinite, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .sevenDays, .custom]
|
||||
}
|
||||
|
||||
|
||||
public static func pollDurations() -> [Duration] {
|
||||
return [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays]
|
||||
}
|
||||
|
|
|
@ -70,7 +70,6 @@ public class RouterPath: ObservableObject {
|
|||
@Published public var path: [RouterDestination] = []
|
||||
@Published public var presentedSheet: SheetDestination?
|
||||
|
||||
|
||||
public init() {}
|
||||
|
||||
public func navigate(to: RouterDestination) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import AVKit
|
||||
import CoreHaptics
|
||||
import UIKit
|
||||
import AVKit
|
||||
|
||||
public class SoundEffectManager {
|
||||
public static let shared: SoundEffectManager = .init()
|
||||
|
@ -13,7 +13,7 @@ public class SoundEffectManager {
|
|||
}
|
||||
|
||||
private let userPreferences = UserPreferences.shared
|
||||
|
||||
|
||||
private var currentPlayer: AVAudioPlayer?
|
||||
|
||||
private init() {}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public protocol StatusDataControlling: ObservableObject {
|
||||
var isReblogged: Bool { get set }
|
||||
var isBookmarked: Bool { get set }
|
||||
var isFavorited: Bool { get set }
|
||||
|
||||
|
||||
var favoritesCount: Int { get set }
|
||||
var reblogsCount: Int { get set }
|
||||
var repliesCount: Int { get set }
|
||||
|
||||
|
||||
func toggleBookmark(remoteStatus: String?) async
|
||||
func toggleReblog(remoteStatus: String?) async
|
||||
func toggleFavorite(remoteStatus: String?) async
|
||||
|
@ -21,14 +21,14 @@ public protocol StatusDataControlling: ObservableObject {
|
|||
@MainActor
|
||||
public final class StatusDataControllerProvider {
|
||||
public static let shared = StatusDataControllerProvider()
|
||||
|
||||
|
||||
private var cache: NSMutableDictionary = [:]
|
||||
|
||||
|
||||
private struct CacheKey: Hashable {
|
||||
let statusId: String
|
||||
let client: Client
|
||||
}
|
||||
|
||||
|
||||
public func dataController(for status: any AnyStatus, client: Client) -> StatusDataController {
|
||||
let key = CacheKey(statusId: status.id, client: client)
|
||||
if let controller = cache[key] as? StatusDataController {
|
||||
|
@ -38,7 +38,7 @@ public final class StatusDataControllerProvider {
|
|||
cache[key] = controller
|
||||
return controller
|
||||
}
|
||||
|
||||
|
||||
public func updateDataControllers(for statuses: [Status], client: Client) {
|
||||
for status in statuses {
|
||||
let realStatus: AnyStatus = status.reblog ?? status
|
||||
|
@ -52,42 +52,42 @@ public final class StatusDataControllerProvider {
|
|||
public final class StatusDataController: StatusDataControlling {
|
||||
private let status: AnyStatus
|
||||
private let client: Client
|
||||
|
||||
|
||||
public var isReblogged: Bool
|
||||
public var isBookmarked: Bool
|
||||
public var isFavorited: Bool
|
||||
|
||||
|
||||
public var favoritesCount: Int
|
||||
public var reblogsCount: Int
|
||||
public var repliesCount: Int
|
||||
|
||||
|
||||
init(status: AnyStatus, client: Client) {
|
||||
self.status = status
|
||||
self.client = client
|
||||
|
||||
self.isReblogged = status.reblogged == true
|
||||
self.isBookmarked = status.bookmarked == true
|
||||
self.isFavorited = status.favourited == true
|
||||
|
||||
self.reblogsCount = status.reblogsCount
|
||||
self.repliesCount = status.repliesCount
|
||||
self.favoritesCount = status.favouritesCount
|
||||
|
||||
isReblogged = status.reblogged == true
|
||||
isBookmarked = status.bookmarked == true
|
||||
isFavorited = status.favourited == true
|
||||
|
||||
reblogsCount = status.reblogsCount
|
||||
repliesCount = status.repliesCount
|
||||
favoritesCount = status.favouritesCount
|
||||
}
|
||||
|
||||
|
||||
public func updateFrom(status: AnyStatus, publishUpdate: Bool) {
|
||||
self.isReblogged = status.reblogged == true
|
||||
self.isBookmarked = status.bookmarked == true
|
||||
self.isFavorited = status.favourited == true
|
||||
|
||||
self.reblogsCount = status.reblogsCount
|
||||
self.repliesCount = status.repliesCount
|
||||
self.favoritesCount = status.favouritesCount
|
||||
|
||||
isReblogged = status.reblogged == true
|
||||
isBookmarked = status.bookmarked == true
|
||||
isFavorited = status.favourited == true
|
||||
|
||||
reblogsCount = status.reblogsCount
|
||||
repliesCount = status.repliesCount
|
||||
favoritesCount = status.favouritesCount
|
||||
|
||||
if publishUpdate {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func toggleFavorite(remoteStatus: String?) async {
|
||||
guard client.isAuth else { return }
|
||||
isFavorited.toggle()
|
||||
|
@ -104,8 +104,7 @@ public final class StatusDataController: StatusDataControlling {
|
|||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func toggleReblog(remoteStatus: String?) async {
|
||||
guard client.isAuth else { return }
|
||||
isReblogged.toggle()
|
||||
|
@ -122,7 +121,7 @@ public final class StatusDataController: StatusDataControlling {
|
|||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func toggleBookmark(remoteStatus: String?) async {
|
||||
guard client.isAuth else { return }
|
||||
isBookmarked.toggle()
|
||||
|
|
|
@ -7,7 +7,7 @@ public class StatusEmbedCache {
|
|||
public static let shared = StatusEmbedCache()
|
||||
|
||||
private var cache: [URL: Status] = [:]
|
||||
|
||||
|
||||
public var badStatusesURLs = Set<URL>()
|
||||
|
||||
private init() {}
|
||||
|
|
|
@ -49,7 +49,7 @@ public class UserPreferences: ObservableObject {
|
|||
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
|
||||
|
||||
@AppStorage("requested_review") public var requestedReview = false
|
||||
|
||||
|
||||
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
||||
|
||||
public enum SwipeActionsIconStyle: String, CaseIterable {
|
||||
|
@ -70,7 +70,7 @@ public class UserPreferences: ObservableObject {
|
|||
// Main actor-isolated static property 'allCases' cannot be used to
|
||||
// satisfy nonisolated protocol requirement
|
||||
//
|
||||
nonisolated public static var allCases: [Self] {
|
||||
public nonisolated static var allCases: [Self] {
|
||||
[.iconWithText, .iconOnly]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,12 +127,13 @@ public struct ExploreView: View {
|
|||
private var suggestedAccountsSection: some View {
|
||||
Section("explore.section.suggested-users") {
|
||||
ForEach(viewModel.suggestedAccounts
|
||||
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
||||
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count))
|
||||
{ account in
|
||||
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
|
||||
Text("see-more")
|
||||
.foregroundColor(theme.tintColor)
|
||||
|
@ -144,11 +145,12 @@ public struct ExploreView: View {
|
|||
private var trendingTagsSection: some View {
|
||||
Section("explore.section.trending.tags") {
|
||||
ForEach(viewModel.trendingTags
|
||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
||||
TagRowView(tag: tag)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
|
||||
{ tag in
|
||||
TagRowView(tag: tag)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
|
||||
Text("see-more")
|
||||
.foregroundColor(theme.tintColor)
|
||||
|
@ -160,11 +162,12 @@ public struct ExploreView: View {
|
|||
private var trendingPostsSection: some View {
|
||||
Section("explore.section.trending.posts") {
|
||||
ForEach(viewModel.trendingStatuses
|
||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
||||
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
|
||||
{ status in
|
||||
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
NavigationLink(value: RouterDestination.trendingTimeline) {
|
||||
Text("see-more")
|
||||
|
@ -177,11 +180,12 @@ public struct ExploreView: View {
|
|||
private var trendingLinksSection: some View {
|
||||
Section("explore.section.trending.links") {
|
||||
ForEach(viewModel.trendingLinks
|
||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||
StatusRowCardView(card: card)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
|
||||
{ card in
|
||||
StatusRowCardView(card: card)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingLinks) { card in
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
public struct TagsListView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
|
||||
let tags: [Tag]
|
||||
|
||||
|
||||
public init(tags: [Tag]) {
|
||||
self.tags = tags
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
ForEach(tags) { tag in
|
||||
|
|
|
@ -2,6 +2,7 @@ import Foundation
|
|||
|
||||
public struct ServerError: Decodable, Error {
|
||||
public let error: String?
|
||||
public var httpCode: Int
|
||||
}
|
||||
|
||||
extension ServerError: Sendable {}
|
||||
|
|
|
@ -22,14 +22,14 @@ public struct ServerFilter: Codable, Identifiable, Hashable, Sendable {
|
|||
public let context: [Context]
|
||||
public let expiresIn: Int?
|
||||
public let expiresAt: ServerDate?
|
||||
|
||||
|
||||
public func hasExpiry() -> Bool {
|
||||
return expiresAt != nil
|
||||
}
|
||||
|
||||
|
||||
public func isExpired() -> Bool {
|
||||
if let expiresAtDate = expiresAt?.asDate {
|
||||
return expiresAtDate < Date()
|
||||
return expiresAtDate < Date()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -103,7 +103,6 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
|||
public let sensitive: Bool
|
||||
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?) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
|
@ -277,5 +276,3 @@ extension Status: Sendable {}
|
|||
|
||||
// Every property in ReblogStatus is immutable.
|
||||
extension ReblogStatus: Sendable {}
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftUI
|
||||
import os
|
||||
import SwiftUI
|
||||
|
||||
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||
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) {
|
||||
self.server = server
|
||||
self.version = version
|
||||
self.critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
|
||||
critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
|
||||
urlSession = URLSession.shared
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
|||
linkHandler = .init(rawLink: link)
|
||||
}
|
||||
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 {
|
||||
|
@ -184,7 +184,10 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
|||
do {
|
||||
return try decoder.decode(Entity.self, from: data)
|
||||
} 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 error
|
||||
|
|
|
@ -13,7 +13,7 @@ public enum Oauth: Endpoint {
|
|||
return "oauth/token"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var jsonValue: Encodable? {
|
||||
switch self {
|
||||
case let .token(code, clientId, clientSecret):
|
||||
|
@ -22,7 +22,7 @@ public enum Oauth: Endpoint {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct TokenData: Encodable {
|
||||
public let grantType = "authorization_code"
|
||||
public let clientId: String
|
||||
|
|
|
@ -30,28 +30,28 @@ public struct OpenAIClient {
|
|||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
return decoder
|
||||
}
|
||||
|
||||
|
||||
public struct ChatRequest: OpenAIRequest {
|
||||
public struct Message: Encodable {
|
||||
public let role = "user"
|
||||
public let content: String
|
||||
}
|
||||
|
||||
|
||||
let model = "gpt-3.5-turbo"
|
||||
let messages: [Message]
|
||||
|
||||
|
||||
let temperature: CGFloat
|
||||
|
||||
|
||||
var path: String {
|
||||
"chat/completions"
|
||||
}
|
||||
|
||||
public init(content: String, temperature: CGFloat) {
|
||||
self.messages = [.init(content: content)]
|
||||
messages = [.init(content: content)]
|
||||
self.temperature = temperature
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum Prompt {
|
||||
case correct(input: String)
|
||||
case shorten(input: String)
|
||||
|
@ -81,7 +81,7 @@ public struct OpenAIClient {
|
|||
public let role: String
|
||||
public let content: String
|
||||
}
|
||||
|
||||
|
||||
public let message: Message?
|
||||
}
|
||||
|
||||
|
|
|
@ -47,23 +47,23 @@ extension Models.Notification.NotificationType {
|
|||
|
||||
func icon(isPrivate: Bool) -> Image {
|
||||
if isPrivate {
|
||||
return Image(systemName:"tray.fill")
|
||||
return Image(systemName: "tray.fill")
|
||||
}
|
||||
switch self {
|
||||
case .status:
|
||||
return Image(systemName:"pencil")
|
||||
return Image(systemName: "pencil")
|
||||
case .mention:
|
||||
return Image(systemName:"at")
|
||||
return Image(systemName: "at")
|
||||
case .reblog:
|
||||
return Image("Rocket.Fill")
|
||||
case .follow, .follow_request:
|
||||
return Image(systemName:"person.fill.badge.plus")
|
||||
return Image(systemName: "person.fill.badge.plus")
|
||||
case .favourite:
|
||||
return Image(systemName:"star.fill")
|
||||
return Image(systemName: "star.fill")
|
||||
case .poll:
|
||||
return Image(systemName:"chart.bar.fill")
|
||||
return Image(systemName: "chart.bar.fill")
|
||||
case .update:
|
||||
return Image(systemName:"pencil.line")
|
||||
return Image(systemName: "pencil.line")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -145,7 +145,8 @@ public struct NotificationsListView: View {
|
|||
case .error:
|
||||
ErrorView(title: "notifications.error.title",
|
||||
message: "notifications.error.message",
|
||||
buttonTitle: "action.retry") {
|
||||
buttonTitle: "action.retry")
|
||||
{
|
||||
Task {
|
||||
await viewModel.fetchNotifications()
|
||||
}
|
||||
|
|
|
@ -155,7 +155,8 @@ public struct StatusDetailView: View {
|
|||
private var errorView: some View {
|
||||
ErrorView(title: "status.error.title",
|
||||
message: "status.error.message",
|
||||
buttonTitle: "action.retry") {
|
||||
buttonTitle: "action.retry")
|
||||
{
|
||||
Task {
|
||||
await viewModel.fetch()
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
class StatusDetailViewModel: ObservableObject {
|
||||
|
@ -79,9 +79,9 @@ class StatusDetailViewModel: ObservableObject {
|
|||
var statuses = data.context.ancestors
|
||||
statuses.append(data.status)
|
||||
statuses.append(contentsOf: data.context.descendants)
|
||||
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
|
||||
|
||||
if animate {
|
||||
withAnimation {
|
||||
isLoadingContext = false
|
||||
|
|
|
@ -51,14 +51,14 @@ struct StatusEditorAccessoryView: View {
|
|||
matching: .any(of: [.images, .videos]))
|
||||
.fileImporter(isPresented: $isFileImporterPresented,
|
||||
allowedContentTypes: [.image, .video],
|
||||
allowsMultipleSelection: true) { result in
|
||||
allowsMultipleSelection: true)
|
||||
{ result in
|
||||
if let urls = try? result.get() {
|
||||
viewModel.processURLs(urls: urls)
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||
.disabled(viewModel.showPoll)
|
||||
|
||||
|
||||
Button {
|
||||
withAnimation {
|
||||
|
|
|
@ -1,86 +1,87 @@
|
|||
import AVFoundation
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
actor StatusEditorCompressor {
|
||||
enum CompressorError: Error {
|
||||
case noData
|
||||
}
|
||||
|
||||
|
||||
func compressImageFrom(url: URL) async -> Data? {
|
||||
return await withCheckedContinuation{ continuation in
|
||||
return await withCheckedContinuation { continuation in
|
||||
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
||||
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let maxPixelSize: Int
|
||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||
maxPixelSize = 1536
|
||||
} else {
|
||||
maxPixelSize = 4096
|
||||
}
|
||||
|
||||
|
||||
let downsampleOptions = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
|
||||
] as [CFString : Any] as CFDictionary
|
||||
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let isPNG: Bool = {
|
||||
guard let utType = cgImage.utType else { return false }
|
||||
return (utType as String) == UTType.png.identifier
|
||||
}()
|
||||
|
||||
|
||||
let destinationProperties = [
|
||||
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75
|
||||
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75,
|
||||
] as CFDictionary
|
||||
|
||||
|
||||
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
|
||||
CGImageDestinationFinalize(imageDestination)
|
||||
|
||||
|
||||
continuation.resume(returning: data as Data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func compressImageForUpload(_ image: UIImage) async throws -> Data {
|
||||
var image = image
|
||||
if image.size.height > 5000 || image.size.width > 5000 {
|
||||
image = image.resized(to: .init(width: image.size.width / 4,
|
||||
height: image.size.height / 4))
|
||||
}
|
||||
|
||||
|
||||
guard var imageData = image.jpegData(compressionQuality: 0.8) else {
|
||||
throw CompressorError.noData
|
||||
}
|
||||
|
||||
let maxSize: Int = 10 * 1024 * 1024
|
||||
|
||||
let maxSize = 10 * 1024 * 1024
|
||||
|
||||
if imageData.count > maxSize {
|
||||
while imageData.count > maxSize {
|
||||
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
|
||||
}
|
||||
imageData = compressedData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return imageData
|
||||
}
|
||||
|
||||
|
||||
func compressVideo(_ url: URL) async -> URL? {
|
||||
await withCheckedContinuation { continuation in
|
||||
let urlAsset = AVURLAsset(url: url, options: nil)
|
||||
|
@ -97,5 +98,4 @@ actor StatusEditorCompressor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,11 +31,11 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
// Main actor-isolated static property 'allCases' cannot be used to
|
||||
// satisfy nonisolated protocol requirement
|
||||
//
|
||||
nonisolated public static var allCases: [StatusEditorUTTypeSupported] {
|
||||
public nonisolated static var allCases: [StatusEditorUTTypeSupported] {
|
||||
[.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video,
|
||||
.movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage, .adobeRawImage]
|
||||
}
|
||||
|
||||
|
||||
static func types() -> [UTType] {
|
||||
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ public struct StatusEditorView: View {
|
|||
|
||||
@ViewBuilder
|
||||
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 selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
||||
{
|
||||
|
|
|
@ -20,6 +20,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var theme: Theme?
|
||||
var preferences: UserPreferences?
|
||||
var languageConfirmationDialogLanguages: (detected: String, selected: String)?
|
||||
|
@ -69,7 +70,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
var statusTextCharacterLength: Int {
|
||||
urlLengthAdjustments - statusText.string.utf16.count - spoilerTextCount
|
||||
}
|
||||
|
||||
|
||||
private var itemsProvider: [NSItemProvider]?
|
||||
|
||||
@Published var backupStatusText: NSAttributedString?
|
||||
|
@ -135,7 +136,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
private var mentionString: String?
|
||||
|
||||
|
||||
private var uploadTask: Task<Void, Never>?
|
||||
private var suggestedTask: Task<Void, Never>?
|
||||
|
||||
|
@ -363,11 +364,11 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
// MARK: - Shar sheet / Item provider
|
||||
|
||||
|
||||
func processURLs(urls: [URL]) {
|
||||
isMediasLoading = true
|
||||
let items = urls.filter { $0.startAccessingSecurityScopedResource() }
|
||||
.compactMap { NSItemProvider(contentsOf: $0) }
|
||||
.compactMap { NSItemProvider(contentsOf: $0) }
|
||||
processItemsProvider(items: items)
|
||||
}
|
||||
|
||||
|
@ -391,7 +392,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
error: nil))
|
||||
} else if let content = content as? ImageFileTranseferable,
|
||||
let compressedData = await compressor.compressImageFrom(url: content.url),
|
||||
let image = UIImage(data: compressedData) {
|
||||
let image = UIImage(data: compressedData)
|
||||
{
|
||||
mediasImages.append(.init(image: image,
|
||||
movieTransferable: nil,
|
||||
gifTransferable: nil,
|
||||
|
@ -616,7 +618,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
} else if let videoURL = originalContainer.movieTransferable?.url,
|
||||
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())
|
||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||
movieTransferable: originalContainer.movieTransferable,
|
||||
|
|
|
@ -7,22 +7,23 @@ import SwiftUI
|
|||
|
||||
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
|
||||
@ObservedObject private var fetcher: Fetcher
|
||||
private let isRemote: Bool
|
||||
private let routerPath: RouterPath
|
||||
private let client: Client
|
||||
|
||||
|
||||
public init(fetcher: Fetcher,
|
||||
client: Client,
|
||||
routerPath: RouterPath,
|
||||
isRemote: Bool = false) {
|
||||
isRemote: Bool = false)
|
||||
{
|
||||
self.fetcher = fetcher
|
||||
self.isRemote = isRemote
|
||||
self.client = client
|
||||
self.routerPath = routerPath
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
switch fetcher.statusesState {
|
||||
case .loading:
|
||||
|
@ -33,29 +34,30 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
case .error:
|
||||
ErrorView(title: "status.error.title",
|
||||
message: "status.error.loading.message",
|
||||
buttonTitle: "action.retry") {
|
||||
buttonTitle: "action.retry")
|
||||
{
|
||||
Task {
|
||||
await fetcher.fetchNewestStatuses()
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
case let .display(statuses, nextPageState):
|
||||
ForEach(statuses, id: \.viewId) { status in
|
||||
StatusRowView(viewModel: { StatusRowViewModel(status: status,
|
||||
client: client,
|
||||
routerPath: routerPath,
|
||||
isRemote: isRemote)
|
||||
|
||||
})
|
||||
.id(status.id)
|
||||
.onAppear {
|
||||
fetcher.statusDidAppear(status: status)
|
||||
}
|
||||
.onDisappear {
|
||||
fetcher.statusDidDisappear(status: status)
|
||||
}
|
||||
|
||||
})
|
||||
.id(status.id)
|
||||
.onAppear {
|
||||
fetcher.statusDidAppear(status: status)
|
||||
}
|
||||
.onDisappear {
|
||||
fetcher.statusDidDisappear(status: status)
|
||||
}
|
||||
}
|
||||
switch nextPageState {
|
||||
case .hasNextPage:
|
||||
|
@ -72,7 +74,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var loadingRow: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
|
|
@ -22,7 +22,8 @@ class VideoPlayerViewModel: ObservableObject {
|
|||
}
|
||||
guard let player else { return }
|
||||
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
|
||||
object: player.currentItem, queue: .main) { [weak self] _ in
|
||||
object: player.currentItem, queue: .main)
|
||||
{ [weak self] _ in
|
||||
if autoPlay {
|
||||
self?.player?.seek(to: CMTime.zero)
|
||||
self?.player?.play()
|
||||
|
|
|
@ -10,20 +10,20 @@ public struct StatusRowView: View {
|
|||
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
@Environment(\.isCompact) private var isCompact: Bool
|
||||
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
|
||||
@StateObject var viewModel: StatusRowViewModel
|
||||
|
||||
|
||||
// StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen.
|
||||
public init(viewModel: @escaping () -> StatusRowViewModel) {
|
||||
_viewModel = StateObject(wrappedValue: viewModel())
|
||||
}
|
||||
|
||||
|
||||
var contextMenu: some View {
|
||||
StatusRowContextMenu(viewModel: viewModel)
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if viewModel.isFiltered, let filter = viewModel.filter {
|
||||
|
@ -151,7 +151,7 @@ public struct StatusRowView: View {
|
|||
client: viewModel.client)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var accesibilityActions: some View {
|
||||
// Add the individual mentions as accessibility actions
|
||||
|
@ -160,20 +160,20 @@ public struct StatusRowView: View {
|
|||
viewModel.routerPath.navigate(to: .accountDetail(id: mention.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
|
||||
withAnimation {
|
||||
viewModel.displaySpoiler.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button("@\(viewModel.status.account.username)") {
|
||||
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
|
||||
}
|
||||
|
||||
|
||||
contextMenu
|
||||
}
|
||||
|
||||
|
||||
private func makeFilterView(filter: Filter) -> some View {
|
||||
HStack {
|
||||
Text("status.filter.filtered-by-\(filter.title)")
|
||||
|
@ -186,7 +186,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var remoteContentLoadingView: some View {
|
||||
ZStack(alignment: .center) {
|
||||
VStack {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import Combine
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import NaturalLanguage
|
||||
import Network
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
@MainActor
|
||||
public class StatusRowViewModel: ObservableObject {
|
||||
|
@ -39,25 +39,26 @@ public class StatusRowViewModel: ObservableObject {
|
|||
recalcCollapse()
|
||||
}
|
||||
}
|
||||
|
||||
// number of lines to show, nil means show the whole post
|
||||
@Published var lineLimit: Int? = nil
|
||||
// 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
|
||||
let collapsedLines: Int = 8
|
||||
// user preference, set in init
|
||||
var collapseLongPosts: Bool = false
|
||||
|
||||
|
||||
private func recalcCollapse() {
|
||||
let hasContentWarning = !status.spoilerText.asRawText.isEmpty
|
||||
let showCollapseButton = collapseLongPosts && isCollapsed && !hasContentWarning
|
||||
&& finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength
|
||||
&& finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength
|
||||
let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil
|
||||
if newlineLimit != lineLimit {
|
||||
lineLimit = newlineLimit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let theme = Theme.shared
|
||||
private let userMentionned: Bool
|
||||
|
||||
|
@ -94,7 +95,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
textDisabled: Bool = false)
|
||||
{
|
||||
self.status = status
|
||||
self.finalStatus = status.reblog ?? status
|
||||
finalStatus = status.reblog ?? status
|
||||
self.client = client
|
||||
self.routerPath = routerPath
|
||||
self.isFocused = isFocused
|
||||
|
@ -112,13 +113,12 @@ public class StatusRowViewModel: ObservableObject {
|
|||
displaySpoiler = !finalStatus.spoilerText.asRawText.isEmpty
|
||||
}
|
||||
|
||||
|
||||
if status.mentions.first(where: { $0.id == CurrentAccount.shared.account?.id }) != nil {
|
||||
userMentionned = true
|
||||
} else {
|
||||
userMentionned = false
|
||||
}
|
||||
|
||||
|
||||
isFiltered = filter != nil
|
||||
|
||||
if let url = embededStatusURL(),
|
||||
|
@ -127,7 +127,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
isEmbedLoading = false
|
||||
embeddedStatus = embed
|
||||
}
|
||||
|
||||
|
||||
collapseLongPosts = UserPreferences.shared.collapseLongPosts
|
||||
recalcCollapse()
|
||||
}
|
||||
|
@ -187,7 +187,8 @@ public class StatusRowViewModel: ObservableObject {
|
|||
if !content.statusesURLs.isEmpty,
|
||||
let url = content.statusesURLs.first,
|
||||
!StatusEmbedCache.shared.badStatusesURLs.contains(url),
|
||||
client.hasConnection(with: url) {
|
||||
client.hasConnection(with: url)
|
||||
{
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
|
@ -202,7 +203,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let embed = StatusEmbedCache.shared.get(url: url) {
|
||||
isEmbedLoading = false
|
||||
embeddedStatus = embed
|
||||
|
@ -224,8 +225,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
}
|
||||
if let embed {
|
||||
StatusEmbedCache.shared.set(url: url, status: embed)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
StatusEmbedCache.shared.badStatusesURLs.insert(url)
|
||||
}
|
||||
withAnimation {
|
||||
|
|
|
@ -24,7 +24,7 @@ struct StatusRowActionsView: View {
|
|||
// Main actor-isolated static property 'allCases' cannot be used to
|
||||
// satisfy nonisolated protocol requirement
|
||||
//
|
||||
nonisolated public static var allCases: [StatusRowActionsView.Action] {
|
||||
public nonisolated static var allCases: [StatusRowActionsView.Action] {
|
||||
[.respond, .boost, .favorite, .bookmark, .share]
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,8 @@ struct StatusRowActionsView: View {
|
|||
{
|
||||
ShareLink(item: url,
|
||||
subject: Text(viewModel.finalStatus.account.safeDisplayName),
|
||||
message: Text(viewModel.finalStatus.content.asRawText)) {
|
||||
message: Text(viewModel.finalStatus.content.asRawText))
|
||||
{
|
||||
action.image(dataController: statusDataController)
|
||||
}
|
||||
.buttonStyle(.statusAction())
|
||||
|
@ -142,7 +143,8 @@ struct StatusRowActionsView: View {
|
|||
(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id))
|
||||
if let count = action.count(dataController: statusDataController,
|
||||
viewModel: viewModel,
|
||||
theme: theme), !viewModel.isRemote {
|
||||
theme: theme), !viewModel.isRemote
|
||||
{
|
||||
Text("\(count)")
|
||||
.foregroundColor(Color(UIColor.secondaryLabel))
|
||||
.font(.scaledFootnote)
|
||||
|
@ -150,7 +152,6 @@ struct StatusRowActionsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleAction(action: Action) {
|
||||
Task {
|
||||
|
|
|
@ -71,7 +71,8 @@ struct StatusRowContextMenu: View {
|
|||
{
|
||||
ShareLink(item: url,
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import SwiftUI
|
|||
|
||||
struct StatusRowDetailView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
|
||||
@EnvironmentObject private var statusDataController: StatusDataController
|
||||
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
|
|
@ -124,7 +124,8 @@ public struct StatusRowMediaPreviewView: View {
|
|||
}
|
||||
}
|
||||
.alert("status.editor.media.image-description",
|
||||
isPresented: $isAltAlertDisplayed) {
|
||||
isPresented: $isAltAlertDisplayed)
|
||||
{
|
||||
Button("alert.button.ok", action: {})
|
||||
} message: {
|
||||
Text(altTextDisplayed ?? "")
|
||||
|
|
|
@ -109,9 +109,9 @@ struct StatusRowSwipeView: View {
|
|||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost),
|
||||
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
||||
isFavorited: statusDataController.isFavorited,
|
||||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost))
|
||||
isFavorited: statusDataController.isFavorited,
|
||||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost))
|
||||
.labelStyle(.iconOnly)
|
||||
.environment(\.symbolVariants, .none)
|
||||
case .iconWithText:
|
||||
|
@ -120,9 +120,9 @@ struct StatusRowSwipeView: View {
|
|||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost),
|
||||
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
||||
isFavorited: statusDataController.isFavorited,
|
||||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost))
|
||||
isFavorited: statusDataController.isFavorited,
|
||||
isBookmarked: statusDataController.isBookmarked,
|
||||
privateBoost: privateBoost))
|
||||
.labelStyle(.titleAndIcon)
|
||||
.environment(\.symbolVariants, .none)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ struct StatusRowTextView: View {
|
|||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
|
|
|
@ -34,7 +34,7 @@ public actor TimelineCache {
|
|||
try await engine.removeAllData()
|
||||
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
|
||||
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)
|
||||
} catch {}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ actor TimelineDatasource {
|
|||
}
|
||||
|
||||
func get() -> [Status] {
|
||||
statuses.filter{ $0.filtered?.first?.filter.filterAction != .hide }
|
||||
statuses.filter { $0.filtered?.first?.filter.filterAction != .hide }
|
||||
}
|
||||
|
||||
func reset() {
|
||||
|
|
|
@ -162,7 +162,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
await fetchNewestStatuses()
|
||||
}
|
||||
|
||||
|
||||
func fetchNewestStatuses() async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
|
|
Loading…
Reference in a new issue