Swiftformat

This commit is contained in:
Thomas Ricouard 2023-02-18 07:26:48 +01:00
parent 425a4eef4f
commit ca4e80101f
39 changed files with 168 additions and 160 deletions

View file

@ -62,7 +62,8 @@ struct IceCubesApp: App {
pushNotificationsService.handledNotification = nil pushNotificationsService.handledNotification = nil
if appAccountsManager.currentAccount.oauthToken?.accessToken != notification?.account.token.accessToken, if appAccountsManager.currentAccount.oauthToken?.accessToken != notification?.account.token.accessToken,
let account = appAccountsManager.availableAccounts.first(where: let account = appAccountsManager.availableAccounts.first(where:
{ $0.oauthToken?.accessToken == notification?.account.token.accessToken }) { { $0.oauthToken?.accessToken == notification?.account.token.accessToken })
{
appAccountsManager.currentAccount = account appAccountsManager.currentAccount = account
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
selectedTab = .notifications selectedTab = .notifications
@ -143,7 +144,7 @@ struct IceCubesApp: App {
sideBarLoadedTabs.removeAll() sideBarLoadedTabs.removeAll()
} }
} }
private var notificationsSecondaryColumn: some View { private var notificationsSecondaryColumn: some View {
NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil) NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil)
.environment(\.isSecondaryColumn, true) .environment(\.isSecondaryColumn, true)

View file

@ -1,24 +1,24 @@
import SwiftUI
import Models
import Env
import DesignSystem import DesignSystem
import Status import Env
import Models
import Network import Network
import Status
import SwiftUI
public struct ReportView: View { public struct ReportView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
let status: Status let status: Status
@State private var commentText: String = "" @State private var commentText: String = ""
@State private var isSendingReport: Bool = false @State private var isSendingReport: Bool = false
struct ReportSent: Decodable { struct ReportSent: Decodable {
let id : String let id: String
} }
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
Form { Form {
@ -28,7 +28,7 @@ public struct ReportView: View {
axis: .vertical) axis: .vertical)
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
StatusEmbeddedView(status: status, client: .init(server: ""), routerPath: RouterPath()) StatusEmbeddedView(status: status, client: .init(server: ""), routerPath: RouterPath())
.allowsHitTesting(false) .allowsHitTesting(false)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
@ -45,9 +45,9 @@ public struct ReportView: View {
Task { Task {
do { do {
let _: ReportSent = let _: ReportSent =
try await client.post(endpoint: Statuses.report(accountId: status.account.id, try await client.post(endpoint: Statuses.report(accountId: status.account.id,
statusId: status.id, statusId: status.id,
comment: commentText)) comment: commentText))
dismiss() dismiss()
isSendingReport = false isSendingReport = false
} catch { } catch {
@ -62,7 +62,7 @@ public struct ReportView: View {
} }
} }
} }
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button { Button {
dismiss() dismiss()

View file

@ -21,7 +21,7 @@ struct ContentSettingsView: View {
Section("settings.content.media") { Section("settings.content.media") {
Toggle(isOn: $userPreferences.showAltTextForMedia) { Toggle(isOn: $userPreferences.showAltTextForMedia) {
Text("settings.content.media.show.alt") Text("settings.content.media.show.alt")
} }
}.listRowBackground(theme.primaryBackgroundColor) }.listRowBackground(theme.primaryBackgroundColor)
@ -69,7 +69,6 @@ struct ContentSettingsView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
.navigationTitle("settings.content.navigation-title") .navigationTitle("settings.content.navigation-title")
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network
import Status import Status
import SwiftUI import SwiftUI
import Network
struct DisplaySettingsView: View { struct DisplaySettingsView: View {
typealias FontState = Theme.FontState typealias FontState = Theme.FontState
@ -67,10 +67,10 @@ struct DisplaySettingsView: View {
} }
} }
.navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() }) .navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() })
Toggle("settings.display.font.rounded", isOn: $userPreferences.useSFRoundedFont) Toggle("settings.display.font.rounded", isOn: $userPreferences.useSFRoundedFont)
.disabled(userPreferences.chosenFont != nil) .disabled(userPreferences.chosenFont != nil)
VStack { VStack {
Slider(value: $userPreferences.fontSizeScale, in: 0.5 ... 1.5, step: 0.1) Slider(value: $userPreferences.fontSizeScale, in: 0.5 ... 1.5, step: 0.1)
Text("settings.display.font.scaling-\(String(format: "%.1f", userPreferences.fontSizeScale))") Text("settings.display.font.scaling-\(String(format: "%.1f", userPreferences.fontSizeScale))")
@ -81,7 +81,7 @@ struct DisplaySettingsView: View {
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
Section("settings.display.section.display") { Section("settings.display.section.display") {
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) { Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in

View file

@ -44,9 +44,9 @@ struct IconSelectorView: View {
let icons: [Icon] let icons: [Icon]
static let items = [ static let items = [
IconSelector(title:"settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8, IconSelector(title: "settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8,
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25]), .alt15, .alt16, .alt17, .alt18, .alt19, .alt25]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]), IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]), IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) @te6-in (GitHub)", icons: [.alt29, .alt30, .alt31, .alt32]), IconSelector(title: "\("settings.app.icon.designed-by".localized) @te6-in (GitHub)", icons: [.alt29, .alt30, .alt31, .alt32]),
@ -108,7 +108,7 @@ struct IconSelectorView: View {
} }
extension String { extension String {
var localized: String { var localized: String {
return NSLocalizedString(self, comment:"") return NSLocalizedString(self, comment: "")
} }
} }

View file

@ -100,7 +100,7 @@ struct SettingsTabs: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
} }
private func logoutAccount(account: AppAccount) async { private func logoutAccount(account: AppAccount) async {
if let token = account.oauthToken, if let token = account.oauthToken,
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
@ -235,7 +235,7 @@ struct SettingsTabs: View {
AddAccountView() AddAccountView()
} }
} }
private var editAccountButton: some View { private var editAccountButton: some View {
Button(role: isEditingAccount ? .none : .destructive) { Button(role: isEditingAccount ? .none : .destructive) {
withAnimation { withAnimation {

View file

@ -1,13 +1,13 @@
import AppAccount import AppAccount
import CryptoKit import CryptoKit
import Env import Env
import Intents
import KeychainSwift import KeychainSwift
import Models import Models
import UIKit
import UserNotifications
import Intents
import Network import Network
import Notifications import Notifications
import UIKit
import UserNotifications
@MainActor @MainActor
class NotificationService: UNNotificationServiceExtension { class NotificationService: UNNotificationServiceExtension {
@ -62,13 +62,12 @@ class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.body = notification.body.escape() bestAttemptContent.body = notification.body.escape()
bestAttemptContent.userInfo["plaintext"] = plaintextData bestAttemptContent.userInfo["plaintext"] = plaintextData
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf")) bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf"))
let preferences = UserPreferences.shared let preferences = UserPreferences.shared
preferences.pushNotificationsCount += 1 preferences.pushNotificationsCount += 1
bestAttemptContent.badge = .init(integerLiteral: preferences.pushNotificationsCount) bestAttemptContent.badge = .init(integerLiteral: preferences.pushNotificationsCount)
if let urlString = notification.icon, if let urlString = notification.icon,
let url = URL(string: urlString) let url = URL(string: urlString)
{ {
@ -81,9 +80,10 @@ class NotificationService: UNNotificationServiceExtension {
if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
if let image = UIImage(data: data) { if let image = UIImage(data: data) {
try? image.pngData()?.write(to: fileURL) try? image.pngData()?.write(to: fileURL)
if let remoteNotification = await toRemoteNotification(localNotification: notification), if let remoteNotification = await toRemoteNotification(localNotification: notification),
let type = remoteNotification.supportedType { let type = remoteNotification.supportedType
{
let intent = buildMessageIntent(remoteNotification: remoteNotification, let intent = buildMessageIntent(remoteNotification: remoteNotification,
currentUser: bestAttemptContent.userInfo["i"] as? String ?? "", currentUser: bestAttemptContent.userInfo["i"] as? String ?? "",
avatarURL: fileURL) avatarURL: fileURL)
@ -111,7 +111,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
} }
private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? { private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? {
do { do {
if let account = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) { if let account = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) {
@ -124,10 +124,11 @@ class NotificationService: UNNotificationServiceExtension {
} }
return nil return nil
} }
private func buildMessageIntent(remoteNotification: Models.Notification, private func buildMessageIntent(remoteNotification: Models.Notification,
currentUser: String, currentUser: String,
avatarURL: URL) -> INSendMessageIntent { avatarURL: URL) -> INSendMessageIntent
{
let handle = INPersonHandle(value: remoteNotification.account.id, type: .unknown) let handle = INPersonHandle(value: remoteNotification.account.id, type: .unknown)
let avatar = INImage(url: avatarURL) let avatar = INImage(url: avatarURL)
let sender = INPerson(personHandle: handle, let sender = INPerson(personHandle: handle,

View file

@ -19,6 +19,7 @@ public class AppAccountViewModel: ObservableObject {
} }
} }
} }
@Published var roundedAvatar: UIImage? @Published var roundedAvatar: UIImage?
var acct: String { var acct: String {
@ -39,31 +40,32 @@ public class AppAccountViewModel: ObservableObject {
do { do {
account = Self.accountsCache[appAccount.id] account = Self.accountsCache[appAccount.id]
roundedAvatar = Self.avatarsCache[appAccount.id] roundedAvatar = Self.avatarsCache[appAccount.id]
account = try await client.get(endpoint: Accounts.verifyCredentials) account = try await client.get(endpoint: Accounts.verifyCredentials)
Self.accountsCache[appAccount.id] = account Self.accountsCache[appAccount.id] = account
if let account { if let account {
await refreshAvatar(account: account) await refreshAvatar(account: account)
} }
} catch {} } catch {}
} }
private func refreshAcct(account: Account) { private func refreshAcct(account: Account) {
do { do {
if appAccount.accountName == nil { if appAccount.accountName == nil {
appAccount.accountName = "\(account.acct)@\(appAccount.server)" appAccount.accountName = "\(account.acct)@\(appAccount.server)"
try appAccount.save() try appAccount.save()
} }
} catch { } } catch {}
} }
private func refreshAvatar(account: Account) async { private func refreshAvatar(account: Account) async {
if let (data, _) = try? await URLSession.shared.data(from: account.avatar), if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
let image = UIImage(data: data)?.roundedImage { let image = UIImage(data: data)?.roundedImage
roundedAvatar = image {
Self.avatarsCache[account.id] = image roundedAvatar = image
Self.avatarsCache[account.id] = image
} }
} }
} }

View file

@ -64,7 +64,7 @@ public struct AppAccountsSelectorView: View {
.frame(width: 9, height: 9) .frame(width: 9, height: 9)
} }
} }
.accessibilityLabel("accessibility.app-account.selector.accounts") .accessibilityLabel("accessibility.app-account.selector.accounts")
} }
@ViewBuilder @ViewBuilder

View file

@ -4,8 +4,8 @@ import Network
@MainActor @MainActor
public class CurrentAccount: ObservableObject { public class CurrentAccount: ObservableObject {
static private var accountsCache: [String: Account] = [:] private static var accountsCache: [String: Account] = [:]
@Published public private(set) var account: Account? @Published public private(set) var account: Account?
@Published public private(set) var lists: [List] = [] @Published public private(set) var lists: [List] = []
@Published public private(set) var tags: [Tag] = [] @Published public private(set) var tags: [Tag] = []

View file

@ -18,12 +18,12 @@ public extension EnvironmentValues {
get { self[SecondaryColumnKey.self] } get { self[SecondaryColumnKey.self] }
set { self[SecondaryColumnKey.self] = newValue } set { self[SecondaryColumnKey.self] = newValue }
} }
var extraLeadingInset: CGFloat { var extraLeadingInset: CGFloat {
get { self[ExtraLeadingInset.self] } get { self[ExtraLeadingInset.self] }
set { self[ExtraLeadingInset.self] = newValue } set { self[ExtraLeadingInset.self] = newValue }
} }
var isCompact: Bool { var isCompact: Bool {
get { self[IsCompact.self] } get { self[IsCompact.self] }
set { self[IsCompact.self] = newValue } set { self[IsCompact.self] = newValue }

View file

@ -30,21 +30,21 @@ public class PushNotificationsService: NSObject, ObservableObject {
static let keychainAuthKey = "notifications_auth_key" static let keychainAuthKey = "notifications_auth_key"
static let keychainPrivateKey = "notifications_private_key" static let keychainPrivateKey = "notifications_private_key"
} }
public static let shared = PushNotificationsService() public static let shared = PushNotificationsService()
public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = [] public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = []
@Published public var pushToken: Data? @Published public var pushToken: Data?
@Published public var handledNotification: HandledNotification? @Published public var handledNotification: HandledNotification?
override init() { override init() {
super.init() super.init()
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
} }
private var keychain: KeychainSwift { private var keychain: KeychainSwift {
let keychain = KeychainSwift() let keychain = KeychainSwift()
#if !DEBUG && !targetEnvironment(simulator) #if !DEBUG && !targetEnvironment(simulator)
@ -135,18 +135,19 @@ public class PushNotificationsService: NSObject, ObservableObject {
} }
extension PushNotificationsService: UNUserNotificationCenterDelegate { extension PushNotificationsService: UNUserNotificationCenterDelegate {
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
guard let plaintext = response.notification.request.content.userInfo["plaintext"] as? Data, guard let plaintext = response.notification.request.content.userInfo["plaintext"] as? Data,
let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext), let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext),
let account = subscriptions.first(where: { $0.account.token.accessToken == mastodonPushNotification.accessToken }) else { let account = subscriptions.first(where: { $0.account.token.accessToken == mastodonPushNotification.accessToken })
return else {
return
} }
do { do {
let client = Client(server: account.account.server, oauthToken: account.account.token) let client = Client(server: account.account.server, oauthToken: account.account.token)
let notification: Models.Notification = let notification: Models.Notification =
try await client.get(endpoint: Notifications.notification(id:String(mastodonPushNotification.notificationID))) try await client.get(endpoint: Notifications.notification(id: String(mastodonPushNotification.notificationID)))
self.handledNotification = .init(account: account.account, notification: notification) handledNotification = .init(account: account.account, notification: notification)
} catch { } } catch {}
} }
} }

View file

@ -42,10 +42,10 @@ public enum StatusAction: String, CaseIterable, Identifiable {
} }
public func color(themeTintColor: Color, useThemeColor: Bool, outside: Bool) -> Color { public func color(themeTintColor: Color, useThemeColor: Bool, outside: Bool) -> Color {
if (useThemeColor) { if useThemeColor {
return outside ? themeTintColor : .gray return outside ? themeTintColor : .gray
} }
switch self { switch self {
case .none: case .none:
return .gray return .gray

View file

@ -38,7 +38,7 @@ public class UserPreferences: ObservableObject {
@AppStorage("show_tab_label_iphone") public var showiPhoneTabLabel = true @AppStorage("show_tab_label_iphone") public var showiPhoneTabLabel = true
@AppStorage("show_alt_text_for_media") public var showAltTextForMedia = true @AppStorage("show_alt_text_for_media") public var showAltTextForMedia = true
@AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true @AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true
@AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite @AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite
@ -47,9 +47,9 @@ public class UserPreferences: ObservableObject {
@AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = StatusAction.none @AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = StatusAction.none
@AppStorage("swipeactions-use-theme-color") public var swipeActionsUseThemeColor = false @AppStorage("swipeactions-use-theme-color") public var swipeActionsUseThemeColor = false
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText @AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
@AppStorage("font_use_sf_rounded") public var useSFRoundedFont = false @AppStorage("font_use_sf_rounded") public var useSFRoundedFont = false
@AppStorage("requested_review") public var requestedReview = false @AppStorage("requested_review") public var requestedReview = false
public enum SwipeActionsIconStyle: String, CaseIterable { public enum SwipeActionsIconStyle: String, CaseIterable {

View file

@ -4,7 +4,7 @@ public final class Account: Codable, Identifiable, Equatable, Hashable {
public static func == (lhs: Account, rhs: Account) -> Bool { public static func == (lhs: Account, rhs: Account) -> Bool {
lhs.id == rhs.id lhs.id == rhs.id
} }
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(id) hasher.combine(id)
} }
@ -55,7 +55,6 @@ public final class Account: Codable, Identifiable, Equatable, Hashable {
return header.lastPathComponent != "missing.png" return header.lastPathComponent != "missing.png"
} }
public init(id: String, username: String, displayName: String, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) { public init(id: String, username: String, displayName: String, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) {
self.id = id self.id = id
self.username = username self.username = username
@ -77,7 +76,7 @@ public final class Account: Codable, Identifiable, Equatable, Hashable {
self.bot = bot self.bot = bot
self.discoverable = discoverable self.discoverable = discoverable
} }
public static func placeholder() -> Account { public static func placeholder() -> Account {
.init(id: UUID().uuidString, .init(id: UUID().uuidString,
username: "Username", username: "Username",

View file

@ -31,7 +31,7 @@ public struct Relationship: Codable {
notifying: false) notifying: false)
} }
} }
public extension Relationship { public extension Relationship {
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self) let values = try decoder.container(keyedBy: CodingKeys.self)

View file

@ -1,7 +1,7 @@
import Foundation import Foundation
extension String { public extension String {
public func escape() -> String { func escape() -> String {
return replacingOccurrences(of: "&", with: "&") return replacingOccurrences(of: "&", with: "&")
.replacingOccurrences(of: "&lt;", with: "<") .replacingOccurrences(of: "&lt;", with: "<")
.replacingOccurrences(of: "&gt;", with: ">") .replacingOccurrences(of: "&gt;", with: ">")
@ -10,7 +10,7 @@ extension String {
.replacingOccurrences(of: "&#39;", with: "") .replacingOccurrences(of: "&#39;", with: "")
} }
public func URLSafeBase64ToBase64() -> String { func URLSafeBase64ToBase64() -> String {
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
let countMod4 = count % 4 let countMod4 = count % 4

View file

@ -5,32 +5,32 @@
// Created by Jérôme Danthinne on 31/01/2023. // Created by Jérôme Danthinne on 31/01/2023.
// //
import Models
import Foundation import Foundation
import Models
extension Array where Element == Models.Notification { extension Array where Element == Models.Notification {
func consolidated(selectedType: Models.Notification.NotificationType?) async -> [ConsolidatedNotification] { func consolidated(selectedType: Models.Notification.NotificationType?) async -> [ConsolidatedNotification] {
await withCheckedContinuation({ result in await withCheckedContinuation { result in
DispatchQueue.global().async { DispatchQueue.global().async {
let notifications: [ConsolidatedNotification] = let notifications: [ConsolidatedNotification] =
Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) } Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) }
.values .values
.compactMap { notifications in .compactMap { notifications in
guard let notification = notifications.first, guard let notification = notifications.first,
let supportedType = notification.supportedType let supportedType = notification.supportedType
else { return nil } else { return nil }
return ConsolidatedNotification(notifications: notifications, return ConsolidatedNotification(notifications: notifications,
type: supportedType, type: supportedType,
createdAt: notification.createdAt, createdAt: notification.createdAt,
accounts: notifications.map(\.account), accounts: notifications.map(\.account),
status: notification.status) status: notification.status)
} }
.sorted { .sorted {
$0.createdAt.asDate > $1.createdAt.asDate $0.createdAt.asDate > $1.createdAt.asDate
} }
result.resume(returning: notifications) result.resume(returning: notifications)
} }
}) }
} }
} }

View file

@ -2,9 +2,9 @@ import DesignSystem
import EmojiText import EmojiText
import Env import Env
import Models import Models
import Network
import Status import Status
import SwiftUI import SwiftUI
import Network
struct NotificationRowView: View { struct NotificationRowView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@ -28,7 +28,8 @@ struct NotificationRowView: View {
makeMainLabel(type: notification.type) makeMainLabel(type: notification.type)
makeContent(type: notification.type) makeContent(type: notification.type)
if notification.type == .follow_request, if notification.type == .follow_request,
followRequests.map(\.id).contains(notification.accounts[0].id) { followRequests.map(\.id).contains(notification.accounts[0].id)
{
FollowRequestButtons(account: notification.accounts[0]) FollowRequestButtons(account: notification.accounts[0])
} }
} }

View file

@ -23,7 +23,7 @@ extension Models.Notification.NotificationType {
return "notifications.label.update" return "notifications.label.update"
} }
} }
public func notificationKey() -> String { public func notificationKey() -> String {
switch self { switch self {
case .status: case .status:

View file

@ -93,13 +93,14 @@ public struct StatusDetailView: View {
.navigationTitle(viewModel.title) .navigationTitle(viewModel.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func makeStatusesListView(statuses: [Status], date: Date) -> some View { private func makeStatusesListView(statuses: [Status], date _: Date) -> some View {
ForEach(statuses) { status in ForEach(statuses) { status in
var isReplyToPrevious: Bool = false var isReplyToPrevious: Bool = false
if let index = statuses.firstIndex(where: { $0.id == status.id }), if let index = statuses.firstIndex(where: { $0.id == status.id }),
index > 0, index > 0,
statuses[index - 1].id == status.inReplyToId { statuses[index - 1].id == status.inReplyToId
{
isReplyToPrevious = true isReplyToPrevious = true
} }
let viewModel: StatusRowViewModel = .init(status: status, let viewModel: StatusRowViewModel = .init(status: status,

View file

@ -5,7 +5,7 @@ import Models
import NukeUI import NukeUI
import SwiftUI import SwiftUI
struct StatusEditorMediaView: View { struct StatusEditorMediaView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusEditorViewModel @ObservedObject var viewModel: StatusEditorViewModel

View file

@ -21,7 +21,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
case gif = "public.gif" case gif = "public.gif"
case gif2 = "com.compuserve.gif" case gif2 = "com.compuserve.gif"
case quickTimeMovie = "com.apple.quicktime-movie" case quickTimeMovie = "com.apple.quicktime-movie"
case uiimage = "com.apple.uikit.image" case uiimage = "com.apple.uikit.image"
static func types() -> [UTType] { static func types() -> [UTType] {
@ -57,8 +57,8 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
if let image = result as? UIImage { if let image = result as? UIImage {
return image return image
} else if let imageURL = result as? URL, } else if let imageURL = result as? URL,
let data = try? Data(contentsOf: imageURL), let data = try? Data(contentsOf: imageURL),
let image = UIImage(data: data) let image = UIImage(data: data)
{ {
return image return image
} else if let data = result as? Data, } else if let data = result as? Data,

View file

@ -7,9 +7,9 @@ import Models
import Network import Network
import NukeUI import NukeUI
import PhotosUI import PhotosUI
import StoreKit
import SwiftUI import SwiftUI
import UIKit import UIKit
import StoreKit
public struct StatusEditorView: View { public struct StatusEditorView: View {
@EnvironmentObject private var appAccounts: AppAccountsManager @EnvironmentObject private var appAccounts: AppAccountsManager
@ -204,8 +204,8 @@ public struct StatusEditorView: View {
object: nil) object: nil)
if !viewModel.mode.isInShareExtension && !preferences.requestedReview { if !viewModel.mode.isInShareExtension && !preferences.requestedReview {
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene) SKStoreReviewController.requestReview(in: scene)
} }
preferences.requestedReview = true preferences.requestedReview = true
} }
} }

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Models
import SwiftUI
import Env import Env
import Models
import Network import Network
import SwiftUI
@MainActor @MainActor
public struct StatusEmbeddedView: View { public struct StatusEmbeddedView: View {
@ -27,7 +27,7 @@ public struct StatusEmbeddedView: View {
client: client, client: client,
routerPath: routerPath, routerPath: routerPath,
showActions: false)) showActions: false))
.environment(\.isCompact, true) .environment(\.isCompact, true)
} }
Spacer() Spacer()
} }

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network
import Shimmer import Shimmer
import SwiftUI import SwiftUI
import Network
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher { public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme

View file

@ -11,7 +11,7 @@ public struct StatusRowView: View {
@Environment(\.isCompact) private var isCompact: Bool @Environment(\.isCompact) private var isCompact: Bool
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@StateObject var viewModel: StatusRowViewModel @StateObject var viewModel: StatusRowViewModel
public init(viewModel: StatusRowViewModel) { public init(viewModel: StatusRowViewModel) {
@ -41,7 +41,8 @@ public struct StatusRowView: View {
} }
HStack(alignment: .top, spacing: .statusColumnsSpacing) { HStack(alignment: .top, spacing: .statusColumnsSpacing) {
if !isCompact, if !isCompact,
theme.avatarPosition == .leading { theme.avatarPosition == .leading
{
Button { Button {
viewModel.routerPath.navigate(to: .accountDetailWithAccount(account: status.account)) viewModel.routerPath.navigate(to: .accountDetailWithAccount(account: status.account))
} label: { } label: {
@ -180,7 +181,7 @@ public struct StatusRowView: View {
} }
} }
} }
private var remoteContentLoadingView: some View { private var remoteContentLoadingView: some View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
VStack { VStack {

View file

@ -12,7 +12,7 @@ public class StatusRowViewModel: ObservableObject {
let isFocused: Bool let isFocused: Bool
let isRemote: Bool let isRemote: Bool
let showActions: Bool let showActions: Bool
@Published var favoritesCount: Int @Published var favoritesCount: Int
@Published var isFavorited: Bool @Published var isFavorited: Bool
@Published var isReblogged: Bool @Published var isReblogged: Bool
@ -31,7 +31,7 @@ public class StatusRowViewModel: ObservableObject {
@Published var favoriters: [Account] = [] @Published var favoriters: [Account] = []
@Published var rebloggers: [Account] = [] @Published var rebloggers: [Account] = []
@Published var isLoadingRemoteContent: Bool = false @Published var isLoadingRemoteContent: Bool = false
@Published var localStatusId: String? @Published var localStatusId: String?
@Published var localStatus: Status? @Published var localStatus: Status?
@ -334,7 +334,7 @@ public class StatusRowViewModel: ObservableObject {
} }
} }
} }
func fetchRemoteStatus() async -> Bool { func fetchRemoteStatus() async -> Bool {
guard isRemote, let remoteStatusURL = URL(string: status.reblog?.url ?? status.url ?? "") else { return false } guard isRemote, let remoteStatusURL = URL(string: status.reblog?.url ?? status.url ?? "") else { return false }
isLoadingRemoteContent = true isLoadingRemoteContent = true
@ -344,8 +344,8 @@ public class StatusRowViewModel: ObservableObject {
following: nil), following: nil),
forceVersion: .v2) forceVersion: .v2)
if let status = results?.statuses.first { if let status = results?.statuses.first {
self.localStatusId = status.id localStatusId = status.id
self.localStatus = status localStatus = status
isLoadingRemoteContent = false isLoadingRemoteContent = false
return true return true
} else { } else {

View file

@ -63,7 +63,8 @@ struct StatusRowActionsView: View {
ForEach(Actions.allCases, id: \.self) { action in ForEach(Actions.allCases, id: \.self) { action in
if action == .share { if action == .share {
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url, if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
let url = URL(string: urlString) { let url = URL(string: urlString)
{
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)) {

View file

@ -1,17 +1,17 @@
import SwiftUI
import DesignSystem import DesignSystem
import Models
import Env import Env
import Models
import SwiftUI
struct StatusRowContentView: View { struct StatusRowContentView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact @Environment(\.isCompact) private var isCompact
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
let status: AnyStatus let status: AnyStatus
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
var body: some View { var body: some View {
if !status.spoilerText.asRawText.isEmpty { if !status.spoilerText.asRawText.isEmpty {
StatusRowSpoilerView(status: status, displaySpoiler: $viewModel.displaySpoiler) StatusRowSpoilerView(status: status, displaySpoiler: $viewModel.displaySpoiler)
@ -26,7 +26,8 @@ struct StatusRowContentView: View {
if !reasons.contains(.placeholder), if !reasons.contains(.placeholder),
!isCompact, !isCompact,
(viewModel.isEmbedLoading || viewModel.embeddedStatus != nil) { viewModel.isEmbedLoading || viewModel.embeddedStatus != nil
{
StatusEmbeddedView(status: viewModel.embeddedStatus ?? Status.placeholder(), StatusEmbeddedView(status: viewModel.embeddedStatus ?? Status.placeholder(),
client: viewModel.client, client: viewModel.client,
routerPath: viewModel.routerPath) routerPath: viewModel.routerPath)
@ -35,7 +36,7 @@ struct StatusRowContentView: View {
.shimmering(active: viewModel.isEmbedLoading) .shimmering(active: viewModel.isEmbedLoading)
.transition(.opacity) .transition(.opacity)
} }
if !status.mediaAttachments.isEmpty { if !status.mediaAttachments.isEmpty {
HStack { HStack {
StatusRowMediaPreviewView(attachments: status.mediaAttachments, StatusRowMediaPreviewView(attachments: status.mediaAttachments,
@ -47,7 +48,7 @@ struct StatusRowContentView: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
if let card = status.card, if let card = status.card,
!viewModel.isEmbedLoading, !viewModel.isEmbedLoading,
!isCompact, !isCompact,

View file

@ -57,7 +57,8 @@ struct StatusRowContextMenu: View {
Divider() Divider()
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url, if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
let url = URL(string: urlString) { let url = URL(string: urlString)
{
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)) {

View file

@ -1,13 +1,13 @@
import SwiftUI
import DesignSystem import DesignSystem
import Models import Models
import SwiftUI
struct StatusRowHeaderView: View { struct StatusRowHeaderView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
let status: AnyStatus let status: AnyStatus
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
var body: some View { var body: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
Button { Button {
@ -23,7 +23,7 @@ struct StatusRowHeaderView: View {
.accessibilityElement() .accessibilityElement()
.accessibilityLabel(Text("\(status.account.displayName)")) .accessibilityLabel(Text("\(status.account.displayName)"))
} }
@ViewBuilder @ViewBuilder
private func accountView(status: AnyStatus) -> some View { private func accountView(status: AnyStatus) -> some View {
HStack(alignment: .center) { HStack(alignment: .center) {
@ -46,7 +46,7 @@ struct StatusRowHeaderView: View {
} }
} }
} }
@ViewBuilder @ViewBuilder
private var threadIcon: some View { private var threadIcon: some View {
if viewModel.status.reblog?.inReplyToAccountId != nil || viewModel.status.inReplyToAccountId != nil { if viewModel.status.reblog?.inReplyToAccountId != nil || viewModel.status.inReplyToAccountId != nil {
@ -57,7 +57,7 @@ struct StatusRowHeaderView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
private var contextMenuButton: some View { private var contextMenuButton: some View {
Menu { Menu {
StatusRowContextMenu(viewModel: viewModel) StatusRowContextMenu(viewModel: viewModel)

View file

@ -1,9 +1,9 @@
import SwiftUI
import DesignSystem import DesignSystem
import SwiftUI
struct StatusRowReblogView: View { struct StatusRowReblogView: View {
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
var body: some View { var body: some View {
if viewModel.status.reblog != nil { if viewModel.status.reblog != nil {
HStack(spacing: 2) { HStack(spacing: 2) {
@ -27,4 +27,3 @@ struct StatusRowReblogView: View {
} }
} }
} }

View file

@ -1,9 +1,9 @@
import SwiftUI
import DesignSystem import DesignSystem
import SwiftUI
struct StatusRowReplyView: View { struct StatusRowReplyView: View {
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
var body: some View { var body: some View {
if let accountId = viewModel.status.inReplyToAccountId, if let accountId = viewModel.status.inReplyToAccountId,
let mention = viewModel.status.mentions.first(where: { $0.id == accountId }) let mention = viewModel.status.mentions.first(where: { $0.id == accountId })

View file

@ -1,11 +1,11 @@
import SwiftUI
import DesignSystem import DesignSystem
import Models import Models
import SwiftUI
struct StatusRowSpoilerView: View { struct StatusRowSpoilerView: View {
let status: AnyStatus let status: AnyStatus
@Binding var displaySpoiler: Bool @Binding var displaySpoiler: Bool
var body: some View { var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
Text("⚠︎") Text("⚠︎")

View file

@ -1,19 +1,19 @@
import SwiftUI import DesignSystem
import Env import Env
import Models import Models
import DesignSystem import SwiftUI
struct StatusRowSwipeView: View { struct StatusRowSwipeView: View {
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
enum Mode { enum Mode {
case leading, trailing case leading, trailing
} }
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
let mode: Mode let mode: Mode
var body: some View { var body: some View {
switch mode { switch mode {
case .leading: case .leading:
@ -22,7 +22,7 @@ struct StatusRowSwipeView: View {
trailingSwipeActions trailingSwipeActions
} }
} }
@ViewBuilder @ViewBuilder
private var trailingSwipeActions: some View { private var trailingSwipeActions: some View {
if preferences.swipeActionsStatusTrailingRight != StatusAction.none, !viewModel.isRemote { if preferences.swipeActionsStatusTrailingRight != StatusAction.none, !viewModel.isRemote {
@ -105,10 +105,10 @@ struct StatusRowSwipeView: View {
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle) makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
} }
} }
@ViewBuilder @ViewBuilder
private func makeSwipeLabel(action: StatusAction, style: UserPreferences.SwipeActionsIconStyle) -> some View { private func makeSwipeLabel(action: StatusAction, style: UserPreferences.SwipeActionsIconStyle) -> some View {
switch (style) { switch style {
case .iconOnly: case .iconOnly:
Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked)) Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked))
.labelStyle(.iconOnly) .labelStyle(.iconOnly)

View file

@ -1,11 +1,11 @@
import SwiftUI
import DesignSystem import DesignSystem
import Models import Models
import SwiftUI
struct StatusRowTextView: View { struct StatusRowTextView: View {
let status: AnyStatus let status: AnyStatus
let viewModel: StatusRowViewModel let viewModel: StatusRowViewModel
var body: some View { var body: some View {
HStack { HStack {
EmojiTextApp(status.content, emojis: status.emojis, language: status.language) EmojiTextApp(status.content, emojis: status.emojis, language: status.language)

View file

@ -1,14 +1,14 @@
import SwiftUI
import Models
import DesignSystem import DesignSystem
import Env import Env
import Models
import SwiftUI
struct StatusRowTranslateView: View { struct StatusRowTranslateView: View {
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
let status: AnyStatus let status: AnyStatus
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
private var shouldShowTranslateButton: Bool { private var shouldShowTranslateButton: Bool {
let statusLang = viewModel.getStatusLang() let statusLang = viewModel.getStatusLang()
@ -22,10 +22,10 @@ struct StatusRowTranslateView: View {
return false return false
} }
} }
var body: some View { var body: some View {
if let userLang = preferences.serverPreferences?.postLanguage, if let userLang = preferences.serverPreferences?.postLanguage,
shouldShowTranslateButton shouldShowTranslateButton
{ {
Button { Button {
Task { Task {

View file

@ -248,7 +248,7 @@ extension TimelineViewModel: StatusesFetcher {
canStreamEvents = true canStreamEvents = true
return return
} }
// As this is a long runnign task we need to ensure that the user didn't changed the timeline filter. // As this is a long runnign task we need to ensure that the user didn't changed the timeline filter.
guard initialTimeline == timeline else { guard initialTimeline == timeline else {
canStreamEvents = true canStreamEvents = true