mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 10:11:00 +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 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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -30,17 +30,15 @@ struct AddAccountView: View {
|
||||||
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 {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
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
|
||||||
|
@ -17,19 +17,19 @@ class DisplaySettingsLocalColors: ObservableObject {
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,7 @@ struct SupportAppView: View {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -35,9 +35,9 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -95,10 +95,9 @@ 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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 {
|
||||||
|
@ -65,23 +65,23 @@ public final class StatusDataController: StatusDataControlling {
|
||||||
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()
|
||||||
|
@ -105,7 +105,6 @@ public final class StatusDataController: StatusDataControlling {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -29,7 +29,7 @@ public struct ServerFilter: Codable, Identifiable, Hashable, Sendable {
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -47,7 +47,7 @@ public struct OpenAIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -51,7 +51,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +60,6 @@ struct StatusEditorAccessoryView: View {
|
||||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||||
.disabled(viewModel.showPoll)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.showPoll.toggle()
|
viewModel.showPoll.toggle()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
actor StatusEditorCompressor {
|
actor StatusEditorCompressor {
|
||||||
enum CompressorError: Error {
|
enum CompressorError: Error {
|
||||||
|
@ -8,7 +8,7 @@ actor StatusEditorCompressor {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -26,7 +26,7 @@ actor StatusEditorCompressor {
|
||||||
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)
|
||||||
|
@ -45,7 +45,7 @@ actor StatusEditorCompressor {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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)
|
||||||
|
@ -66,12 +66,13 @@ actor StatusEditorCompressor {
|
||||||
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
|
||||||
|
@ -97,5 +98,4 @@ actor StatusEditorCompressor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)?
|
||||||
|
@ -367,7 +368,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
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,
|
||||||
|
|
|
@ -16,7 +16,8 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
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
|
||||||
|
@ -33,13 +34,14 @@ 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
|
||||||
|
@ -48,14 +50,14 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,10 +39,11 @@ 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
|
||||||
|
@ -51,7 +52,7 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
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
|
||||||
|
@ -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,7 +113,6 @@ 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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -151,7 +153,6 @@ struct StatusRowActionsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func handleAction(action: Action) {
|
private func handleAction(action: Action) {
|
||||||
Task {
|
Task {
|
||||||
if viewModel.isRemote, viewModel.localStatusId == nil || viewModel.localStatus == nil {
|
if viewModel.isRemote, viewModel.localStatusId == nil || viewModel.localStatus == nil {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ?? "")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue