SwiftFormat

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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) {

View file

@ -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: {

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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() {}

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
{

View file

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

View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 ?? "")

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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