Merge pull request #93 from VernissageApp/feature/swift-core

Feature/swift core
This commit is contained in:
Marcin Czachurski 2023-10-20 17:50:53 +02:00 committed by GitHub
commit fffb477cb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
132 changed files with 1491 additions and 2862 deletions

View File

@ -7,9 +7,7 @@ let package = Package(
name: "ClientKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class Client: ObservableObject {
@Observable public class Client {
public static let shared = Client()
private init() { }

View File

@ -6,7 +6,7 @@
import Foundation
public class AccountModel: ObservableObject, Identifiable {
@Observable public class AccountModel: Identifiable {
public let id: String
public let accessToken: String?
public let refreshToken: String?
@ -28,7 +28,7 @@ public class AccountModel: ObservableObject, Identifiable {
public let username: String
public let lastSeenStatusId: String?
@Published public var avatarData: Data?
public var avatarData: Data?
public init(id: String,
accessToken: String?,

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class AttachmentModel: ObservableObject, Identifiable {
@Observable public class AttachmentModel: Identifiable {
public let id: String
public let type: MediaAttachment.MediaAttachmentType
public let url: URL
@ -21,11 +21,11 @@ public class AttachmentModel: ObservableObject, Identifiable {
public let metaImageWidth: Int32?
public let metaImageHeight: Int32?
@Published public var exifCamera: String?
@Published public var exifCreatedDate: String?
@Published public var exifExposure: String?
@Published public var exifLens: String?
@Published public var data: Data?
public var exifCamera: String?
public var exifCreatedDate: String?
public var exifExposure: String?
public var exifLens: String?
public var data: Data?
public init(id: String,
type: MediaAttachment.MediaAttachmentType,

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class StatusModel: ObservableObject {
@Observable public class StatusModel {
public let id: EntityId
public let rebloggedStatusId: EntityId?
public let content: Html
@ -38,8 +38,8 @@ public class StatusModel: ObservableObject {
public let reblogStatus: Status?
@Published public var favourited: Bool
@Published public var mediaAttachments: [AttachmentModel]
public var favourited: Bool
public var mediaAttachments: [AttachmentModel]
public init(status: Status) {
self.id = status.id

View File

@ -1,35 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import ClientKit
extension AccountData {
func toAccountModel() -> AccountModel {
let accountModel = AccountModel(id: self.id,
accessToken: self.accessToken,
refreshToken: self.refreshToken,
acct: self.acct,
avatar: self.avatar,
clientId: self.clientId,
clientSecret: self.clientSecret,
clientVapidKey: self.clientVapidKey,
createdAt: self.createdAt,
displayName: self.displayName,
followersCount: self.followersCount,
followingCount: self.followingCount,
header: self.header,
locked: self.locked,
note: self.note,
serverUrl: self.serverUrl,
statusesCount: self.statusesCount,
url: self.url,
username: self.username,
lastSeenStatusId: self.lastSeenStatusId,
avatarData: self.avatarData)
return accountModel
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AccountData)
public class AccountData: NSManagedObject {
}

View File

@ -1,84 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AccountData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountData> {
return NSFetchRequest<AccountData>(entityName: "AccountData")
}
@NSManaged public var accessToken: String?
@NSManaged public var refreshToken: String?
@NSManaged public var acct: String
@NSManaged public var avatar: URL?
@NSManaged public var avatarData: Data?
@NSManaged public var clientId: String
@NSManaged public var clientSecret: String
@NSManaged public var clientVapidKey: String
@NSManaged public var createdAt: String
@NSManaged public var displayName: String?
@NSManaged public var followersCount: Int32
@NSManaged public var followingCount: Int32
@NSManaged public var header: URL?
@NSManaged public var id: String
@NSManaged public var locked: Bool
@NSManaged public var note: String?
@NSManaged public var serverUrl: URL
@NSManaged public var statusesCount: Int32
@NSManaged public var url: URL?
@NSManaged public var username: String
@NSManaged public var statuses: Set<StatusData>?
@NSManaged public var viewedStatuses: Set<ViewedStatus>?
@NSManaged public var accountRelationships: Set<AccountRelationship>?
@NSManaged public var lastSeenStatusId: String?
}
// MARK: Generated accessors for statuses
extension AccountData {
@objc(addStatusesObject:)
@NSManaged public func addToStatuses(_ value: StatusData)
@objc(removeStatusesObject:)
@NSManaged public func removeFromStatuses(_ value: StatusData)
@objc(addStatuses:)
@NSManaged public func addToStatuses(_ values: NSSet)
@objc(removeStatuses:)
@NSManaged public func removeFromStatuses(_ values: NSSet)
@objc(addViewedStatusesObject:)
@NSManaged public func addToViewedStatuses(_ value: ViewedStatus)
@objc(removeViewedStatusesObject:)
@NSManaged public func removeFromViewedStatuses(_ value: ViewedStatus)
@objc(addViewedStatuses:)
@NSManaged public func addToViewedStatuses(_ values: NSSet)
@objc(removeViewedStatuses:)
@NSManaged public func removeFromViewedStatuses(_ values: NSSet)
@objc(addAccountRelationshipsObject:)
@NSManaged public func addToAccountRelationships(_ value: AccountRelationship)
@objc(removeAccountRelationshipsObject:)
@NSManaged public func removeFromVAccountRelationships(_ value: AccountRelationship)
@objc(addAccountRelationships:)
@NSManaged public func addToAccountRelationships(_ values: NSSet)
@objc(removeAccountRelationships:)
@NSManaged public func removeFromAccountRelationships(_ values: NSSet)
}
extension AccountData: Identifiable {
}

122
CoreData/AccountData.swift Normal file
View File

@ -0,0 +1,122 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import ClientKit
@Model final public class AccountData {
@Attribute(.unique) public var id: String
public var accessToken: String?
public var refreshToken: String?
public var acct: String
public var avatar: URL?
public var avatarData: Data?
public var clientId: String
public var clientSecret: String
public var clientVapidKey: String
public var createdAt: String
public var displayName: String?
public var followersCount: Int32
public var followingCount: Int32
public var header: URL?
public var locked: Bool
public var note: String?
public var serverUrl: URL
public var statusesCount: Int32
public var url: URL?
public var username: String
/// Last status seen on home timeline by the user.
public var lastSeenStatusId: String?
/// Last status loaded on home timeline.
public var lastLoadedStatusId: String?
@Relationship(deleteRule: .cascade, inverse: \ViewedStatus.pixelfedAccount) public var viewedStatuses: [ViewedStatus]
@Relationship(deleteRule: .cascade, inverse: \AccountRelationship.pixelfedAccount) public var accountRelationships: [AccountRelationship]
init(
accessToken: String? = nil,
refreshToken: String? = nil,
acct: String = "",
avatar: URL? = nil,
avatarData: Data? = nil,
clientId: String = "",
clientSecret: String = "",
clientVapidKey: String = "",
createdAt: String = "",
displayName: String? = nil,
followersCount: Int32 = .zero,
followingCount: Int32 = .zero,
header: URL? = nil,
id: String = "",
locked: Bool = false,
note: String? = nil,
serverUrl: URL,
statusesCount: Int32 = .zero,
url: URL? = nil,
username: String = "",
viewedStatuses: [ViewedStatus] = [],
accountRelationships: [AccountRelationship] = [],
lastSeenStatusId: String? = nil
) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.acct = acct
self.avatar = avatar
self.avatarData = avatarData
self.clientId = clientId
self.clientSecret = clientSecret
self.clientVapidKey = clientVapidKey
self.createdAt = createdAt
self.displayName = displayName
self.followersCount = followersCount
self.followingCount = followingCount
self.header = header
self.id = id
self.locked = locked
self.note = note
self.serverUrl = serverUrl
self.statusesCount = statusesCount
self.url = url
self.username = username
self.viewedStatuses = viewedStatuses
self.accountRelationships = accountRelationships
self.lastSeenStatusId = lastSeenStatusId
}
}
extension AccountData: Identifiable {
}
extension AccountData {
func toAccountModel() -> AccountModel {
let accountModel = AccountModel(id: self.id,
accessToken: self.accessToken,
refreshToken: self.refreshToken,
acct: self.acct,
avatar: self.avatar,
clientId: self.clientId,
clientSecret: self.clientSecret,
clientVapidKey: self.clientVapidKey,
createdAt: self.createdAt,
displayName: self.displayName,
followersCount: self.followersCount,
followingCount: self.followingCount,
header: self.header,
locked: self.locked,
note: self.note,
serverUrl: self.serverUrl,
statusesCount: self.statusesCount,
url: self.url,
username: self.username,
lastSeenStatusId: self.lastSeenStatusId,
avatarData: self.avatarData)
return accountModel
}
}

View File

@ -5,26 +5,27 @@
//
import Foundation
import CoreData
import SwiftData
class AccountDataHandler {
public static let shared = AccountDataHandler()
private init() { }
func getAccountsData(viewContext: NSManagedObjectContext? = nil) -> [AccountData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AccountData.fetchRequest()
func getAccountsData(modelContext: ModelContext) -> [AccountData] {
do {
return try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<AccountData>()
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Accounts cannot be retrieved (getAccountsData).")
return []
}
}
func getCurrentAccountData(viewContext: NSManagedObjectContext? = nil) -> AccountData? {
let accounts = self.getAccountsData(viewContext: viewContext)
let defaultSettings = ApplicationSettingsHandler.shared.get()
func getCurrentAccountData(modelContext: ModelContext) -> AccountData? {
let accounts = self.getAccountsData(modelContext: modelContext)
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
let currentAccount = accounts.first { accountData in
accountData.id == defaultSettings.currentAccount
@ -37,34 +38,43 @@ class AccountDataHandler {
return accounts.first
}
func getAccountData(accountId: String, viewContext: NSManagedObjectContext? = nil) -> AccountData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AccountData.fetchRequest()
fetchRequest.fetchLimit = 1
fetchRequest.predicate = NSPredicate(format: "id = %@", accountId)
func getAccountData(accountId: String, modelContext: ModelContext) -> AccountData? {
do {
return try context.fetch(fetchRequest).first
var fetchDescriptor = FetchDescriptor<AccountData>(
predicate: #Predicate { $0.id == accountId}
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getAccountData).")
return nil
}
}
func remove(accountData: AccountData) {
let context = CoreDataHandler.shared.container.viewContext
context.delete(accountData)
func remove(accountData: AccountData, modelContext: ModelContext) {
do {
try context.save()
modelContext.delete(accountData)
try modelContext.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during deleting account data (remove).")
}
}
func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, accountId: String, modelContext: ModelContext) throws {
guard let accountDataFromDb = self.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
if (accountDataFromDb.lastSeenStatusId ?? "0") < (lastSeenStatusId ?? "0") {
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
}
func createAccountDataEntity(viewContext: NSManagedObjectContext? = nil) -> AccountData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AccountData(context: context)
if (accountDataFromDb.lastLoadedStatusId ?? "0") < (lastLoadedStatusId ?? "0") {
accountDataFromDb.lastLoadedStatusId = lastLoadedStatusId
}
try modelContext.save()
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AccountRelationship)
public class AccountRelationship: NSManagedObject {
}

View File

@ -1,22 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AccountRelationship {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountRelationship> {
return NSFetchRequest<AccountRelationship>(entityName: "AccountRelationship")
}
@NSManaged public var accountId: String
@NSManaged public var boostedStatusesMuted: Bool
@NSManaged public var pixelfedAccount: AccountData
}
extension AccountRelationship: Identifiable {
}

View File

@ -0,0 +1,23 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
@Model final public class AccountRelationship {
@Attribute(.unique) public var accountId: String
public var boostedStatusesMuted: Bool
public var pixelfedAccount: AccountData?
init(accountId: String, boostedStatusesMuted: Bool, pixelfedAccount: AccountData? = nil) {
self.accountId = accountId
self.boostedStatusesMuted = boostedStatusesMuted
self.pixelfedAccount = pixelfedAccount
}
}
extension AccountRelationship: Identifiable {
}

View File

@ -5,65 +5,60 @@
//
import Foundation
import CoreData
import PixelfedKit
import SwiftData
class AccountRelationshipHandler {
public static let shared = AccountRelationshipHandler()
private init() { }
func createAccountRelationshipEntity(viewContext: NSManagedObjectContext? = nil) -> AccountRelationship {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AccountRelationship(context: context)
}
/// Check if boosted statuses from given account are muted.
func isBoostedStatusesMuted(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool {
func isBoostedStatusesMuted(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
if status.reblog == nil {
return false
}
let accountRelationship = self.getAccountRelationship(for: accountId, relation: status.account.id, viewContext: viewContext)
let accountRelationship = self.getAccountRelationship(for: accountId, relation: status.account.id, modelContext: modelContext)
return accountRelationship?.boostedStatusesMuted ?? false
}
func isBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, viewContext: NSManagedObjectContext? = nil) -> Bool {
let accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, viewContext: viewContext)
func isBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, modelContext: ModelContext) -> Bool {
let accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, modelContext: modelContext)
return accountRelationship?.boostedStatusesMuted ?? false
}
func setBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, boostedStatusesMuted: Bool, viewContext: NSManagedObjectContext? = nil) {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
var accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, viewContext: context)
func setBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, boostedStatusesMuted: Bool, modelContext: ModelContext) {
var accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, modelContext: modelContext)
if accountRelationship == nil {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, viewContext: context) else {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
let newAccountRelationship = AccountRelationshipHandler.shared.createAccountRelationshipEntity(viewContext: context)
newAccountRelationship.accountId = relationAccountId
newAccountRelationship.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToAccountRelationships(newAccountRelationship)
let newAccountRelationship = AccountRelationship(accountId: relationAccountId, boostedStatusesMuted: false, pixelfedAccount: accountDataFromDb)
modelContext.insert(newAccountRelationship)
accountDataFromDb.accountRelationships.append(newAccountRelationship)
accountRelationship = newAccountRelationship
}
accountRelationship?.boostedStatusesMuted = boostedStatusesMuted
CoreDataHandler.shared.save(viewContext: context)
do {
try modelContext.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during saving boosted muted statuses.")
}
}
private func getAccountRelationship(for accountId: String, relation relationAccountId: String, viewContext: NSManagedObjectContext? = nil) -> AccountRelationship? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AccountRelationship.fetchRequest()
fetchRequest.fetchLimit = 1
let statusAccountIddPredicate = NSPredicate(format: "accountId = %@", relationAccountId)
let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [statusAccountIddPredicate, accountPredicate])
private func getAccountRelationship(for accountId: String, relation relationAccountId: String, modelContext: ModelContext) -> AccountRelationship? {
do {
return try context.fetch(fetchRequest).first
var fetchDescriptor = FetchDescriptor<AccountRelationship>(
predicate: #Predicate { $0.accountId == relationAccountId && $0.pixelfedAccount?.id == accountId }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching account relationship (isBoostedMutedForAccount).")
return nil

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(ApplicationSettings)
public class ApplicationSettings: NSManagedObject {
}

View File

@ -1,46 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension ApplicationSettings {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ApplicationSettings> {
return NSFetchRequest<ApplicationSettings>(entityName: "ApplicationSettings")
}
@NSManaged public var currentAccount: String?
@NSManaged public var theme: Int32
@NSManaged public var tintColor: Int32
@NSManaged public var avatarShape: Int32
@NSManaged public var activeIcon: String
@NSManaged public var lastRefreshTokens: Date
@NSManaged public var hapticTabSelectionEnabled: Bool
@NSManaged public var hapticRefreshEnabled: Bool
@NSManaged public var hapticButtonPressEnabled: Bool
@NSManaged public var hapticAnimationEnabled: Bool
@NSManaged public var hapticNotificationEnabled: Bool
@NSManaged public var showSensitive: Bool
@NSManaged public var showPhotoDescription: Bool
@NSManaged public var menuPosition: Int32
@NSManaged public var showAvatarsOnTimeline: Bool
@NSManaged public var showFavouritesOnTimeline: Bool
@NSManaged public var showAltIconOnTimeline: Bool
@NSManaged public var warnAboutMissingAlt: Bool
@NSManaged public var showGridOnUserProfile: Bool
@NSManaged public var showReboostedStatuses: Bool
@NSManaged public var hideStatusesWithoutAlt: Bool
@NSManaged public var customNavigationMenuItem1: Int32
@NSManaged public var customNavigationMenuItem2: Int32
@NSManaged public var customNavigationMenuItem3: Int32
}
extension ApplicationSettings: Identifiable {
}

View File

@ -0,0 +1,94 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import EnvironmentKit
@Model final public class ApplicationSettings {
public var currentAccount: String?
public var theme: Int32
public var tintColor: Int32
public var avatarShape: Int32
public var activeIcon: String
public var lastRefreshTokens: Date
public var hapticTabSelectionEnabled: Bool
public var hapticRefreshEnabled: Bool
public var hapticButtonPressEnabled: Bool
public var hapticAnimationEnabled: Bool
public var hapticNotificationEnabled: Bool
public var showSensitive: Bool
public var showPhotoDescription: Bool
public var menuPosition: Int32
public var showAvatarsOnTimeline: Bool
public var showFavouritesOnTimeline: Bool
public var showAltIconOnTimeline: Bool
public var warnAboutMissingAlt: Bool
public var showGridOnUserProfile: Bool
public var showReboostedStatuses: Bool
public var hideStatusesWithoutAlt: Bool
public var customNavigationMenuItem1: Int32
public var customNavigationMenuItem2: Int32
public var customNavigationMenuItem3: Int32
init(
currentAccount: String? = nil,
theme: Int32 = Int32(Theme.system.rawValue),
tintColor: Int32 = Int32(TintColor.accentColor2.rawValue),
avatarShape: Int32 = Int32(AvatarShape.circle.rawValue),
activeIcon: String = "Default",
lastRefreshTokens: Date = Date.distantPast,
hapticTabSelectionEnabled: Bool = true,
hapticRefreshEnabled: Bool = true,
hapticButtonPressEnabled: Bool = true,
hapticAnimationEnabled: Bool = true,
hapticNotificationEnabled: Bool = true,
showSensitive: Bool = false,
showPhotoDescription: Bool = false,
menuPosition: Int32 = Int32(MenuPosition.top.rawValue),
showAvatarsOnTimeline: Bool = false,
showFavouritesOnTimeline: Bool = false,
showAltIconOnTimeline: Bool = false,
warnAboutMissingAlt: Bool = true,
showGridOnUserProfile: Bool = false,
showReboostedStatuses: Bool = false,
hideStatusesWithoutAlt: Bool = false,
customNavigationMenuItem1: Int32 = 1,
customNavigationMenuItem2: Int32 = 2,
customNavigationMenuItem3: Int32 = 5
) {
self.currentAccount = currentAccount
self.theme = theme
self.tintColor = tintColor
self.avatarShape = avatarShape
self.activeIcon = activeIcon
self.lastRefreshTokens = lastRefreshTokens
self.hapticTabSelectionEnabled = hapticTabSelectionEnabled
self.hapticRefreshEnabled = hapticRefreshEnabled
self.hapticButtonPressEnabled = hapticButtonPressEnabled
self.hapticAnimationEnabled = hapticAnimationEnabled
self.hapticNotificationEnabled = hapticNotificationEnabled
self.showSensitive = showSensitive
self.showPhotoDescription = showPhotoDescription
self.menuPosition = menuPosition
self.showAvatarsOnTimeline = showAvatarsOnTimeline
self.showFavouritesOnTimeline = showFavouritesOnTimeline
self.showAltIconOnTimeline = showAltIconOnTimeline
self.warnAboutMissingAlt = warnAboutMissingAlt
self.showGridOnUserProfile = showGridOnUserProfile
self.showReboostedStatuses = showReboostedStatuses
self.hideStatusesWithoutAlt = hideStatusesWithoutAlt
self.customNavigationMenuItem1 = customNavigationMenuItem1
self.customNavigationMenuItem2 = customNavigationMenuItem2
self.customNavigationMenuItem3 = customNavigationMenuItem3
}
}
extension ApplicationSettings: Identifiable {
}

View File

@ -5,20 +5,21 @@
//
import Foundation
import CoreData
import EnvironmentKit
import SwiftData
class ApplicationSettingsHandler {
public static let shared = ApplicationSettingsHandler()
private init() { }
func get(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
func get(modelContext: ModelContext) -> ApplicationSettings {
var settingsList: [ApplicationSettings] = []
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = ApplicationSettings.fetchRequest()
do {
settingsList = try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<ApplicationSettings>()
fetchDescriptor.includePendingChanges = true
settingsList = try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching application settings.")
}
@ -26,18 +27,23 @@ class ApplicationSettingsHandler {
if let settings = settingsList.first {
return settings
} else {
let settings = self.createApplicationSettingsEntity(viewContext: context)
settings.avatarShape = Int32(AvatarShape.circle.rawValue)
settings.theme = Int32(Theme.system.rawValue)
settings.tintColor = Int32(TintColor.accentColor2.rawValue)
CoreDataHandler.shared.save(viewContext: context)
do {
let settings = ApplicationSettings()
modelContext.insert(settings)
return settings
try modelContext.save()
return settings
} catch {
CoreDataError.shared.handle(error, message: "Error during saving new application settings.")
let settings = ApplicationSettings()
return settings
}
}
}
func update(applicationState: ApplicationState) {
let defaultSettings = ApplicationSettingsHandler.shared.get()
func update(applicationState: ApplicationState, modelContext: ModelContext) {
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
if let tintColor = TintColor(rawValue: Int(defaultSettings.tintColor)) {
applicationState.tintColor = tintColor
@ -73,146 +79,141 @@ class ApplicationSettingsHandler {
applicationState.hapticNotificationEnabled = defaultSettings.hapticNotificationEnabled
}
func set(accountId: String?) {
let defaultSettings = self.get()
func set(accountId: String?, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.currentAccount = accountId
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(tintColor: TintColor) {
let defaultSettings = self.get()
func set(tintColor: TintColor, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.tintColor = Int32(tintColor.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(theme: Theme) {
let defaultSettings = self.get()
func set(theme: Theme, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.theme = Int32(theme.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(avatarShape: AvatarShape) {
let defaultSettings = self.get()
func set(avatarShape: AvatarShape, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.avatarShape = Int32(avatarShape.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticTabSelectionEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticTabSelectionEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticTabSelectionEnabled = hapticTabSelectionEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticRefreshEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticRefreshEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticRefreshEnabled = hapticRefreshEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticAnimationEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticAnimationEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticAnimationEnabled = hapticAnimationEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticNotificationEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticNotificationEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticNotificationEnabled = hapticNotificationEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticButtonPressEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticButtonPressEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticButtonPressEnabled = hapticButtonPressEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showSensitive: Bool) {
let defaultSettings = self.get()
func set(showSensitive: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showSensitive = showSensitive
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showPhotoDescription: Bool) {
let defaultSettings = self.get()
func set(showPhotoDescription: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showPhotoDescription = showPhotoDescription
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(activeIcon: String) {
let defaultSettings = self.get()
func set(activeIcon: String, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.activeIcon = activeIcon
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(menuPosition: MenuPosition) {
let defaultSettings = self.get()
func set(menuPosition: MenuPosition, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.menuPosition = Int32(menuPosition.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showAvatarsOnTimeline: Bool) {
let defaultSettings = self.get()
func set(showAvatarsOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showAvatarsOnTimeline = showAvatarsOnTimeline
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showFavouritesOnTimeline: Bool) {
let defaultSettings = self.get()
func set(showFavouritesOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showFavouritesOnTimeline = showFavouritesOnTimeline
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showAltIconOnTimeline: Bool) {
let defaultSettings = self.get()
func set(showAltIconOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showAltIconOnTimeline = showAltIconOnTimeline
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(warnAboutMissingAlt: Bool) {
let defaultSettings = self.get()
func set(warnAboutMissingAlt: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.warnAboutMissingAlt = warnAboutMissingAlt
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(customNavigationMenuItem1: Int) {
let defaultSettings = self.get()
func set(customNavigationMenuItem1: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem1 = Int32(customNavigationMenuItem1)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(customNavigationMenuItem2: Int) {
let defaultSettings = self.get()
func set(customNavigationMenuItem2: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem2 = Int32(customNavigationMenuItem2)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(customNavigationMenuItem3: Int) {
let defaultSettings = self.get()
func set(customNavigationMenuItem3: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem3 = Int32(customNavigationMenuItem3)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showGridOnUserProfile: Bool) {
let defaultSettings = self.get()
func set(showGridOnUserProfile: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showGridOnUserProfile = showGridOnUserProfile
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showReboostedStatuses: Bool) {
let defaultSettings = self.get()
func set(showReboostedStatuses: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showReboostedStatuses = showReboostedStatuses
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hideStatusesWithoutAlt: Bool) {
let defaultSettings = self.get()
func set(hideStatusesWithoutAlt: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hideStatusesWithoutAlt = hideStatusesWithoutAlt
CoreDataHandler.shared.save()
}
private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ApplicationSettings(context: context)
try? modelContext.save()
}
}

View File

@ -1,30 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension AttachmentData {
func copyFrom(_ attachment: MediaAttachment) {
self.id = attachment.id
self.url = attachment.url
self.blurhash = attachment.blurhash
self.previewUrl = attachment.previewUrl
self.remoteUrl = attachment.remoteUrl
self.text = attachment.description
self.type = attachment.type.rawValue
// We can set image width only when it wasn't previusly recalculated.
if let width = (attachment.meta as? ImageMetadata)?.original?.width, self.metaImageWidth <= 0 && width > 0 {
self.metaImageWidth = Int32(width)
}
// We can set image height only when it wasn't previusly recalculated.
if let height = (attachment.meta as? ImageMetadata)?.original?.height, self.metaImageHeight <= 0 && height > 0 {
self.metaImageHeight = Int32(height)
}
}
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension AttachmentData: Comparable {
public static func < (lhs: AttachmentData, rhs: AttachmentData) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AttachmentData)
public class AttachmentData: NSManagedObject {
}

View File

@ -1,36 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AttachmentData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AttachmentData> {
return NSFetchRequest<AttachmentData>(entityName: "AttachmentData")
}
@NSManaged public var blurhash: String?
@NSManaged public var data: Data?
@NSManaged public var exifCamera: String?
@NSManaged public var exifCreatedDate: String?
@NSManaged public var exifExposure: String?
@NSManaged public var exifLens: String?
@NSManaged public var id: String
@NSManaged public var previewUrl: URL?
@NSManaged public var remoteUrl: URL?
@NSManaged public var statusId: String
@NSManaged public var text: String?
@NSManaged public var type: String
@NSManaged public var url: URL
@NSManaged public var metaImageWidth: Int32
@NSManaged public var metaImageHeight: Int32
@NSManaged public var order: Int32
@NSManaged public var statusRelation: StatusData?
}
extension AttachmentData: Identifiable {
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension AttachmentData {
func isFaulty() -> Bool {
return self.isDeleted || self.isFault
}
}

View File

@ -1,24 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension [AttachmentData] {
func getHighestImage() -> AttachmentData? {
var attachment = self.first
var imgHeight = 0.0
for item in self {
let attachmentheight = Double(item.metaImageHeight)
if attachmentheight > imgHeight {
attachment = item
imgHeight = attachmentheight
}
}
return attachment
}
}

View File

@ -1,38 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
class AttachmentDataHandler {
public static let shared = AttachmentDataHandler()
private init() { }
func createAttachmnentDataEntity(viewContext: NSManagedObjectContext? = nil) -> AttachmentData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AttachmentData(context: context)
}
func getDownloadedAttachmentData(accountId: String, length: Int, viewContext: NSManagedObjectContext? = nil) -> [AttachmentData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AttachmentData.fetchRequest()
fetchRequest.fetchLimit = length
let sortDescriptor = NSSortDescriptor(key: "statusRelation.id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate1 = NSPredicate(format: "statusRelation.pixelfedAccount.id = %@", accountId)
let predicate2 = NSPredicate(format: "data != nil")
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1, predicate2])
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching attachment data (getDownloadedAttachmentData).")
return []
}
}
}

View File

@ -1,99 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import CoreData
import OSLog
import EnvironmentKit
public class CoreDataHandler {
public static let shared = CoreDataHandler()
lazy var container: NSPersistentContainer = {
let container = NSPersistentContainer(name: AppConstants.coreDataPersistantContainerName)
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.mczachurski.vernissage")!
.appendingPathComponent("Data.sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
container.loadPersistentStores(completionHandler: { [unowned container] (_, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
// You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
// Migrate old store do current (shared between app and widget)
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
// Migration process.
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
Logger.main.error("\(error.localizedDescription)")
}
// Delete old store.
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
Logger.main.error("\(error.localizedDescription)")
}
})
}
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
public func newBackgroundContext() -> NSManagedObjectContext {
self.container.newBackgroundContext()
}
public func save(viewContext: NSManagedObjectContext? = nil) {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
if context.hasChanges {
context.performAndWait {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
// You should not use this function in a shipping application, although it may be useful during development.
#if DEBUG
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
#else
CoreDataError.shared.handle(error, message: "An error occurred while writing the data.")
#endif
}
}
}
}
}

View File

@ -1,19 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension StatusData {
func attachments() -> [AttachmentData] {
guard let attachments = self.attachmentsRelation else {
return []
}
return attachments.sorted(by: { lhs, rhs in
lhs.order < rhs.order
})
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(StatusData)
public class StatusData: NSManagedObject {
}

View File

@ -1,68 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension StatusData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<StatusData> {
return NSFetchRequest<StatusData>(entityName: "StatusData")
}
@NSManaged public var accountAvatar: URL?
@NSManaged public var accountDisplayName: String?
@NSManaged public var accountId: String
@NSManaged public var accountUsername: String
@NSManaged public var applicationName: String?
@NSManaged public var applicationWebsite: URL?
@NSManaged public var bookmarked: Bool
@NSManaged public var content: String
@NSManaged public var createdAt: String
@NSManaged public var favourited: Bool
@NSManaged public var favouritesCount: Int32
@NSManaged public var id: String
@NSManaged public var inReplyToAccount: String?
@NSManaged public var inReplyToId: String?
@NSManaged public var muted: Bool
@NSManaged public var pinned: Bool
@NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int32
@NSManaged public var repliesCount: Int32
@NSManaged public var sensitive: Bool
@NSManaged public var spoilerText: String?
@NSManaged public var uri: String?
@NSManaged public var url: URL?
@NSManaged public var visibility: String
@NSManaged public var attachmentsRelation: Set<AttachmentData>?
@NSManaged public var pixelfedAccount: AccountData
@NSManaged public var rebloggedStatusId: String?
@NSManaged public var rebloggedAccountAvatar: URL?
@NSManaged public var rebloggedAccountDisplayName: String?
@NSManaged public var rebloggedAccountId: String?
@NSManaged public var rebloggedAccountUsername: String?
}
// MARK: Generated accessors for attachmentRelation
extension StatusData {
@objc(addAttachmentsRelationObject:)
@NSManaged public func addToAttachmentsRelation(_ value: AttachmentData)
@objc(removeAttachmentsRelationObject:)
@NSManaged public func removeFromAttachmentsRelation(_ value: AttachmentData)
@objc(addAttachmentsRelation:)
@NSManaged public func addToAttachmentsRelation(_ values: NSSet)
@objc(removeAttachmentsRelation:)
@NSManaged public func removeFromAttachmentsRelation(_ values: NSSet)
}
extension StatusData: Identifiable {
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension StatusData {
func isFaulty() -> Bool {
return self.isDeleted || self.isFault
}
}

View File

@ -1,88 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension StatusData {
func copyFrom(_ status: Status) {
if let reblog = status.reblog {
self.copyFrom(reblog)
self.id = status.id
self.rebloggedStatusId = reblog.id
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id
self.rebloggedAccountUsername = status.account.acct
} else {
self.id = status.id
self.createdAt = status.createdAt
self.accountAvatar = status.account.avatar
self.accountDisplayName = status.account.displayName
self.accountId = status.account.id
self.accountUsername = status.account.acct
self.applicationName = status.application?.name
self.applicationWebsite = status.application?.website
self.bookmarked = status.bookmarked
self.content = status.content.htmlValue
self.favourited = status.favourited
self.favouritesCount = Int32(status.favouritesCount)
self.inReplyToAccount = status.inReplyToAccount
self.inReplyToId = status.inReplyToId
self.muted = status.muted
self.pinned = status.pinned
self.reblogged = status.reblogged
self.reblogsCount = Int32(status.reblogsCount)
self.repliesCount = Int32(status.repliesCount)
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url
self.visibility = status.visibility.rawValue
}
}
func updateFrom(_ status: Status) {
if let reblog = status.reblog {
self.updateFrom(reblog)
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id
self.rebloggedAccountUsername = status.account.acct
} else {
self.accountAvatar = status.account.avatar
self.accountDisplayName = status.account.displayName
self.accountUsername = status.account.acct
self.applicationName = status.application?.name
self.applicationWebsite = status.application?.website
self.bookmarked = status.bookmarked
self.content = status.content.htmlValue
self.favourited = status.favourited
self.favouritesCount = Int32(status.favouritesCount)
self.inReplyToAccount = status.inReplyToAccount
self.inReplyToId = status.inReplyToId
self.muted = status.muted
self.pinned = status.pinned
self.reblogged = status.reblogged
self.reblogsCount = Int32(status.reblogsCount)
self.repliesCount = Int32(status.repliesCount)
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url
self.visibility = status.visibility.rawValue
}
}
}
public extension StatusData {
func getOrginalStatusId() -> String {
return self.rebloggedStatusId ?? self.id
}
}

View File

@ -1,136 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
import PixelfedKit
class StatusDataHandler {
public static let shared = StatusDataHandler()
private init() { }
func getAllStatuses(accountId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return []
}
}
func getAllOlderStatuses(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate1 = NSPredicate(format: "id < %@", statusId)
let predicate2 = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1, predicate2])
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return []
}
}
func getStatusData(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
fetchRequest.fetchLimit = 1
let predicate1 = NSPredicate(format: "id = %@", statusId)
let predicate2 = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1, predicate2])
do {
return try context.fetch(fetchRequest).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return nil
}
}
func getMaximumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
do {
let statuses = try context.fetch(fetchRequest)
return statuses.first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching maximum status (getMaximumStatus).")
return nil
}
}
func getMinimumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
do {
let statuses = try context.fetch(fetchRequest)
return statuses.first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching minimum status (getMinimumtatus).")
return nil
}
}
func remove(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) {
let status = self.getStatusData(accountId: accountId, statusId: statusId)
guard let status else {
return
}
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
context.delete(status)
do {
try context.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during deleting status (remove).")
}
}
func setFavourited(accountId: String, statusId: String) {
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
if let statusData = self.getStatusData(accountId: accountId, statusId: statusId, viewContext: backgroundContext) {
statusData.favourited = true
CoreDataHandler.shared.save(viewContext: backgroundContext)
}
}
func createStatusDataEntity(viewContext: NSManagedObjectContext? = nil) -> StatusData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return StatusData(context: context)
}
}

View File

@ -0,0 +1,30 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import OSLog
import EnvironmentKit
import SwiftData
public class SwiftDataHandler {
public static let shared = SwiftDataHandler()
lazy var sharedModelContainer: ModelContainer = {
let schema = Schema([
ApplicationSettings.self,
AccountData.self,
ViewedStatus.self,
AccountRelationship.self
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
}

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Vernissage-019.xcdatamodel</string>
</dict>
<dict/>
</plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="acct" attributeType="String"/>

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(ViewedStatus)
public class ViewedStatus: NSManagedObject {
}

View File

@ -1,23 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension ViewedStatus {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ViewedStatus> {
return NSFetchRequest<ViewedStatus>(entityName: "ViewedStatus")
}
@NSManaged public var id: String
@NSManaged public var reblogId: String?
@NSManaged public var date: Date
@NSManaged public var pixelfedAccount: AccountData
}
extension ViewedStatus: Identifiable {
}

View File

@ -0,0 +1,26 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
@Model final public class ViewedStatus {
@Attribute(.unique) public var id: String
public var reblogId: String?
public var date: Date
public var pixelfedAccount: AccountData?
init(id: String, reblogId: String? = nil, date: Date, pixelfedAccount: AccountData? = nil) {
self.id = id
self.reblogId = reblogId
self.date = date
self.pixelfedAccount = pixelfedAccount
}
}
extension ViewedStatus: Identifiable {
}

View File

@ -5,37 +5,49 @@
//
import Foundation
import CoreData
import SwiftData
import PixelfedKit
class ViewedStatusHandler {
public static let shared = ViewedStatusHandler()
private init() { }
/// Append new visible statuses to database.
func append(contentsOf statuses: [Status], accountId: String, modelContext: ModelContext) throws {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
for status in statuses {
guard self.getViewedStatus(accountId: accountId, statusId: status.id, modelContext: modelContext) == nil else {
continue
}
let viewedStatus = ViewedStatus(id: status.id, reblogId: status.reblog?.id, date: Date())
modelContext.insert(viewedStatus)
func createViewedStatusEntity(viewContext: NSManagedObjectContext? = nil) -> ViewedStatus {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ViewedStatus(context: context)
viewedStatus.pixelfedAccount = accountDataFromDb
accountDataFromDb.viewedStatuses.append(viewedStatus)
}
try modelContext.save()
}
/// Check if given status (real picture) has been already visible on the timeline (during last month).
func hasBeenAlreadyOnTimeline(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool {
func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
guard let reblog = status.reblog else {
return false
}
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = ViewedStatus.fetchRequest()
fetchRequest.fetchLimit = 1
let statusIdPredicate = NSPredicate(format: "id = %@", reblog.id)
let reblogIdPredicate = NSPredicate(format: "reblogId = %@", reblog.id)
let idPredicates = NSCompoundPredicate.init(type: .or, subpredicates: [statusIdPredicate, reblogIdPredicate])
let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [idPredicates, accountPredicate])
do {
guard let first = try context.fetch(fetchRequest).first else {
let reblogId = reblog.id
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
predicate: #Predicate { $0.pixelfedAccount?.id == accountId && $0.id != status.id && ($0.id == reblogId || $0.reblogId == reblogId) }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
guard let first = try modelContext.fetch(fetchDescriptor).first else {
return false
}
@ -55,25 +67,42 @@ class ViewedStatusHandler {
}
/// Mark to delete statuses older then one month.
func deleteOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) {
let oldViewedStatuses = self.getOldViewedStatuses(viewContext: viewContext)
func deleteOldViewedStatuses(modelContext: ModelContext) throws {
let oldViewedStatuses = self.getOldViewedStatuses(modelContext: modelContext)
for status in oldViewedStatuses {
viewContext?.delete(status)
modelContext.delete(status)
}
try modelContext.save()
}
private func getViewedStatus(accountId: String, statusId: String, modelContext: ModelContext) -> ViewedStatus? {
do {
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
predicate: #Predicate { $0.id == statusId && $0.pixelfedAccount?.id == accountId }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).")
return nil
}
}
private func getOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) -> [ViewedStatus] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
private func getOldViewedStatuses(modelContext: ModelContext) -> [ViewedStatus] {
guard let date = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else {
return []
}
do {
let fetchRequest = ViewedStatus.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "date < %@", date as NSDate)
return try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
predicate: #Predicate { $0.date < date }
)
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).")
return []

View File

@ -7,9 +7,7 @@ let package = Package(
name: "EnvironmentKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -9,7 +9,7 @@ import SwiftUI
import PixelfedKit
import ClientKit
public class ApplicationState: ObservableObject {
@Observable public class ApplicationState {
public static let shared = ApplicationState()
private init() { }
@ -24,94 +24,94 @@ public class ApplicationState: ObservableObject {
private static let defaults = Defaults()
/// Actual signed in account.
@Published public private(set) var account: AccountModel?
public private(set) var account: AccountModel?
/// The maximum number of allowed characters per status.
@Published public private(set) var statusMaxCharacters = defaults.statusMaxCharacters
public private(set) var statusMaxCharacters = defaults.statusMaxCharacters
/// The maximum number of media attachments that can be added to a status.
@Published public private(set) var statusMaxMediaAttachments = defaults.statusMaxMediaAttachments
public private(set) var statusMaxMediaAttachments = defaults.statusMaxMediaAttachments
/// Each URL in a status will be assumed to be exactly this many characters.
@Published public private(set) var statusCharactersReservedPerUrl = defaults.statusCharactersReservedPerUrl
public private(set) var statusCharactersReservedPerUrl = defaults.statusCharactersReservedPerUrl
/// Last status seen by the user.
@Published public var lastSeenStatusId: String?
public var lastSeenStatusId: String?
/// Amount of new statuses which are not displayed yet to the user.
@Published public var amountOfNewStatuses = 0
public var amountOfNewStatuses = 0
/// Model for newly created comment.
@Published public var newComment: CommentModel?
public var newComment: CommentModel?
/// Active icon name.
@Published public var activeIcon = "Default"
public var activeIcon = "Default"
/// Tint color in whole application.
@Published public var tintColor = TintColor.accentColor2
public var tintColor = TintColor.accentColor2
/// Application theme.
@Published public var theme = Theme.system
public var theme = Theme.system
/// Avatar shape.
@Published public var avatarShape = AvatarShape.circle
public var avatarShape = AvatarShape.circle
/// Status id for showed interaction row.
@Published public var showInteractionStatusId = ""
public var showInteractionStatusId = ""
/// Should we fire haptic when user change tabs.
@Published public var hapticTabSelectionEnabled = true
public var hapticTabSelectionEnabled = true
/// Should we fire haptic when user refresh list.
@Published public var hapticRefreshEnabled = true
public var hapticRefreshEnabled = true
/// Should we fire haptic when user tap button.
@Published public var hapticButtonPressEnabled = true
public var hapticButtonPressEnabled = true
/// Should we fire haptic when animation is finished.
@Published public var hapticAnimationEnabled = true
public var hapticAnimationEnabled = true
/// Should we fire haptic when notification occures.
@Published public var hapticNotificationEnabled = true
public var hapticNotificationEnabled = true
/// Should sensitive photos without mask.
@Published public var showSensitive = false
public var showSensitive = false
/// Should photo description for visually impaired be displayed.
@Published public var showPhotoDescription = false
public var showPhotoDescription = false
/// Status which should be shown from URL.
@Published public var showStatusId: String?
public var showStatusId: String?
/// Account which should be shown from URL.
@Published public var showAccountId: String?
public var showAccountId: String?
/// Updated user profile.
@Published public var updatedProfile: Account?
public var updatedProfile: Account?
/// Information which menu should be shown (top or bottom).
@Published public var menuPosition = MenuPosition.top
public var menuPosition = MenuPosition.top
/// Should avatars be visible on timelines.
@Published public var showAvatarsOnTimeline = false
public var showAvatarsOnTimeline = false
/// Should favourites be visible on timelines.
@Published public var showFavouritesOnTimeline = false
public var showFavouritesOnTimeline = false
/// Should ALT icon be visible on timelines.
@Published public var showAltIconOnTimeline = false
public var showAltIconOnTimeline = false
/// Show warning about missing ALT texts on compose screen.
@Published public var warnAboutMissingAlt = true
public var warnAboutMissingAlt = true
/// Show grid of photos on user profile.
@Published public var showGridOnUserProfile = false
public var showGridOnUserProfile = false
/// Show reboosted statuses on home timeline.
@Published public var showReboostedStatuses = false
public var showReboostedStatuses = false
/// Hide statuses without ALT text.
@Published public var hideStatusesWithoutAlt = false
public var hideStatusesWithoutAlt = false
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
self.account = accountModel

View File

@ -1296,40 +1296,6 @@
}
}
},
"global.error.canceledImageDownload" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Download image has been canceled."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "La descarga de la imagen ha sido cancelada."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Irudiaren deskarga bertan behera utzi da."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le téléchargement de l'image a été annulé."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pobieranie zdjęcia zostało anulowane."
}
}
}
},
"global.error.cannotConfigureTransactionListener" : {
"localizations" : {
"en" : {
@ -1806,40 +1772,6 @@
}
}
},
"global.error.statusesNotRetrieved" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Statuses not retrieved."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "No se pudieron obtener los estados."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ez dira egoerak eskuratu."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Statuts non récupérés."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Statusy nie zostały pobrane."
}
}
}
},
"global.title.close" : {
"comment" : "Close",
"localizations" : {
@ -2060,40 +1992,6 @@
}
}
},
"home.title.noPhotos" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unfortunately, there are no photos here."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Desafortunadamente, no hay fotos aquí."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Argazkirik ez."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Malheureusement, il n'y a pas de photos ici."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Niestety nie ma jeszcze żadnych zdjęć."
}
}
}
},
"instance.error.loadingDataFailed" : {
"localizations" : {
"en" : {

View File

@ -7,9 +7,7 @@ let package = Package(
name: "PixelfedKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -46,7 +46,7 @@ From time to time you have to come back and translate lines which has been added
Things that should be implemented in version 2.0:
- [ ] Use auto generated resources (Color/Images) instead static extensions (how to do this in separete Swift Packages?)
- [ ] Move to xcstring (new Xcode transaction system)
- [x] Move to xcstring (new Xcode transaction system)
- [ ] Move to new Observable macro (iOS 17)
- [ ] Migrate to SwiftData (iOS 17)
- [ ] Use ViewModels

View File

@ -7,9 +7,7 @@ let package = Package(
name: "ServicesKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -21,7 +21,7 @@ public class ErrorService {
case is LocalizedError:
ToastrService.shared.showError(title: message, subtitle: error.localizedDescription)
default:
ToastrService.shared.showError(subtitle: localizedMessage)
ToastrService.shared.showError(title: "", subtitle: localizedMessage)
}
}
@ -35,7 +35,7 @@ public class ErrorService {
case is LocalizedError:
ToastrService.shared.showError(localizedMessage: localizedMessage, subtitle: error.localizedDescription)
default:
ToastrService.shared.showError(subtitle: localizedMessage)
ToastrService.shared.showError(title: "", subtitle: localizedMessage)
}
}

View File

@ -28,7 +28,6 @@ public class HapticService {
impactGenerator.prepare()
}
@MainActor
public func fireHaptic(of type: HapticType) {
guard supportsHaptics else { return }

View File

@ -1,6 +1,9 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"global.error.downloadingImageFailed" : {
"localizations" : {
"en" : {
@ -24,6 +27,7 @@
}
},
"global.error.unexpected" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {

View File

@ -39,7 +39,7 @@ public class ToastrService {
Drops.show(drop)
}
public func showError(title: LocalizedStringResource = LocalizedStringResource("global.error.unexpected"), imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
public func showError(title: LocalizedStringResource, imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
let image = self.createImage(systemName: imageSystemName, color: UIColor(Color.accentColor))
self.showError(title: title.key, image: image, subtitle: subtitle)
}
@ -49,12 +49,12 @@ public class ToastrService {
self.showError(title: localizedMessage, image: image, subtitle: subtitle)
}
public func showError(title: LocalizedStringResource = LocalizedStringResource("global.error.unexpected"), imageName: String, subtitle: String? = nil) {
public func showError(title: LocalizedStringResource, imageName: String, subtitle: String? = nil) {
let image = self.createImage(name: imageName, color: UIColor(Color.accentColor))
self.showError(title: title.key, image: image, subtitle: subtitle)
}
private func showError(title: String = "global.error.unexpected", image: UIImage?, subtitle: String? = nil) {
private func showError(title: String, image: UIImage?, subtitle: String? = nil) {
let drop = Drop(
title: NSLocalizedString(title, comment: "Error displayed to the user."),
subtitle: subtitle,

View File

@ -7,12 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F802884E297AEED5000BDD51 /* DatabaseError.swift */; };
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F805DCF029DBEF83006A1FD9 /* ReportView.swift */; };
F808641429756666009F035C /* NotificationRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808641329756666009F035C /* NotificationRowView.swift */; };
@ -22,8 +16,6 @@
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD42966BB7E001D9973 /* Nuke */; };
F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD62966BB7E001D9973 /* NukeExtensions */; };
F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD82966BB7E001D9973 /* NukeUI */; };
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDC2966CF17001D9973 /* StatusData+Status.swift */; };
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */; };
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */; };
@ -38,13 +30,9 @@
F858906B29E1CC7A00D4BDED /* UIApplication+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */; };
F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F85D0C642ABA08F9002B3577 /* Assets.xcassets */; };
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4974296407F100751DF7 /* HomeTimelineService.swift */; };
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497629640A5200751DF7 /* ImageRow.swift */; };
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497829640B9D00751DF7 /* ImagesCarousel.swift */; };
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497C29640D5900751DF7 /* InteractionRow.swift */; };
F85D497F296416C800751DF7 /* CommentsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497E296416C800751DF7 /* CommentsSectionView.swift */; };
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */; };
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49842964301800751DF7 /* StatusData+Attachments.swift */; };
F85D4DFE29B78C8400345267 /* HashtagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4DFD29B78C8400345267 /* HashtagModel.swift */; };
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DBF8E296732E20069BF89 /* AccountsView.swift */; };
F86167C6297FE6CC004D1F67 /* AvatarShapesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86167C5297FE6CC004D1F67 /* AvatarShapesSectionView.swift */; };
@ -59,29 +47,14 @@
F864F77829BB930000B13921 /* PhotoWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77729BB930000B13921 /* PhotoWidgetEntry.swift */; };
F864F77A29BB94A800B13921 /* PixelfedKit in Frameworks */ = {isa = PBXBuildFile; productRef = F864F77929BB94A800B13921 /* PixelfedKit */; };
F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77B29BB982100B13921 /* StatusFetcher.swift */; };
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F864F78529BB9A7100B13921 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F864F78629BB9A7400B13921 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F864F78729BB9A7700B13921 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F864F78829BB9A7B00B13921 /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F864F78529BB9A7100B13921 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F864F78729BB9A7700B13921 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F864F78829BB9A7B00B13921 /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F864F78929BB9A7D00B13921 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F864F78A29BB9A8000B13921 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F864F78B29BB9A8300B13921 /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F864F78C29BB9A8500B13921 /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F864F7A529BBA01D00B13921 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F865B4CD2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4CE2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4CF2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4D12A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F865B4D22A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; };
@ -121,7 +94,6 @@
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51229E02FD800CE6141 /* ComposeView.swift */; };
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51529E0307F00CE6141 /* NotificationsName.swift */; };
F88BC51B29E0350300CE6141 /* ClientKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC51A29E0350300CE6141 /* ClientKit */; };
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */; };
F88BC51F29E03ED300CE6141 /* ClientKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC51E29E03ED300CE6141 /* ClientKit */; };
F88BC52729E0431D00CE6141 /* ServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52629E0431D00CE6141 /* ServicesKit */; };
F88BC52A29E046D700CE6141 /* WidgetsKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52929E046D700CE6141 /* WidgetsKit */; };
@ -132,40 +104,26 @@
F88BC53D29E06EAD00CE6141 /* ServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC53C29E06EAD00CE6141 /* ServicesKit */; };
F88BC53F29E06EB100CE6141 /* WidgetsKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC53E29E06EB100CE6141 /* WidgetsKit */; };
F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F88BC54329E072AC00CE6141 /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F88BC54629E072B500CE6141 /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F88BC54929E072C000CE6141 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */; };
F88BC55029E074EB00CE6141 /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F88BC54629E072B500CE6141 /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F88BC54729E072B800CE6141 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F88BC54929E072C000CE6141 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F88BC55229E0798900CE6141 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88BC55129E0798900CE6141 /* SharedAssets.xcassets */; };
F88BC55429E0798900CE6141 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88BC55129E0798900CE6141 /* SharedAssets.xcassets */; };
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; };
F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; };
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; };
F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; };
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F88C2475295C37BB0006098B /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* StatusView.swift */; };
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D41297E69FD0057491A /* StatusesView.swift */; };
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D47297E90CD0057491A /* TrendStatusesView.swift */; };
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D49297EA0490057491A /* RouterPath.swift */; };
F88E4D4D297EA4290057491A /* EmojiText in Frameworks */ = {isa = PBXBuildFile; productRef = F88E4D4C297EA4290057491A /* EmojiText */; };
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D55297EAD6E0057491A /* AppRouteur.swift */; };
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; };
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD26295F400E009B20C9 /* NotificationsView.swift */; };
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CD29C35BF50022C449 /* ImageRowItem.swift */; };
F88FAD2B295F43B8009B20C9 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */; };
F89229EF2ADA63620040C964 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F89229EE2ADA63620040C964 /* Localizable.xcstrings */; };
F89229F02ADA63620040C964 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F89229EE2ADA63620040C964 /* Localizable.xcstrings */; };
@ -177,7 +135,6 @@
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00429A1F9B500F4159F /* AppMetadata.swift */; };
F89B5CC029D019B600549F2F /* HTMLString in Frameworks */ = {isa = PBXBuildFile; productRef = F89B5CBF29D019B600549F2F /* HTMLString */; };
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B5CC129D01BF700549F2F /* InstanceView.swift */; };
F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */; };
F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4129717FDC001DA3D4 /* AccountsSectionView.swift */; };
F89D6C4429718092001DA3D4 /* AccentsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4329718092001DA3D4 /* AccentsSectionView.swift */; };
F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; };
@ -193,13 +150,12 @@
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; };
F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; };
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; };
F8D0E5222AE2A2630061C561 /* HomeTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */; };
F8D0E5242AE2A88A0061C561 /* HomeTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */; };
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; };
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CB2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CC2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CD2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
@ -208,16 +164,12 @@
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; };
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */; };
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03229CDD52500416CCA /* EditProfileView.swift */; };
F8E7ADFA2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFB2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFC2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7AE002AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7AE002AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7AE022AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8E7AE032AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8E7AE042AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */; };
F8F6E44D29BCC1F90004795E /* PhotoMediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44829BCC0F00004795E /* PhotoMediumWidgetView.swift */; };
F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44A29BCC0FF0004795E /* PhotoLargeWidgetView.swift */; };
@ -259,21 +211,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataClass.swift"; sourceTree = "<group>"; };
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataClass.swift"; sourceTree = "<group>"; };
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = "<group>"; };
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F802884E297AEED5000BDD51 /* DatabaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = "<group>"; };
F805DCF029DBEF83006A1FD9 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
F808641329756666009F035C /* NotificationRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowView.swift; sourceTree = "<group>"; };
F8121CA7298A86D600B466C7 /* InstanceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceRowView.swift; sourceTree = "<group>"; };
F815F60B29E49CF20044566B /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-014.xcdatamodel"; sourceTree = "<group>"; };
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = "<group>"; };
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = "<group>"; };
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = "<group>"; };
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfilePrivateAccountView.swift; sourceTree = "<group>"; };
F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestsView.swift; sourceTree = "<group>"; };
@ -287,16 +230,11 @@
F84625F329FE2BF9002D3AF4 /* QRCodeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeProvider.swift; sourceTree = "<group>"; };
F84625F729FE2C2F002D3AF4 /* AccountFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFetcher.swift; sourceTree = "<group>"; };
F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Window.swift"; sourceTree = "<group>"; };
F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-010.xcdatamodel"; sourceTree = "<group>"; };
F85D0C642ABA08F9002B3577 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
F85D4974296407F100751DF7 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = "<group>"; };
F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = "<group>"; };
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesCarousel.swift; sourceTree = "<group>"; };
F85D497C29640D5900751DF7 /* InteractionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionRow.swift; sourceTree = "<group>"; };
F85D497E296416C800751DF7 /* CommentsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsSectionView.swift; sourceTree = "<group>"; };
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Comperable.swift"; sourceTree = "<group>"; };
F85D49842964301800751DF7 /* StatusData+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Attachments.swift"; sourceTree = "<group>"; };
F85D4DFD29B78C8400345267 /* HashtagModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagModel.swift; sourceTree = "<group>"; };
F85DBF8E296732E20069BF89 /* AccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsView.swift; sourceTree = "<group>"; };
F86167C5297FE6CC004D1F67 /* AvatarShapesSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarShapesSectionView.swift; sourceTree = "<group>"; };
@ -312,11 +250,7 @@
F864F77729BB930000B13921 /* PhotoWidgetEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidgetEntry.swift; sourceTree = "<group>"; };
F864F77B29BB982100B13921 /* StatusFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetcher.swift; sourceTree = "<group>"; };
F864F7A429BBA01D00B13921 /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Faulty.swift"; sourceTree = "<group>"; };
F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Faulty.swift"; sourceTree = "<group>"; };
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-013.xcdatamodel"; sourceTree = "<group>"; };
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettings.swift; sourceTree = "<group>"; };
F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = "<group>"; };
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsHandler.swift; sourceTree = "<group>"; };
F866F6A629604629002E8F88 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
@ -341,15 +275,12 @@
F8705A7D29FF880600DA818A /* FileFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileFetcher.swift; sourceTree = "<group>"; };
F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationOptions.swift; sourceTree = "<group>"; };
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItemDetails.swift; sourceTree = "<group>"; };
F871F21F29EF0FEC00A351EF /* Vernissage-011.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-011.xcdatamodel"; sourceTree = "<group>"; };
F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-019.xcdatamodel"; sourceTree = "<group>"; };
F8742FC329990AFB00E9642B /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
F8764186298ABB520057D362 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = "<group>"; };
F876418C298AE5020057D362 /* PaginableStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginableStatusesView.swift; sourceTree = "<group>"; };
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadataService.swift; sourceTree = "<group>"; };
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = "<group>"; };
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = "<group>"; };
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-015.xcdatamodel"; sourceTree = "<group>"; };
F883401F29B62AE900C3E096 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
F88AB05229B3613900345EDE /* PhotoUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoUrl.swift; sourceTree = "<group>"; };
F88AB05429B3626300345EDE /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
@ -363,7 +294,6 @@
F88BC51429E02FEB00CE6141 /* VernissageShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VernissageShareExtension.entitlements; sourceTree = "<group>"; };
F88BC51529E0307F00CE6141 /* NotificationsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsName.swift; sourceTree = "<group>"; };
F88BC51929E0344000CE6141 /* ClientKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ClientKit; sourceTree = "<group>"; };
F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+AccountModel.swift"; sourceTree = "<group>"; };
F88BC52529E0421F00CE6141 /* ServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ServicesKit; sourceTree = "<group>"; };
F88BC52829E0467400CE6141 /* WidgetsKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WidgetsKit; sourceTree = "<group>"; };
F88BC52B29E04AC200CE6141 /* EnvironmentKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EnvironmentKit; sourceTree = "<group>"; };
@ -374,19 +304,14 @@
F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
F88C246F295C37BB0006098B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = "<group>"; };
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* SwiftDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDataHandler.swift; sourceTree = "<group>"; };
F88C2481295C3A4F0006098B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
F88E4D41297E69FD0057491A /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = "<group>"; };
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendStatusesView.swift; sourceTree = "<group>"; };
F88E4D49297EA0490057491A /* RouterPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterPath.swift; sourceTree = "<group>"; };
F88E4D55297EAD6E0057491A /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = "<group>"; };
F88FAD26295F400E009B20C9 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = "<group>"; };
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-007.xcdatamodel"; sourceTree = "<group>"; };
F891E7CD29C35BF50022C449 /* ImageRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItem.swift; sourceTree = "<group>"; };
F88FAD29295F43B8009B20C9 /* AccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountData.swift; sourceTree = "<group>"; };
F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItemAsync.swift; sourceTree = "<group>"; };
F89229EE2ADA63620040C964 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
F897978E29684BCB00B22335 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
@ -395,20 +320,15 @@
F89A46DD296EABA20062125F /* StatusPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholderView.swift; sourceTree = "<group>"; };
F89AC00429A1F9B500F4159F /* AppMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadata.swift; sourceTree = "<group>"; };
F89B5CC129D01BF700549F2F /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = "<group>"; };
F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+HighestImage.swift"; sourceTree = "<group>"; };
F89D6C4129717FDC001DA3D4 /* AccountsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSectionView.swift; sourceTree = "<group>"; };
F89D6C4329718092001DA3D4 /* AccentsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentsSectionView.swift; sourceTree = "<group>"; };
F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSectionView.swift; sourceTree = "<group>"; };
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = "<group>"; };
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = "<group>"; };
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = "<group>"; };
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = "<group>"; };
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesGrid.swift; sourceTree = "<group>"; };
F8B05AC929B488C600857221 /* Vernissage-003.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-003.xcdatamodel"; sourceTree = "<group>"; };
F8B05ACA29B489B100857221 /* HapticsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsSectionView.swift; sourceTree = "<group>"; };
F8B05ACC29B48DD000857221 /* Vernissage-004.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-004.xcdatamodel"; sourceTree = "<group>"; };
F8B05ACD29B48E2F00857221 /* MediaSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSettingsView.swift; sourceTree = "<group>"; };
F8B0885D29942E31002AB40A /* ThirdPartyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyView.swift; sourceTree = "<group>"; };
F8B0885F29943498002AB40A /* OtherSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherSectionView.swift; sourceTree = "<group>"; };
@ -416,25 +336,18 @@
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = "<group>"; };
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-016.xcdatamodel"; sourceTree = "<group>"; };
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineView.swift; sourceTree = "<group>"; };
F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = "<group>"; };
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataClass.swift"; sourceTree = "<group>"; };
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatus.swift; sourceTree = "<group>"; };
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatusHandler.swift; sourceTree = "<group>"; };
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-017.xcdatamodel"; sourceTree = "<group>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = "<group>"; };
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = "<group>"; };
F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = "<group>"; };
F8E7ADF82AD44AFD0038FFFD /* Vernissage-018.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-018.xcdatamodel"; sourceTree = "<group>"; };
F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountRelationship+CoreDataClass.swift"; sourceTree = "<group>"; };
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountRelationship+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRelationship.swift; sourceTree = "<group>"; };
F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRelationshipHandler.swift; sourceTree = "<group>"; };
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = "<group>"; };
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = "<group>"; };
F8F6E44329BC5CAA0004795E /* VernissageWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VernissageWidgetExtension.entitlements; sourceTree = "<group>"; };
F8F6E44429BC5CC50004795E /* Vernissage.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vernissage.entitlements; sourceTree = "<group>"; };
F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSmallWidgetView.swift; sourceTree = "<group>"; };
@ -443,7 +356,6 @@
F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ContainerBackground.swift"; sourceTree = "<group>"; };
F8FB8AB929EB2ED400342C04 /* NavigationMenuButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuButtons.swift; sourceTree = "<group>"; };
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -538,7 +450,7 @@
F89D6C4829718868001DA3D4 /* StatusView */,
F88C246D295C37B80006098B /* MainView.swift */,
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */,
F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */,
F88AB05729B36B8200345EDE /* AccountsPhotoView.swift */,
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */,
F883401F29B62AE900C3E096 /* SearchView.swift */,
@ -578,32 +490,13 @@
F8341F96295C6427009C8EE6 /* CoreData */ = {
isa = PBXGroup;
children = (
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */,
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */,
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */,
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */,
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */,
F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */,
F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */,
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */,
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */,
F85D49842964301800751DF7 /* StatusData+Attachments.swift */,
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */,
F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */,
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */,
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */,
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */,
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */,
F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */,
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */,
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */,
F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */,
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */,
F88C2474295C37BB0006098B /* CoreDataHandler.swift */,
F866F69F296040A8002E8F88 /* ApplicationSettings.swift */,
F88FAD29295F43B8009B20C9 /* AccountData.swift */,
F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */,
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */,
F88C2474295C37BB0006098B /* SwiftDataHandler.swift */,
F866F6A229604161002E8F88 /* AccountDataHandler.swift */,
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */,
F80048072961E6DE00E6868A /* StatusDataHandler.swift */,
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */,
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */,
F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */,
F864F7A429BBA01D00B13921 /* CoreDataError.swift */,
@ -622,8 +515,6 @@
F83901A2295D863B00456AE2 /* Widgets */ = {
isa = PBXGroup;
children = (
F85D497629640A5200751DF7 /* ImageRow.swift */,
F891E7CD29C35BF50022C449 /* ImageRowItem.swift */,
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */,
F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */,
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */,
@ -878,10 +769,10 @@
children = (
F85D4970296402DC00751DF7 /* AuthorizationService.swift */,
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */,
F85D4974296407F100751DF7 /* HomeTimelineService.swift */,
F88E4D49297EA0490057491A /* RouterPath.swift */,
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */,
F86BC9E829EBBB66009415EC /* ImageSaver.swift */,
F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -1128,43 +1019,30 @@
F8705A7729FF7ABD00DA818A /* QRCodeSmallWidgetView.swift in Sources */,
F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */,
F84625ED29FE295B002D3AF4 /* QRCodeLargeWidgetView.swift in Sources */,
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */,
F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */,
F864F76629BB91B400B13921 /* PhotoWidget.swift in Sources */,
F8E7ADFB2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F8F6E44D29BCC1F90004795E /* PhotoMediumWidgetView.swift in Sources */,
F815F60C29E49CF20044566B /* Avatar.swift in Sources */,
F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */,
F864F76429BB91B400B13921 /* VernissageWidgetBundle.swift in Sources */,
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */,
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F8D8E0CC2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */,
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */,
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */,
F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */,
F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8FAA0AD2AB0BCB400FD78BD /* View+ContainerBackground.swift in Sources */,
F864F78629BB9A7400B13921 /* AccountData+CoreDataClass.swift in Sources */,
F8705A7B29FF872F00DA818A /* QRCodeGenerator.swift in Sources */,
F865B4CE2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F8705A7E29FF880600DA818A /* FileFetcher.swift in Sources */,
F865B4D22A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F864F78529BB9A7100B13921 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F864F78529BB9A7100B13921 /* ApplicationSettings.swift in Sources */,
F84625E929FE2788002D3AF4 /* QRCodeWidget.swift in Sources */,
F864F78729BB9A7700B13921 /* AccountData+CoreDataProperties.swift in Sources */,
F864F78729BB9A7700B13921 /* AccountData.swift in Sources */,
F8E7AE032AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F864F78829BB9A7B00B13921 /* CoreDataHandler.swift in Sources */,
F864F78829BB9A7B00B13921 /* SwiftDataHandler.swift in Sources */,
F8F6E45129BCE9190004795E /* UIImage+Resize.swift in Sources */,
F864F78929BB9A7D00B13921 /* AccountDataHandler.swift in Sources */,
F864F78A29BB9A8000B13921 /* ApplicationSettingsHandler.swift in Sources */,
F84625F229FE2B6B002D3AF4 /* QRCodeWidgetEntry.swift in Sources */,
F84625EB29FE28D4002D3AF4 /* QRCodeWidgetEntryView.swift in Sources */,
F864F78C29BB9A8500B13921 /* AttachmentDataHandler.swift in Sources */,
F84625F829FE2C2F002D3AF4 /* AccountFetcher.swift in Sources */,
F84625F429FE2BF9002D3AF4 /* QRCodeProvider.swift in Sources */,
F864F78B29BB9A8300B13921 /* StatusDataHandler.swift in Sources */,
F8705A7929FF7CCB00DA818A /* QRCodeMediumWidgetView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1174,32 +1052,18 @@
buildActionMask = 2147483647;
files = (
F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */,
F8E7ADFC2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */,
F88BC54729E072B800CE6141 /* AccountData.swift in Sources */,
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */,
F8E7AE002AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */,
F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F88BC54629E072B500CE6141 /* CoreDataHandler.swift in Sources */,
F8E7AE002AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F88BC54629E072B500CE6141 /* SwiftDataHandler.swift in Sources */,
F88BC50529E02F3900CE6141 /* ShareViewController.swift in Sources */,
F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */,
F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */,
F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */,
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8D8E0CD2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */,
F8E7AE042AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */,
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */,
F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */,
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */,
F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */,
F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F865B4CF2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F88BC54929E072C000CE6141 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F88BC55029E074EB00CE6141 /* Vernissage.xcdatamodeld in Sources */,
F88BC54329E072AC00CE6141 /* StatusDataHandler.swift in Sources */,
F88BC54929E072C000CE6141 /* ApplicationSettings.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1207,62 +1071,44 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */,
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */,
F88AB05529B3626300345EDE /* ImageGrid.swift in Sources */,
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */,
F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */,
F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */,
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */,
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */,
F865B4D12A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F8B08862299435C9002AB40A /* SupportView.swift in Sources */,
F8B05ACB29B489B100857221 /* HapticsSectionView.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */,
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData.swift in Sources */,
F88C2475295C37BB0006098B /* SwiftDataHandler.swift in Sources */,
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */,
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */,
F883402029B62AE900C3E096 /* SearchView.swift in Sources */,
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */,
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */,
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */,
F808641429756666009F035C /* NotificationRowView.swift in Sources */,
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F8624D3D29F2D3AC00204986 /* SelectedMenuItemDetails.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */,
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
F86A4305299AA12800DF7645 /* PurchaseError.swift in Sources */,
F8B05ACE29B48E2F00857221 /* MediaSettingsView.swift in Sources */,
F89D6C4429718092001DA3D4 /* AccentsSectionView.swift in Sources */,
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */,
F878842229A4A4E3003CFAD2 /* AppMetadataService.swift in Sources */,
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
F8D0E5222AE2A2630061C561 /* HomeTimelineView.swift in Sources */,
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */,
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */,
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */,
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */,
F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */,
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */,
F8E7ADFA2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */,
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */,
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */,
F89A46DE296EABA20062125F /* StatusPlaceholderView.swift in Sources */,
@ -1274,13 +1120,10 @@
F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */,
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */,
F858906B29E1CC7A00D4BDED /* UIApplication+Window.swift in Sources */,
F865B4CD2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F876418D298AE5020057D362 /* PaginableStatusesView.swift in Sources */,
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
F8764187298ABB520057D362 /* ViewState.swift in Sources */,
F870EE5229F1645C00A2D43B /* MainNavigationOptions.swift in Sources */,
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */,
F86B7214296BFDCE00EE59EC /* UserProfileHeaderView.swift in Sources */,
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
F86167C6297FE6CC004D1F67 /* AvatarShapesSectionView.swift in Sources */,
@ -1292,10 +1135,10 @@
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */,
F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */,
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */,
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */,
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */,
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */,
F8D0E5242AE2A88A0061C561 /* HomeTimelineService.swift in Sources */,
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */,
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
@ -1304,11 +1147,11 @@
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */,
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8D8E0CB2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F89992C9296D6DC7005994BF /* CommentBodyView.swift in Sources */,
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F866F6A1296040A8002E8F88 /* ApplicationSettings.swift in Sources */,
F8E7AE022AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F86A4303299A9AF500DF7645 /* TipsStore.swift in Sources */,
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */,
@ -1831,38 +1674,6 @@
productName = Semaphore;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */,
F8E7ADF82AD44AFD0038FFFD /* Vernissage-018.xcdatamodel */,
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */,
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */,
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */,
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */,
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */,
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */,
F871F21F29EF0FEC00A351EF /* Vernissage-011.xcdatamodel */,
F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */,
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */,
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */,
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */,
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */,
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */,
F8B05ACC29B48DD000857221 /* Vernissage-004.xcdatamodel */,
F8B05AC929B488C600857221 /* Vernissage-003.xcdatamodel */,
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */,
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
currentVersion = F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = F88C2460295C37B80006098B /* Project object */;
}

View File

@ -8,8 +8,8 @@ import Foundation
import SwiftUI
import WidgetsKit
@MainActor
extension View {
func withAppRouteur() -> some View {
self.navigationDestination(for: RouteurDestinations.self) { destination in
switch destination {

View File

@ -10,14 +10,13 @@ import ServicesKit
import OSLog
import EnvironmentKit
@MainActor
final class TipsStore: ObservableObject {
@Observable final class TipsStore {
/// Products are registered in AppStore connect (and for development in InAppPurchaseStoreKitConfiguration.storekit file).
@Published private(set) var items = [Product]()
private(set) var items = [Product]()
/// Status of the purchase.
@Published private(set) var status: ActionStatus? {
private(set) var status: ActionStatus? {
didSet {
switch status {
case .failed:
@ -29,7 +28,7 @@ final class TipsStore: ObservableObject {
}
/// True when error during purchase occures.
@Published var hasError = false
var hasError = false
/// Error during purchase.
var error: PurchaseError? {

View File

@ -7,11 +7,11 @@
import Foundation
import SwiftUI
class NavigationMenuItemDetails: ObservableObject, Identifiable {
@Published var title: LocalizedStringKey
@Published var image: String
@Observable class NavigationMenuItemDetails: Identifiable {
var title: LocalizedStringKey
var image: String
@Published var viewMode: MainView.ViewMode {
var viewMode: MainView.ViewMode {
didSet {
self.title = viewMode.title
self.image = viewMode.image

View File

@ -6,13 +6,13 @@
import Foundation
public class PhotoUrl: ObservableObject, Identifiable {
@Observable public class PhotoUrl: Identifiable {
public var id: String
@Published public var statusId: String?
@Published public var url: URL?
@Published public var blurhash: String?
@Published public var sensitive = false
public var statusId: String?
public var url: URL?
public var blurhash: String?
public var sensitive = false
init(id: String) {
self.id = id

View File

@ -8,7 +8,7 @@ import SwiftUI
import Foundation
import PixelfedKit
public class RelationshipModel: ObservableObject {
@Observable public class RelationshipModel {
enum RelationshipAction {
case follow
case unfollow
@ -17,46 +17,46 @@ public class RelationshipModel: ObservableObject {
}
/// The account ID.
@Published public var id: EntityId
public var id: EntityId
/// Are you followed by this user?
@Published public var followedBy: Bool
public var followedBy: Bool
/// Is this user blocking you?
@Published public var blockedBy: Bool
public var blockedBy: Bool
/// Are you muting notifications from this user?
@Published public var mutingNotifications: Bool
public var mutingNotifications: Bool
/// Do you have a pending follow request for this user?
@Published public var requested: Bool
public var requested: Bool
/// Are you receiving this users boosts in your home timeline?
@Published public var showingReblogs: Bool
public var showingReblogs: Bool
/// Have you enabled notifications for this user?
@Published public var notifying: Bool
public var notifying: Bool
/// Are you blocking this users domain?
@Published public var domainBlocking: Bool
public var domainBlocking: Bool
/// Are you featuring this user on your profile?
@Published public var endorsed: Bool
public var endorsed: Bool
/// Which languages are you following from this user? Array of String (ISO 639-1 language two-letter code).
@Published public var languages: [String]?
public var languages: [String]?
/// This users profile bio.
@Published public var note: String?
public var note: String?
/// Are you following this user?
@Published public var following: Bool
public var following: Bool
/// Are you blocking this user?
@Published public var blocking: Bool
public var blocking: Bool
/// Are you muting this user?
@Published public var muting: Bool
public var muting: Bool
public init() {
self.id = ""

View File

@ -33,7 +33,7 @@ struct AnimatePlaceholderModifier: AnimatableModifier {
guard isLoading else { return }
isAnim.toggle()
}
.onChange(of: isLoading) { _ in
.onChange(of: isLoading) {
isAnim.toggle()
}
}

View File

@ -7,18 +7,19 @@
import Foundation
import PixelfedKit
import ClientKit
import CoreData
import SwiftData
import AuthenticationServices
import ServicesKit
import EnvironmentKit
/// Srvice responsible for login user into the Pixelfed account.
@MainActor
public class AuthorizationService {
public static let shared = AuthorizationService()
private init() { }
/// Access token verification.
public func verifyAccount(session: AuthorizationSession, accountModel: AccountModel, _ result: @escaping (AccountModel?) -> Void) async {
public func verifyAccount(session: AuthorizationSession, accountModel: AccountModel, modelContext: ModelContext, _ result: @escaping (AccountModel?) -> Void) async {
// When we dont have even one account stored in database then we have to ask user to enter server and sign in.
guard let accessToken = accountModel.accessToken else {
result(nil)
@ -33,12 +34,15 @@ public class AuthorizationService {
let signedInAccountModel = await self.update(accountId: accountModel.id,
basedOn: account,
accessToken: accessToken,
refreshToken: accountModel.refreshToken)
refreshToken: accountModel.refreshToken,
modelContext: modelContext)
result(signedInAccountModel)
} catch {
do {
let signedInAccountModel = try await self.refreshCredentials(for: accountModel, presentationContextProvider: session)
let signedInAccountModel = try await self.refreshCredentials(for: accountModel,
presentationContextProvider: session,
modelContext: modelContext)
result(signedInAccountModel)
} catch {
ErrorService.shared.handle(error, message: "global.error.refreshingCredentialsTitle")
@ -50,7 +54,10 @@ public class AuthorizationService {
}
/// Sign in to the Pixelfed server.
public func sign(in serverAddress: String, session: AuthorizationSession, _ result: @escaping (AccountModel) -> Void) async throws {
public func sign(in serverAddress: String,
session:AuthorizationSession,
modelContext: ModelContext,
_ result: @escaping (AccountModel) -> Void) async throws {
guard let baseUrl = URL(string: serverAddress) else {
throw AuthorisationError.badServerUrl
@ -82,8 +89,7 @@ public class AuthorizationService {
let account = try await authenticatedClient.verifyCredentials()
// Get/create account object in database.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
let accountData = self.getAccountData(account: account, backgroundContext: backgroundContext)
let accountData = self.getAccountData(account: account, serverUrl: baseUrl, modelContext: modelContext)
accountData.id = account.id
accountData.username = account.username
@ -120,27 +126,27 @@ public class AuthorizationService {
}
// Set newly created account as current (only when we create a first account).
let defaultSettings = ApplicationSettingsHandler.shared.get(viewContext: backgroundContext)
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
if defaultSettings.currentAccount == nil {
defaultSettings.currentAccount = accountData.id
}
// Save account/settings data in database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
try modelContext.save()
// Return account data.
let accountModel = accountData.toAccountModel()
result(accountModel)
}
public func refreshAccessTokens() async {
let accounts = AccountDataHandler.shared.getAccountsData()
public func refreshAccessTokens(modelContext: ModelContext) async {
let accounts = AccountDataHandler.shared.getAccountsData(modelContext: modelContext)
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask {
group.addTask { @MainActor in
do {
_ = try await self.refreshAccessToken(accountData: account)
_ = try await self.refreshAccessToken(accountData: account, modelContext: modelContext)
#if DEBUG
ToastrService.shared.showSuccess("global.title.newAccessTokenRetrieved", imageSystemName: "key.fill")
@ -157,7 +163,7 @@ public class AuthorizationService {
}
}
private func refreshAccessToken(accountData: AccountData) async throws -> AccountModel? {
private func refreshAccessToken(accountData: AccountData, modelContext: ModelContext) async throws -> AccountModel? {
let client = PixelfedClient(baseURL: accountData.serverUrl)
guard let refreshToken = accountData.refreshToken else {
@ -177,11 +183,13 @@ public class AuthorizationService {
return await self.update(accountId: accountData.id,
basedOn: account,
accessToken: oAuthSwiftCredential.oauthToken,
refreshToken: oAuthSwiftCredential.oauthRefreshToken)
refreshToken: oAuthSwiftCredential.oauthRefreshToken,
modelContext: modelContext)
}
private func refreshCredentials(for accountModel: AccountModel,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding
presentationContextProvider: ASWebAuthenticationPresentationContextProviding,
modelContext: ModelContext
) async throws -> AccountModel? {
let client = PixelfedClient(baseURL: accountModel.serverUrl)
@ -206,16 +214,17 @@ public class AuthorizationService {
return await self.update(accountId: accountModel.id,
basedOn: account,
accessToken: oAuthSwiftCredential.oauthToken,
refreshToken: oAuthSwiftCredential.oauthRefreshToken)
refreshToken: oAuthSwiftCredential.oauthRefreshToken,
modelContext: modelContext)
}
private func update(accountId: String,
basedOn account: Account,
accessToken: String,
refreshToken: String?
refreshToken: String?,
modelContext: ModelContext
) async -> AccountModel? {
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountId, viewContext: backgroundContext) else {
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return nil
}
@ -247,16 +256,19 @@ public class AuthorizationService {
}
// Save account data in database and in application state.
CoreDataHandler.shared.save(viewContext: backgroundContext)
try? modelContext.save()
return dbAccount.toAccountModel()
}
private func getAccountData(account: Account, backgroundContext: NSManagedObjectContext) -> AccountData {
if let accountFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) {
private func getAccountData(account: Account, serverUrl: URL, modelContext: ModelContext) -> AccountData {
if let accountFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, modelContext: modelContext) {
return accountFromDb
}
return AccountDataHandler.shared.createAccountDataEntity(viewContext: backgroundContext)
let accountData = AccountData(serverUrl: serverUrl)
modelContext.insert(accountData)
return accountData
}
}

View File

@ -5,7 +5,7 @@
//
import Foundation
import CoreData
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
@ -15,157 +15,49 @@ import EnvironmentKit
import Semaphore
/// Service responsible for managing home timeline.
@MainActor
public class HomeTimelineService {
public static let shared = HomeTimelineService()
private init() { }
private let defaultAmountOfDownloadedStatuses = 40
private let maximumAmountOfDownloadedStatuses = 80
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
private let semaphore = AsyncSemaphore(value: 1)
@MainActor
public func loadOnBottom(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool) async throws -> Int {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Get minimum downloaded stauts id.
let oldestStatus = StatusDataHandler.shared.getMinimumStatus(accountId: account.id, viewContext: backgroundContext)
guard let oldestStatus = oldestStatus else {
return 0
}
// Load data on bottom of the list.
let allStatusesFromApi = try await self.load(for: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext,
maxId: oldestStatus.id)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Return amount of newly downloaded statuses.
return allStatusesFromApi.count
}
@MainActor
public func refreshTimeline(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool, updateLastSeenStatus: Bool = false) async throws -> String? {
await semaphore.wait()
defer { semaphore.signal() }
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Retrieve newest visible status (last visible by user).
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
let lastSeenStatusId = dbNewestStatus?.id
// Refresh/load home timeline (refreshing on top downloads always first 40 items).
// When Apple introduce good way to show new items without scroll to top then we can change that method.
let allStatusesFromApi = try await self.refresh(for: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Update last seen status.
if let lastSeenStatusId, updateLastSeenStatus == true {
try self.update(lastSeenStatusId: lastSeenStatusId, for: account, on: backgroundContext)
}
// Delete old viewed statuses from database.
ViewedStatusHandler.shared.deleteOldViewedStatuses(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Return id of last seen status.
return lastSeenStatusId
}
@MainActor
public func update(attachment: AttachmentData, withData imageData: Data, imageWidth: Double, imageHeight: Double) {
attachment.data = imageData
attachment.metaImageWidth = Int32(imageWidth)
attachment.metaImageHeight = Int32(imageHeight)
// TODO: Uncomment/remove when exif metadata will be supported.
// self.setExifProperties(in: attachment, from: imageData)
// Save data into database.
CoreDataHandler.shared.save()
}
public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool) async -> Int {
public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool, modelContext: ModelContext) async -> Int {
await semaphore.wait()
defer { semaphore.signal() }
guard let accessToken = account.accessToken else {
return 0
}
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Get maximimum downloaded stauts id.
let newestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
guard let newestStatus else {
guard let lastSeenStatusId = self.getLastLoadedStatusId(accountId: account.id, modelContext: modelContext) else {
return 0
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
var statuses: [Status] = []
var newestStatusId = newestStatus.id
var newestStatusId = lastSeenStatusId
// There can be more then 80 newest statuses, that's why we have to sometimes send more then one request.
while true {
do {
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId,
limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs)
guard let firstStatus = downloadedStatuses.first else {
break
}
let visibleStatuses = self.getVisibleStatuses(accountId: account.id,
statuses: downloadedStatuses,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
modelContext: modelContext)
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
for status in statusesWithImagesOnly {
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: account.id, status: status, viewContext: backgroundContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
continue
}
// Same rebloged status has been already visible in current portion of data.
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
continue
}
// Same status has been already visible in current portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
statuses.append(status)
}
statuses.append(contentsOf: visibleStatuses)
newestStatusId = firstStatus.id
} catch {
@ -176,266 +68,58 @@ public class HomeTimelineService {
// Start prefetching images.
self.prefetch(statuses: statuses)
// Return number of new statuses not visible yet on the timeline.
return statuses.count
}
private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws {
// Save information about last seen status.
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
public func getVisibleStatuses(accountId: String, statuses: [Status], hideStatusesWithoutAlt: Bool, modelContext: ModelContext) -> [Status] {
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = statuses.getStatusesWithImagesOnly()
var visibleStatuses: [Status] = []
for status in statusesWithImagesOnly {
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: status, modelContext: modelContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext) == false else {
continue
}
// Same rebloged status has been already visible in current portion of data.
if let reblog = status.reblog, visibleStatuses.contains(where: { $0.reblog?.id == reblog.id || $0.id == reblog.id }) {
continue
}
visibleStatuses.append(status)
}
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
print("statuses: \(statuses.count), withImages: \(statusesWithImagesOnly.count), visible: \(visibleStatuses.count)")
return visibleStatuses
}
private func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Update status data in database.
self.copy(from: status, to: statusData, on: backgroundContext)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
return statusData
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext)
}
private func getLastLoadedStatusId(accountId: String, modelContext: ModelContext) -> String? {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
return accountData?.lastLoadedStatusId
}
private func refresh(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
// Retrieve statuses from API.
let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Update all existing statuses in database.
for status in statuses {
if let dbStatus = StatusDataHandler.shared.getStatusData(accountId: account.id, statusId: status.id, viewContext: backgroundContext) {
dbStatus.updateFrom(status)
}
}
// Add statuses which are not existing in database, but has been downloaded via API.
var statusesToAdd: [Status] = []
for status in statuses where StatusDataHandler.shared.getStatusData(accountId: account.id,
statusId: status.id,
viewContext: backgroundContext) == nil {
statusesToAdd.append(status)
}
// Collection with statuses to remove from database.
var dbStatusesToRemove: [StatusData] = []
let allDbStatuses = StatusDataHandler.shared.getAllStatuses(accountId: account.id, viewContext: backgroundContext)
// Find statuses to delete (not exiting in the API results).
for dbStatus in allDbStatuses where !statuses.contains(where: { status in status.id == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
}
// Find statuses to delete (duplicates).
var existingStatusIds: [String] = []
for dbStatus in allDbStatuses {
if existingStatusIds.contains(where: { $0 == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
} else {
existingStatusIds.append(dbStatus.id)
}
}
// Delete statuses from database.
if !dbStatusesToRemove.isEmpty {
for dbStatusToRemove in dbStatusesToRemove {
backgroundContext.delete(dbStatusToRemove)
}
}
// Save statuses in database.
if !statusesToAdd.isEmpty {
_ = try await self.add(statusesToAdd, for: account, on: backgroundContext)
}
// Return all statuses downloaded from API.
return statuses
}
private func load(for account: AccountModel,
includeReblogs: Bool,
hideStatusesWithoutAlt: Bool,
on backgroundContext: NSManagedObjectContext,
maxId: String? = nil
) async throws -> [Status] {
// Retrieve statuses from API.
let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account,
maxId: maxId,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Save statuses in database.
try await self.add(statuses, for: account, on: backgroundContext)
// Return all statuses downloaded from API.
return statuses
}
private func add(_ statuses: [Status],
for account: AccountModel,
on backgroundContext: NSManagedObjectContext
) async throws {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
}
// Proceed statuses with images only.
let statusesWithImages = statuses.getStatusesWithImagesOnly()
// Save all data to database.
for status in statusesWithImages {
// Save status to database.
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
self.copy(from: status, to: statusData, on: backgroundContext)
statusData.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToStatuses(statusData)
// Save statusId to viewed statuses.
let viewedStatus = ViewedStatusHandler.shared.createViewedStatusEntity(viewContext: backgroundContext)
viewedStatus.id = status.id
viewedStatus.reblogId = status.reblog?.id
viewedStatus.date = Date()
viewedStatus.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToViewedStatuses(viewedStatus)
}
}
private func copy(from status: Status,
to statusData: StatusData,
on backgroundContext: NSManagedObjectContext
) {
statusData.copyFrom(status)
for (index, attachment) in status.getAllImageMediaAttachments().enumerated() {
// Save attachment in database.
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext)
attachmentData.copyFrom(attachment)
attachmentData.statusId = statusData.id
attachmentData.order = Int32(index)
if attachmentData.isInserted {
attachmentData.statusRelation = statusData
statusData.addToAttachmentsRelation(attachmentData)
}
}
}
private func setExifProperties(in attachmentData: AttachmentData, from imageData: Data) {
// Read exif information.
if let exifProperties = imageData.getExifData() {
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
attachmentData.exifCamera = "\(make) \(model)"
}
// "Lens" or "Lens Model"
if let lens = exifProperties.getExifValue("Lens") {
attachmentData.exifLens = lens
}
if let createData = exifProperties.getExifValue("CreateDate") {
attachmentData.exifCreatedDate = createData
}
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
let exposureTime = exifProperties.getExifValue("ExposureTime"),
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
}
}
}
private func prefetch(statuses: [Status]) {
let statusModels = statuses.toStatusModels()
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, on backgroundContext: NSManagedObjectContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, viewContext: backgroundContext)
}
private func getUniqueStatusesForHomeTimeline(account: AccountModel,
maxId: EntityId? = nil,
includeReblogs: Bool? = nil,
hideStatusesWithoutAlt: Bool = false,
on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
guard let accessToken = account.accessToken else {
return []
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
var lastStatusId = maxId
var statuses: [Status] = []
while true {
let downloadedStatuses = try await client.getHomeTimeline(maxId: lastStatusId,
limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs)
// When there is not any older statuses we have to finish.
guard let lastStatus = downloadedStatuses.last else {
break
}
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
for status in statusesWithImagesOnly {
// When we process default amount of statuses to show we can stop adding another ones to the list.
if statuses.count == self.defaultAmountOfDownloadedStatuses {
break
}
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: account.id, status: status, viewContext: backgroundContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
continue
}
// Same rebloged status has been already visible in current portion of data.
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
continue
}
// Same status has been already visible in current portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
statuses.append(status)
}
if statuses.count >= self.defaultAmountOfDownloadedStatuses {
break
}
lastStatusId = lastStatus.id
}
return statuses
}
}

View File

@ -69,13 +69,13 @@ enum AlertDestinations: Identifiable {
}
@MainActor
class RouterPath: ObservableObject {
@Observable class RouterPath {
public var urlHandler: ((URL) -> OpenURLAction.Result)?
@Published public var path: [RouteurDestinations] = []
@Published public var presentedSheet: SheetDestinations?
@Published public var presentedOverlay: OverlayDestinations?
@Published public var presentedAlert: AlertDestinations?
public var path: [RouteurDestinations] = []
public var presentedSheet: SheetDestinations?
public var presentedOverlay: OverlayDestinations?
public var presentedAlert: AlertDestinations?
public init() {}

View File

@ -10,27 +10,27 @@ import NukeUI
import ClientKit
import EnvironmentKit
import WidgetKit
import SwiftData
@main
struct VernissageApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let coreDataHandler = CoreDataHandler.shared
@StateObject var applicationState = ApplicationState.shared
@StateObject var client = Client.shared
@StateObject var routerPath = RouterPath()
@StateObject var tipsStore = TipsStore()
@State var applicationState = ApplicationState.shared
@State var client = Client.shared
@State var routerPath = RouterPath()
@State var tipsStore = TipsStore()
@State var applicationViewMode: ApplicationViewMode = .loading
@State var tintColor = ApplicationState.shared.tintColor.color()
@State var theme = ApplicationState.shared.theme.colorScheme()
let modelContainer = SwiftDataHandler.shared.sharedModelContainer
let timer = Timer.publish(every: 120, on: .main, in: .common).autoconnect()
var body: some Scene {
WindowGroup {
NavigationStack(path: $routerPath.path) {
NavigationStack {
switch applicationViewMode {
case .loading:
LoadingView()
@ -50,17 +50,16 @@ struct VernissageApp: App {
.withAlertDestinations(alertDestinations: $routerPath.presentedAlert)
}
}
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
.environmentObject(applicationState)
.environmentObject(client)
.environmentObject(routerPath)
.environmentObject(tipsStore)
.modelContainer(modelContainer)
.environment(applicationState)
.environment(client)
.environment(routerPath)
.environment(tipsStore)
.tint(self.tintColor)
.preferredColorScheme(self.theme)
.task {
await self.onApplicationStart()
}
.navigationViewStyle(.stack)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Task {
@ -78,24 +77,24 @@ struct VernissageApp: App {
await self.calculateNewPhotosInBackground()
}
}
.onChange(of: applicationState.theme) { newValue in
.onChange(of: applicationState.theme) { oldValue, newValue in
self.theme = newValue.colorScheme()
}
.onChange(of: applicationState.tintColor) { newValue in
.onChange(of: applicationState.tintColor) { oldValue, newValue in
self.tintColor = newValue.color()
}
.onChange(of: applicationState.account) { newValue in
.onChange(of: applicationState.account) { oldValue, newValue in
if newValue == nil {
self.applicationViewMode = .signIn
}
}
.onChange(of: applicationState.showStatusId) { newValue in
.onChange(of: applicationState.showStatusId) { oldValue, newValue in
if let statusId = newValue {
self.routerPath.navigate(to: .status(id: statusId))
self.applicationState.showStatusId = nil
}
}
.onChange(of: applicationState.showAccountId) { newValue in
.onChange(of: applicationState.showAccountId) { oldValue, newValue in
if let accountId = newValue {
self.routerPath.navigate(to: .userProfile(accountId: accountId, accountDisplayName: nil, accountUserName: ""))
self.applicationState.showAccountId = nil
@ -119,7 +118,8 @@ struct VernissageApp: App {
await self.refreshAccessTokens()
// When user doesn't exists then we have to open sign in view.
guard let currentAccount = AccountDataHandler.shared.getCurrentAccountData() else {
let modelContext = self.modelContainer.mainContext
guard let currentAccount = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
self.applicationViewMode = .signIn
return
}
@ -129,7 +129,9 @@ struct VernissageApp: App {
// Verify access token correctness.
let authorizationSession = AuthorizationSession()
await AuthorizationService.shared.verifyAccount(session: authorizationSession, accountModel: accountModel) { signedInAccountModel in
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
accountModel: accountModel,
modelContext: modelContext) { signedInAccountModel in
guard let signedInAccountModel else {
self.applicationViewMode = .signIn
return
@ -162,7 +164,8 @@ struct VernissageApp: App {
}
private func loadUserPreferences() {
ApplicationSettingsHandler.shared.update(applicationState: self.applicationState)
let modelContext = self.modelContainer.mainContext
ApplicationSettingsHandler.shared.update(applicationState: self.applicationState, modelContext: modelContext)
self.tintColor = self.applicationState.tintColor.color()
self.theme = self.applicationState.theme.colorScheme()
@ -187,7 +190,8 @@ struct VernissageApp: App {
}
private func refreshAccessTokens() async {
let defaultSettings = ApplicationSettingsHandler.shared.get()
let modelContext = self.modelContainer.mainContext
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
// Run refreshing access tokens once per day.
guard let refreshTokenDate = Calendar.current.date(byAdding: .day, value: 1, to: defaultSettings.lastRefreshTokens), refreshTokenDate < Date.now else {
@ -195,18 +199,23 @@ struct VernissageApp: App {
}
// Refresh access tokens.
await AuthorizationService.shared.refreshAccessTokens()
await AuthorizationService.shared.refreshAccessTokens(modelContext: modelContext)
// Update time when refresh tokens has been updated.
defaultSettings.lastRefreshTokens = Date.now
CoreDataHandler.shared.save()
try? modelContext.save()
}
private func calculateNewPhotosInBackground() async {
let modelContext = self.modelContainer.mainContext
if let account = self.applicationState.account {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt)
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext
)
}
}
}

View File

@ -20,22 +20,11 @@ public extension View {
)
)
}
func imageContextMenu(statusData: StatusData, attachmentData: AttachmentData, uiImage: UIImage?) -> some View {
modifier(
ImageContextMenu(
id: statusData.getOrginalStatusId(),
url: statusData.url,
altText: attachmentData.text,
uiImage: uiImage
)
)
}
}
private struct ImageContextMenu: ViewModifier {
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
private let id: String
private let url: URL?

View File

@ -9,6 +9,7 @@ import SwiftUI
import EnvironmentKit
import ServicesKit
@MainActor
extension View {
func navigationMenuButtons(menuPosition: Binding<MenuPosition>,
onViewModeIconTap: @escaping (MainView.ViewMode) -> Void) -> some View {
@ -16,8 +17,10 @@ extension View {
}
}
@MainActor
private struct NavigationMenuButtons: ViewModifier {
@EnvironmentObject var routerPath: RouterPath
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
private let onViewModeIconTap: (MainView.ViewMode) -> Void
private let imageFontSize = 20.0
@ -181,11 +184,11 @@ private struct NavigationMenuButtons: ViewModifier {
// Saving in core data.
switch displayedCustomMenuItem.position {
case 1:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: item.viewMode.rawValue, modelContext: modelContext)
case 2:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: item.viewMode.rawValue, modelContext: modelContext)
case 3:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: item.viewMode.rawValue, modelContext: modelContext)
default:
break
}
@ -198,7 +201,7 @@ private struct NavigationMenuButtons: ViewModifier {
}
private func loadCustomMenuItems() {
let applicationSettings = ApplicationSettingsHandler.shared.get()
let applicationSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
self.setCustomMenuItem(position: 1, viewMode: MainView.ViewMode(rawValue: Int(applicationSettings.customNavigationMenuItem1)) ?? .home)
self.setCustomMenuItem(position: 2, viewMode: MainView.ViewMode(rawValue: Int(applicationSettings.customNavigationMenuItem2)) ?? .local)

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct AccountsPhotoView: View {
public enum ListType: Hashable {
case trending
@ -27,9 +28,9 @@ struct AccountsPhotoView: View {
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State public var listType: ListType

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct AccountsView: View {
public enum ListType: Hashable {
case followers(entityId: String)
@ -42,8 +43,8 @@ struct AccountsView: View {
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@State var listType: ListType

View File

@ -8,6 +8,7 @@ import Foundation
import UIKit
import SwiftUI
@MainActor
struct ActivityView: UIViewControllerRepresentable {
let image: UIImage

View File

@ -13,9 +13,10 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct ComposeView: View {
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@Environment(\.dismiss) private var dismiss
@ -26,6 +27,8 @@ struct ComposeView: View {
}
var body: some View {
@Bindable var routerPath = routerPath
NavigationView {
BaseComposeView(statusViewModel: self.statusViewModel) {
dismiss()

View File

@ -13,9 +13,12 @@ import ServicesKit
import WidgetsKit
import EnvironmentKit
@MainActor
struct EditProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State private var account: Account?
@ -93,7 +96,7 @@ struct EditProfileView: View {
}
}
}
.onChange(of: self.selectedItems) { _ in
.onChange(of: self.selectedItems) {
Task {
await self.getAvatar()
}
@ -168,9 +171,9 @@ struct EditProfileView: View {
private func formView() -> some View {
Section {
TextField("", text: $displayName)
.onChange(of: self.displayName, perform: { _ in
.onChange(of: self.displayName) {
self.displayName = String(self.displayName.prefix(self.displayNameMaxLength))
})
}
} header: {
Text("editProfile.title.displayName", comment: "Display name")
} footer: {
@ -183,9 +186,9 @@ struct EditProfileView: View {
Section {
TextField("", text: $bio, axis: .vertical)
.lineLimit(5, reservesSpace: true)
.onChange(of: self.bio, perform: { _ in
.onChange(of: self.bio) {
self.bio = String(self.bio.prefix(self.bioMaxLength))
})
}
} header: {
Text("editProfile.title.bio", comment: "Bio")
} footer: {
@ -200,9 +203,9 @@ struct EditProfileView: View {
.autocapitalization(.none)
.keyboardType(.URL)
.autocorrectionDisabled()
.onChange(of: self.website, perform: { _ in
.onChange(of: self.website) {
self.website = String(self.website.prefix(self.websiteMaxLength))
})
}
} header: {
Text("editProfile.title.website", comment: "Website")
} footer: {
@ -245,10 +248,10 @@ struct EditProfileView: View {
if let avatarData = self.avatarData {
_ = try await self.client.accounts?.avatar(image: avatarData)
if let accountData = AccountDataHandler.shared.getAccountData(accountId: account.id) {
if let accountData = AccountDataHandler.shared.getAccountData(accountId: account.id, modelContext: modelContext) {
accountData.avatarData = avatarData
self.applicationState.account?.avatarData = avatarData
CoreDataHandler.shared.save()
try modelContext.save()
}
}

View File

@ -12,10 +12,11 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct FollowRequestsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var accounts: [Account] = []
@State private var downloadedPage = 1

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct HashtagsView: View {
public enum ListType: Hashable {
case trending
@ -30,9 +31,9 @@ struct HashtagsView: View {
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State public var listType: ListType

View File

@ -1,222 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import ServicesKit
import EnvironmentKit
import WidgetsKit
import OSLog
import Semaphore
struct HomeFeedView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@State private var allItemsLoaded = false
@State private var state: ViewState = .loading
@State private var opacity = 0.0
@State private var offset = -50.0
@FetchRequest var dbStatuses: FetchedResults<StatusData>
init(accountId: String) {
_dbStatuses = FetchRequest<StatusData>(
sortDescriptors: [SortDescriptor(\.id, order: .reverse)],
predicate: NSPredicate(format: "pixelfedAccount.id = %@", accountId))
}
var body: some View {
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData()
}
case .loaded:
if self.dbStatuses.isEmpty {
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "home.title.noPhotos")
} else {
self.timeline()
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
await self.loadData()
}
.padding()
}
}
@ViewBuilder
private func timeline() -> some View {
ZStack {
ScrollView {
LazyVStack {
ForEach(dbStatuses, id: \.self) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
ImageRow(statusData: item)
}
if allItemsLoaded == false {
LoadingIndicator()
.task {
do {
if let account = self.applicationState.account {
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(
for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt
)
if newStatusesCount == 0 {
allItemsLoaded = true
}
}
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
}
}
}
}
}
self.newPhotosView()
.offset(y: self.offset)
.opacity(self.opacity)
}
.refreshable {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
await self.refreshData()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
}
.onChange(of: self.applicationState.amountOfNewStatuses) { _ in
self.calculateOffset()
}.onAppear {
self.calculateOffset()
}
}
private func refreshData() async {
do {
if let account = self.applicationState.account {
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
updateLastSeenStatus: true)
asyncAfter(0.75) {
self.applicationState.lastSeenStatusId = lastSeenStatusId
self.applicationState.amountOfNewStatuses = 0
}
}
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
}
}
private func loadData() async {
do {
// We have to load data automatically only when the database is empty.
guard self.dbStatuses.isEmpty else {
withAnimation {
self.state = .loaded
}
return
}
if let account = self.applicationState.account {
_ = try await HomeTimelineService.shared.refreshTimeline(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt)
}
self.applicationState.amountOfNewStatuses = 0
self.state = .loaded
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: true)
self.state = .error(error)
} else {
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: false)
}
}
}
private func calculateOffset() {
if self.applicationState.amountOfNewStatuses > 0 {
withAnimation(.easeIn) {
self.showNewStatusesView()
}
} else {
withAnimation(.easeOut) {
self.hideNewStatusesView()
}
}
}
private func showNewStatusesView() {
self.offset = 0.0
self.opacity = 1.0
}
private func hideNewStatusesView() {
self.offset = -50.0
self.opacity = 0.0
}
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
return self.applicationState.lastSeenStatusId != dbStatuses.first?.id && self.applicationState.lastSeenStatusId == statusId
}
@ViewBuilder
private func upToDatePlaceholder() -> some View {
VStack(alignment: .center) {
Image(systemName: "checkmark.seal")
.resizable()
.frame(width: 64, height: 64)
.fontWeight(.ultraLight)
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
Text("home.title.allCaughtUp", comment: "You're all caught up")
.font(.title2)
.fontWeight(.thin)
.foregroundColor(Color.mainTextColor.opacity(0.6))
}
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 0.75)
}
@ViewBuilder
private func newPhotosView() -> some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer()
HStack {
Image(systemName: "arrow.up")
.fontWeight(.light)
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold)
}
.padding(.vertical, 12)
.padding(.horizontal, 18)
.font(.callout)
.foregroundColor(Color.mainTextColor)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
Spacer()
}
.padding(.top, 10)
.padding(.trailing, 6)
}
}

View File

@ -0,0 +1,328 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import Nuke
import PixelfedKit
import ClientKit
import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct HomeTimelineView: View {
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State private var allItemsLoaded = false
@State private var statusViewModels: [StatusModel] = []
@State private var state: ViewState = .loading
@State private var lastStatusId: String?
@State private var opacity = 0.0
@State private var offset = -50.0
private let defaultLimit = 80
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
var body: some View {
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData()
}
case .loaded:
if self.statusViewModels.isEmpty {
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "statuses.title.noPhotos")
} else {
self.list()
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
await self.loadData()
}
.padding()
}
}
@ViewBuilder
private func list() -> some View {
ZStack {
ScrollView {
LazyVStack(alignment: .center) {
ForEach(self.statusViewModels, id: \.id) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width))
}
if allItemsLoaded == false {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
Spacer()
}
}
}
}
self.newPhotosView()
.offset(y: self.offset)
.opacity(self.opacity)
}
.refreshable {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
.onChange(of: self.applicationState.showReboostedStatuses) {
Task {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
}
.onChange(of: self.applicationState.amountOfNewStatuses) {
self.calculateOffset()
}.onAppear {
self.calculateOffset()
}
}
@ViewBuilder
private func upToDatePlaceholder() -> some View {
VStack(alignment: .center) {
Image(systemName: "checkmark.seal")
.resizable()
.frame(width: 64, height: 64)
.fontWeight(.ultraLight)
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
Text("home.title.allCaughtUp", comment: "You're all caught up")
.font(.title2)
.fontWeight(.thin)
.foregroundColor(Color.mainTextColor.opacity(0.6))
}
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 0.75)
}
@ViewBuilder
private func newPhotosView() -> some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer()
HStack {
Image(systemName: "arrow.up")
.fontWeight(.light)
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold)
}
.padding(.vertical, 12)
.padding(.horizontal, 18)
.font(.callout)
.foregroundColor(Color.mainTextColor)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
Spacer()
}
.padding(.top, 10)
.padding(.trailing, 6)
}
private func loadData() async {
do {
try await self.loadFirstStatuses()
try ViewedStatusHandler.shared.deleteOldViewedStatuses(modelContext: modelContext)
self.state = .loaded
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
self.state = .error(error)
}
}
private func loadFirstStatuses() async throws {
guard let accountId = self.applicationState.account?.id else {
return
}
// Download statuses from API.
let statuses = try await self.loadFromApi()
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: nil, lastLoadedStatusId: statuses.first?.id, accountId: accountId, modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Append to empty list.
self.statusViewModels.append(contentsOf: statusModels)
}
private func loadMoreStatuses() async throws {
if let lastStatusId = self.lastStatusId, let accountId = self.applicationState.account?.id {
// Download statuses from API.
let statuses = try await self.loadFromApi(maxId: lastStatusId)
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Now we have new last status.
if let lastStatusId = statuses.last?.id {
self.lastStatusId = lastStatusId
}
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Append statuses to existing array of statuses (at the end).
self.statusViewModels.append(contentsOf: statusModels)
}
}
private func refreshStatuses() async throws {
guard let accountId = self.applicationState.account?.id else {
return
}
// Download statuses from API.
let statuses = try await self.loadFromApi()
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id, lastLoadedStatusId: statuses.first?.id, accountId: accountId, modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Replace old collection with new one.
self.statusViewModels = statusModels
}
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] {
return try await self.client.publicTimeline?.getHomeTimeline(
maxId: maxId,
sinceId: sinceId,
minId: minId,
limit: self.defaultLimit,
includeReblogs: self.applicationState.showReboostedStatuses) ?? []
}
private func calculateOffset() {
if self.applicationState.amountOfNewStatuses > 0 {
withAnimation(.easeIn) {
self.showNewStatusesView()
}
} else {
withAnimation(.easeOut) {
self.hideNewStatusesView()
}
}
}
private func showNewStatusesView() {
self.offset = 0.0
self.opacity = 1.0
}
private func hideNewStatusesView() {
self.offset = -50.0
self.opacity = 0.0
}
private func prefetch(statusModels: [StatusModel]) {
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
private func shouldHideStatusWithoutAlt(status: Status) -> Bool {
if self.applicationState.hideStatusesWithoutAlt == false {
return false
}
return status.statusContainsAltText() == false
}
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
return self.applicationState.lastSeenStatusId != statusViewModels.first?.id && self.applicationState.lastSeenStatusId == statusId
}
}

View File

@ -12,10 +12,11 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct InstanceView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var state: ViewState = .loading
@State private var instance: Instance?

View File

@ -6,6 +6,7 @@
import SwiftUI
@MainActor
struct LoadingView: View {
var body: some View {
VStack(alignment: .center) {

View File

@ -6,19 +6,20 @@
import SwiftUI
import UIKit
import CoreData
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
import EnvironmentKit
@MainActor
struct MainView: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.modelContext) private var modelContext
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var tipsStore: TipsStore
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@Environment(TipsStore.self) var tipsStore
@State private var navBarTitle: LocalizedStringKey = ViewMode.home.title
@State private var viewMode: ViewMode = .home {
@ -26,8 +27,8 @@ struct MainView: View {
self.navBarTitle = viewMode.title
}
}
@FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults<AccountData>
@Query(sort: \AccountData.acct, order: .forward) var dbAccounts: [AccountData]
public enum ViewMode: Int {
case home = 1
@ -88,28 +89,33 @@ struct MainView: View {
}
var body: some View {
self.getMainView()
.navigationMenuButtons(menuPosition: $applicationState.menuPosition) { viewMode in
self.switchView(to: viewMode)
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
@Bindable var applicationState = applicationState
@Bindable var routerPath = routerPath
if self.applicationState.menuPosition == .top {
self.getPrincipalToolbar()
self.getTrailingToolbar()
NavigationStack(path: $routerPath.path) {
self.getMainView()
.navigationMenuButtons(menuPosition: $applicationState.menuPosition) { viewMode in
self.switchView(to: viewMode)
}
}
.onChange(of: tipsStore.status) { status in
if status == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
if self.applicationState.menuPosition == .top {
self.getPrincipalToolbar()
self.getTrailingToolbar()
}
}
}
.onChange(of: tipsStore.status) { oldStatus, newStatus in
if newStatus == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
}
}
}
}
}
@ViewBuilder
@ -117,7 +123,7 @@ struct MainView: View {
switch self.viewMode {
case .home:
if UIDevice.isIPhone {
HomeFeedView(accountId: applicationState.account?.id ?? String.empty())
HomeTimelineView()
.id(applicationState.account?.id ?? String.empty())
} else {
StatusesView(listType: .home)
@ -282,9 +288,11 @@ struct MainView: View {
let authorizationSession = AuthorizationSession()
let accountModel = account.toAccountModel()
await AuthorizationService.shared.verifyAccount(session: authorizationSession, accountModel: accountModel) { signedInAccountModel in
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
accountModel: accountModel,
modelContext: modelContext) { signedInAccountModel in
guard let signedInAccountModel else {
ToastrService.shared.showError(subtitle: NSLocalizedString("mainview.error.switchAccounts", comment: "Cannot switch accounts."))
ToastrService.shared.showError(title: "", subtitle: NSLocalizedString("mainview.error.switchAccounts", comment: "Cannot switch accounts."))
return
}
@ -300,7 +308,7 @@ struct MainView: View {
lastSeenStatusId: signedInAccountModel.lastSeenStatusId)
// Set account as default (application will open this account after restart).
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id)
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id, modelContext: modelContext)
}
}
}

View File

@ -11,9 +11,10 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct NotificationsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@State var accountId: String
@State private var notifications: [PixelfedKit.Notification] = []

View File

@ -13,9 +13,9 @@ import EnvironmentKit
import WidgetsKit
struct NotificationRowView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var image: SwiftUI.Image?

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct PaginableStatusesView: View {
public enum ListType: Hashable {
case favourites
@ -27,9 +28,9 @@ struct PaginableStatusesView: View {
}
}
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State public var listType: ListType

View File

@ -10,8 +10,9 @@ import ClientKit
import ServicesKit
import WidgetsKit
@MainActor
struct ReportView: View {
@EnvironmentObject private var client: Client
@Environment(Client.self) var client
@Environment(\.dismiss) private var dismiss
@State private var publishDisabled = false

View File

@ -6,8 +6,9 @@
import SwiftUI
@MainActor
struct SearchView: View {
@EnvironmentObject var routerPath: RouterPath
@Environment(RouterPath.self) var routerPath
@State private var query = String.empty()

View File

@ -7,10 +7,11 @@
import SwiftUI
import EnvironmentKit
@MainActor
struct SettingsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var tipsStore: TipsStore
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(TipsStore.self) var tipsStore
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) private var dismiss
@ -20,6 +21,9 @@ struct SettingsView: View {
@State private var appBundleVersion: String?
var body: some View {
@Bindable var routerPath = routerPath
@Bindable var tipsStore = tipsStore
NavigationStack {
NavigationView {
List {
@ -75,17 +79,17 @@ struct SettingsView: View {
.withAppRouteur()
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
}
.onChange(of: self.applicationState.theme) { _ in
.onChange(of: self.applicationState.theme) {
// Change theme of current modal screen (unformtunatelly it's not changed autmatically.
self.theme = self.applicationState.theme.colorScheme() ?? self.getSystemColorScheme()
}
.onChange(of: applicationState.account) { newValue in
.onChange(of: applicationState.account) { oldValue, newValue in
if newValue == nil {
self.dismiss()
}
}
.onChange(of: tipsStore.status) { status in
if status == .successful {
.onChange(of: tipsStore.status) { oldStatus, newStatus in
if newStatus == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()

View File

@ -8,7 +8,8 @@ import SwiftUI
import EnvironmentKit
struct AccentsSectionView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(ApplicationState.self) var applicationState
@Environment(\.modelContext) private var modelContext
private let accentColors1: [TintColor] = [.accentColor1, .accentColor2, .accentColor3, .accentColor4, .accentColor5]
private let accentColors2: [TintColor] = [.accentColor6, .accentColor7, .accentColor8, .accentColor9, .accentColor10]
@ -24,7 +25,7 @@ struct AccentsSectionView: View {
.frame(width: 36, height: 36)
.onTapGesture {
self.applicationState.tintColor = color
ApplicationSettingsHandler.shared.set(tintColor: color)
ApplicationSettingsHandler.shared.set(tintColor: color, modelContext: modelContext)
}
if color == self.applicationState.tintColor {
Image(systemName: "checkmark")
@ -48,7 +49,7 @@ struct AccentsSectionView: View {
.frame(width: 36, height: 36)
.onTapGesture {
self.applicationState.tintColor = color
ApplicationSettingsHandler.shared.set(tintColor: color)
ApplicationSettingsHandler.shared.set(tintColor: color, modelContext: modelContext)
}
if color == self.applicationState.tintColor {
Image(systemName: "checkmark")

View File

@ -10,8 +10,9 @@ import EnvironmentKit
import WidgetsKit
struct AccountsSectionView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State private var accounts: [AccountModel] = []
@State private var dbAccounts: [AccountData] = []
@ -43,7 +44,7 @@ struct AccountsSectionView: View {
}
}
.onAppear {
self.dbAccounts = AccountDataHandler.shared.getAccountsData()
self.dbAccounts = AccountDataHandler.shared.getAccountsData(modelContext: modelContext)
self.accounts = self.dbAccounts.map({ $0.toAccountModel() })
}
}
@ -64,7 +65,7 @@ struct AccountsSectionView: View {
}
if let dbAccount = self.dbAccounts.first(where: {$0.id == account.id }) {
AccountDataHandler.shared.remove(accountData: dbAccount)
AccountDataHandler.shared.remove(accountData: dbAccount, modelContext: modelContext)
}
}
@ -75,7 +76,7 @@ struct AccountsSectionView: View {
if shouldClearApplicationState {
// We have to do this after animation of deleting row is ended.
self.asyncAfter(0.5) {
ApplicationSettingsHandler.shared.set(accountId: nil)
ApplicationSettingsHandler.shared.set(accountId: nil, modelContext: modelContext)
self.applicationState.clearApplicationState()
self.client.clearAccount()
}

View File

@ -8,13 +8,14 @@ import SwiftUI
import EnvironmentKit
struct AvatarShapesSectionView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(ApplicationState.self) var applicationState
@Environment(\.modelContext) private var modelContext
var body: some View {
Section("settings.title.avatar") {
Button {
self.applicationState.avatarShape = .circle
ApplicationSettingsHandler.shared.set(avatarShape: .circle)
ApplicationSettingsHandler.shared.set(avatarShape: .circle, modelContext: modelContext)
} label: {
HStack {
Image("Avatar")
@ -36,7 +37,7 @@ struct AvatarShapesSectionView: View {
Button {
self.applicationState.avatarShape = .roundedRectangle
ApplicationSettingsHandler.shared.set(avatarShape: .roundedRectangle)
ApplicationSettingsHandler.shared.set(avatarShape: .roundedRectangle, modelContext: modelContext)
} label: {
HStack {
Image("Avatar")

View File

@ -8,7 +8,8 @@ import SwiftUI
import EnvironmentKit
struct GeneralSectionView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(ApplicationState.self) var applicationState
@Environment(\.modelContext) private var modelContext
private let customIconNames = ["Default",
"Blue",
@ -41,8 +42,9 @@ struct GeneralSectionView: View {
]
var body: some View {
@Bindable var applicationState = applicationState
Section("settings.title.general") {
// Application icon.
Picker(selection: $applicationState.activeIcon) {
ForEach(self.customIconNames, id: \.self) { icon in
@ -57,9 +59,9 @@ struct GeneralSectionView: View {
Text("settings.title.applicationIcon", comment: "Application icon")
}
.pickerStyle(.navigationLink)
.onChange(of: self.applicationState.activeIcon) { iconName in
ApplicationSettingsHandler.shared.set(activeIcon: iconName)
UIApplication.shared.setAlternateIconName(iconName == "Default" ? nil : iconName)
.onChange(of: self.applicationState.activeIcon) { oldIncomeName, newIconName in
ApplicationSettingsHandler.shared.set(activeIcon: newIconName, modelContext: modelContext)
UIApplication.shared.setAlternateIconName(newIconName == "Default" ? nil : newIconName)
}
// Application theme.
@ -71,8 +73,8 @@ struct GeneralSectionView: View {
} label: {
Text("settings.title.theme", comment: "Theme")
}
.onChange(of: self.applicationState.theme) { theme in
ApplicationSettingsHandler.shared.set(theme: theme)
.onChange(of: self.applicationState.theme) { oldTheme, newTheme in
ApplicationSettingsHandler.shared.set(theme: newTheme, modelContext: modelContext)
}
// Menu position.
@ -84,8 +86,8 @@ struct GeneralSectionView: View {
} label: {
Text("settings.title.menuPosition", comment: "Menu position")
}
.onChange(of: self.applicationState.menuPosition) { menuPosition in
ApplicationSettingsHandler.shared.set(menuPosition: menuPosition)
.onChange(of: self.applicationState.menuPosition) { oldMenuPosition, newMenuPosition in
ApplicationSettingsHandler.shared.set(menuPosition: newMenuPosition, modelContext: modelContext)
}
}
}

View File

@ -8,7 +8,9 @@ import SwiftUI
import EnvironmentKit
struct HapticsSectionView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(ApplicationState.self) var applicationState
@Environment(\.modelContext) private var modelContext
@Environment(\.colorScheme) var colorScheme
@State var hapticTabSelectionEnabled = true
@ -21,27 +23,27 @@ struct HapticsSectionView: View {
Section("settings.title.haptics") {
Toggle("settings.title.hapticsTabSelection", isOn: $hapticTabSelectionEnabled)
.onChange(of: hapticTabSelectionEnabled) { newValue in
.onChange(of: hapticTabSelectionEnabled) { oldValue, newValue in
self.applicationState.hapticTabSelectionEnabled = newValue
ApplicationSettingsHandler.shared.set(hapticTabSelectionEnabled: newValue)
ApplicationSettingsHandler.shared.set(hapticTabSelectionEnabled: newValue, modelContext: modelContext)
}
Toggle("settings.title.hapticsButtonPress", isOn: $hapticButtonPressEnabled)
.onChange(of: hapticButtonPressEnabled) { newValue in
.onChange(of: hapticButtonPressEnabled) { oldValue, newValue in
self.applicationState.hapticButtonPressEnabled = newValue
ApplicationSettingsHandler.shared.set(hapticButtonPressEnabled: newValue)
ApplicationSettingsHandler.shared.set(hapticButtonPressEnabled: newValue, modelContext: modelContext)
}
Toggle("settings.title.hapticsListRefresh", isOn: $hapticRefreshEnabled)
.onChange(of: hapticRefreshEnabled) { newValue in
.onChange(of: hapticRefreshEnabled) { oldValue, newValue in
self.applicationState.hapticRefreshEnabled = newValue
ApplicationSettingsHandler.shared.set(hapticRefreshEnabled: newValue)
ApplicationSettingsHandler.shared.set(hapticRefreshEnabled: newValue, modelContext: modelContext)
}
Toggle("settings.title.hapticsAnimationFinished", isOn: $hapticAnimationEnabled)
.onChange(of: hapticAnimationEnabled) { newValue in
.onChange(of: hapticAnimationEnabled) { oldValue, newValue in
self.applicationState.hapticAnimationEnabled = newValue
ApplicationSettingsHandler.shared.set(hapticAnimationEnabled: newValue)
ApplicationSettingsHandler.shared.set(hapticAnimationEnabled: newValue, modelContext: modelContext)
}
// Toggle("Notification", isOn: $hapticNotificationEnabled)
@ -51,7 +53,7 @@ struct HapticsSectionView: View {
// }
}
.onAppear {
let defaultSettings = ApplicationSettingsHandler.shared.get()
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
self.hapticTabSelectionEnabled = defaultSettings.hapticTabSelectionEnabled
self.hapticButtonPressEnabled = defaultSettings.hapticButtonPressEnabled
self.hapticRefreshEnabled = defaultSettings.hapticRefreshEnabled

View File

@ -8,10 +8,13 @@ import SwiftUI
import EnvironmentKit
struct MediaSettingsView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(ApplicationState.self) var applicationState
@Environment(\.colorScheme) var colorScheme
@Environment(\.modelContext) private var modelContext
var body: some View {
@Bindable var applicationState = applicationState
Section("settings.title.mediaSettings") {
Toggle(isOn: $applicationState.showSensitive) {
@ -22,8 +25,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showSensitive) { newValue in
ApplicationSettingsHandler.shared.set(showSensitive: newValue)
.onChange(of: self.applicationState.showSensitive) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showSensitive: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.showPhotoDescription) {
@ -34,8 +37,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showPhotoDescription) { newValue in
ApplicationSettingsHandler.shared.set(showPhotoDescription: newValue)
.onChange(of: self.applicationState.showPhotoDescription) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showPhotoDescription: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.showAvatarsOnTimeline) {
@ -46,8 +49,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue)
.onChange(of: self.applicationState.showAvatarsOnTimeline) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.showFavouritesOnTimeline) {
@ -58,8 +61,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showFavouritesOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showFavouritesOnTimeline: newValue)
.onChange(of: self.applicationState.showFavouritesOnTimeline) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showFavouritesOnTimeline: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.showAltIconOnTimeline) {
@ -70,8 +73,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showAltIconOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showAltIconOnTimeline: newValue)
.onChange(of: self.applicationState.showAltIconOnTimeline) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showAltIconOnTimeline: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.warnAboutMissingAlt) {
@ -82,8 +85,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.warnAboutMissingAlt) { newValue in
ApplicationSettingsHandler.shared.set(warnAboutMissingAlt: newValue)
.onChange(of: self.applicationState.warnAboutMissingAlt) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(warnAboutMissingAlt: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.showReboostedStatuses) {
@ -94,8 +97,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showReboostedStatuses) { newValue in
ApplicationSettingsHandler.shared.set(showReboostedStatuses: newValue)
.onChange(of: self.applicationState.showReboostedStatuses) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(showReboostedStatuses: newValue, modelContext: modelContext)
}
Toggle(isOn: $applicationState.hideStatusesWithoutAlt) {
@ -106,8 +109,8 @@ struct MediaSettingsView: View {
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.hideStatusesWithoutAlt) { newValue in
ApplicationSettingsHandler.shared.set(hideStatusesWithoutAlt: newValue)
.onChange(of: self.applicationState.hideStatusesWithoutAlt) { oldValue, newValue in
ApplicationSettingsHandler.shared.set(hideStatusesWithoutAlt: newValue, modelContext: modelContext)
}
}
}

View File

@ -9,7 +9,7 @@ import StoreKit
import ServicesKit
struct SupportView: View {
@EnvironmentObject var tipsStore: TipsStore
@Environment(TipsStore.self) var tipsStore
var body: some View {
Section("settings.title.support") {

View File

@ -8,7 +8,7 @@ import SwiftUI
import ServicesKit
struct ThanksView: View {
@EnvironmentObject var routerPath: RouterPath
@Environment(RouterPath.self) var routerPath
var body: some View {
VStack {

View File

@ -12,12 +12,14 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct SignInView: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var serverAddress = String.empty()
@State private var instructionsUrlString: String?
@ -39,10 +41,10 @@ struct SignInView: View {
.keyboardType(.URL)
.disableAutocorrection(true)
.clearButton(text: $serverAddress)
Button(NSLocalizedString("signin.title.signIn", comment: "Sign in")) {
HapticService.shared.fireHaptic(of: .buttonPress)
let baseAddress = self.getServerAddress(uri: self.serverAddress)
self.signIn(baseAddress: baseAddress)
}
@ -51,7 +53,7 @@ struct SignInView: View {
}
}
.buttonStyle(PlainButtonStyle())
} header: {
Text("signin.title.enterServerAddress", comment: "Enter server address")
} footer: {
@ -64,7 +66,7 @@ struct SignInView: View {
}
}
}
Section("signin.title.chooseServer") {
if self.instances.isEmpty {
HStack {
@ -73,7 +75,7 @@ struct SignInView: View {
Spacer()
}
}
ForEach(self.instances.filter { self.serverAddress.isEmpty || $0.uri.contains(self.serverAddress) }, id: \.uri) { instance in
InstanceRowView(instance: instance) { uri in
let baseAddress = self.getServerAddress(uri: uri)
@ -95,12 +97,11 @@ struct SignInView: View {
Task {
do {
let authorizationSession = AuthorizationSession()
try await AuthorizationService.shared.sign(in: baseAddress, session: authorizationSession) { accountModel in
try await AuthorizationService.shared.sign(in: baseAddress,
session: authorizationSession,
modelContext: modelContext) { accountModel in
onSignedIn?(accountModel)
DispatchQueue.main.sync {
dismiss()
}
dismiss()
}
} catch let error as AuthorisationError {
ErrorService.shared.handle(error, localizedMessage: error.localizedDescription, showToastr: true)

View File

@ -11,7 +11,7 @@ import ServicesKit
import WidgetsKit
struct InstanceRowView: View {
@EnvironmentObject var routerPath: RouterPath
@Environment(RouterPath.self) var routerPath
private let instance: Instance
private let action: (String) -> Void

View File

@ -12,6 +12,7 @@ import ServicesKit
import WidgetsKit
import EnvironmentKit
@MainActor
struct StatusView: View {
struct TappedAttachment: Identifiable {
public let id: String
@ -19,10 +20,11 @@ struct StatusView: View {
public let imagePosition: Double
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State var statusId: String
@ -213,7 +215,6 @@ struct StatusView: View {
self.state = .loaded
} catch NetworkError.notSuccessResponse(let response) {
if response.statusCode() == HTTPStatusCode.notFound, let accountId = self.applicationState.account?.id {
StatusDataHandler.shared.remove(accountId: accountId, statusId: self.statusId)
ErrorService.shared.handle(NetworkError.notSuccessResponse(response), message: "status.error.notFound", showToastr: true)
self.dismiss()
}
@ -227,13 +228,6 @@ struct StatusView: View {
}
}
private func setAttachment(_ attachmentData: AttachmentData) {
exifCamera = attachmentData.exifCamera
exifExposure = attachmentData.exifExposure
exifCreatedDate = attachmentData.exifCreatedDate
exifLens = attachmentData.exifLens
}
private func getImageHeight() -> Double {
if let highestImageUrl = self.highestImageUrl, let imageSize = ImageSizeService.shared.get(for: highestImageUrl) {
let calculatedSize = ImageSizeService.shared.calculate(width: imageSize.width, height: imageSize.height)

View File

@ -11,8 +11,8 @@ import EnvironmentKit
import WidgetsKit
struct CommentBodyView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@State var statusViewModel: StatusModel

View File

@ -13,8 +13,8 @@ import WidgetsKit
struct CommentsSectionView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@State public var statusId: String
@State private var commentViewModels: [CommentModel]?
@ -54,7 +54,7 @@ struct CommentsSectionView: View {
}
}
}
.onChange(of: self.applicationState.newComment) { _ in
.onChange(of: self.applicationState.newComment) {
self.commentViewModels = nil
Task {
await self.loadComments()

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct StatusesView: View {
public enum ListType: Hashable {
case home
@ -39,10 +40,11 @@ struct StatusesView: View {
}
}
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State public var listType: ListType
@ -59,7 +61,7 @@ struct StatusesView: View {
@State private var containerWidth: Double = UIDevice.isIPad ? UIScreen.main.bounds.width / 3 : UIScreen.main.bounds.width
@State private var containerHeight: Double = UIDevice.isIPad ? UIScreen.main.bounds.height / 3 : UIScreen.main.bounds.height
private let defaultLimit = 40
private let defaultLimit = 80
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
var body: some View {
@ -137,20 +139,20 @@ struct StatusesView: View {
.refreshable {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.loadTopStatuses()
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
.onChange(of: self.applicationState.showReboostedStatuses) { _ in
.onChange(of: self.applicationState.showReboostedStatuses) {
if self.listType != .home {
return
}
Task { @MainActor in
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.loadTopStatuses()
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
}
}
@ -172,6 +174,10 @@ struct StatusesView: View {
}
private func loadStatuses() async throws {
guard let accountId = self.applicationState.account?.id else {
return
}
let statuses = try await self.loadFromApi()
if statuses.isEmpty {
@ -182,68 +188,71 @@ struct StatusesView: View {
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only statuses with images.
var inPlaceStatuses: [StatusModel] = []
for item in statuses.getStatusesWithImagesOnly() {
// We have to hide statuses without ALT text.
if self.shouldHideStatusWithoutAlt(status: item) {
continue
}
// We have to skip statuses that are boosted from muted accounts.
if let accountId = self.applicationState.account?.id, AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: item) {
continue
}
inPlaceStatuses.append(StatusModel(status: item))
}
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: nil, lastLoadedStatusId: statuses.first?.id, accountId: accountId, modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
}
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
self.prefetch(statusModels: statusModels)
// Append to empty list.
self.statusViewModels.append(contentsOf: inPlaceStatuses)
self.statusViewModels.append(contentsOf: statusModels)
}
private func loadMoreStatuses() async throws {
if let lastStatusId = self.lastStatusId {
let previousStatuses = try await self.loadFromApi(maxId: lastStatusId)
if let lastStatusId = self.lastStatusId, let accountId = self.applicationState.account?.id {
let statuses = try await self.loadFromApi(maxId: lastStatusId)
if previousStatuses.isEmpty {
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Now we have new last status.
if let lastStatusId = previousStatuses.last?.id {
if let lastStatusId = statuses.last?.id {
self.lastStatusId = lastStatusId
}
// Get only statuses with images.
var inPlaceStatuses: [StatusModel] = []
for item in previousStatuses.getStatusesWithImagesOnly() {
// We have to hide statuses without ALT text.
if self.shouldHideStatusWithoutAlt(status: item) {
continue
}
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// We have to skip statuses that are boosted from muted accounts.
if let accountId = self.applicationState.account?.id, AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: item) {
continue
}
inPlaceStatuses.append(StatusModel(status: item))
if self.listType == .home {
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
}
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
self.prefetch(statusModels: statusModels)
// Append statuses to existing array of statuses (at the end).
self.statusViewModels.append(contentsOf: inPlaceStatuses)
self.statusViewModels.append(contentsOf: statusModels)
}
}
private func loadTopStatuses() async throws {
private func refreshStatuses() async throws {
guard let accountId = self.applicationState.account?.id else {
return
}
let statuses = try await self.loadFromApi()
if statuses.isEmpty {
@ -254,28 +263,29 @@ struct StatusesView: View {
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only statuses with images.
var inPlaceStatuses: [StatusModel] = []
for item in statuses.getStatusesWithImagesOnly() {
// We have to hide statuses without ALT text.
if self.shouldHideStatusWithoutAlt(status: item) {
continue
}
// We have to skip statuses that are boosted from muted accounts.
if let accountId = self.applicationState.account?.id, AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: item) {
continue
}
inPlaceStatuses.append(StatusModel(status: item))
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id, lastLoadedStatusId: statuses.first?.id, accountId: accountId, modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
}
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: inPlaceStatuses)
self.prefetch(statusModels: statusModels)
// Replace old collection with new one.
self.waterfallId = String.randomString(length: 8)
self.statusViewModels = inPlaceStatuses
self.statusViewModels = statusModels
}
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] {

View File

@ -6,6 +6,7 @@
import SwiftUI
@MainActor
struct ThirdPartyView: View {
var body: some View {
List {

View File

@ -11,9 +11,10 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct TrendStatusesView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@State public var accountId: String
@ -36,7 +37,7 @@ struct TrendStatusesView: View {
}
.padding()
.pickerStyle(SegmentedPickerStyle())
.onChange(of: tabSelectedValue) { _ in
.onChange(of: tabSelectedValue) {
Task {
do {
self.state = .loading

View File

@ -12,12 +12,12 @@ import WidgetsKit
import EnvironmentKit
struct UserProfileHeaderView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State var account: Account
@ObservedObject var relationship = RelationshipModel()
@State var relationship = RelationshipModel()
@Binding var boostsDisabled: Bool
var body: some View {

View File

@ -13,8 +13,9 @@ import EnvironmentKit
import WidgetsKit
struct UserProfileStatusesView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State public var accountId: String
@ -40,6 +41,8 @@ struct UserProfileStatusesView: View {
}
var body: some View {
@Bindable var applicationState = applicationState
if firstLoadFinished == true {
if self.imageColumns > 1 {
WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
@ -57,7 +60,7 @@ struct UserProfileStatusesView: View {
Button {
withAnimation {
self.applicationState.showGridOnUserProfile = false
ApplicationSettingsHandler.shared.set(showGridOnUserProfile: false)
ApplicationSettingsHandler.shared.set(showGridOnUserProfile: false, modelContext: modelContext)
}
} label: {
Image(systemName: "rectangle.grid.1x2.fill")
@ -68,7 +71,7 @@ struct UserProfileStatusesView: View {
Button {
withAnimation {
self.applicationState.showGridOnUserProfile = true
ApplicationSettingsHandler.shared.set(showGridOnUserProfile: true)
ApplicationSettingsHandler.shared.set(showGridOnUserProfile: true, modelContext: modelContext)
}
} label: {
Image(systemName: "rectangle.grid.2x2.fill")
@ -94,7 +97,7 @@ struct UserProfileStatusesView: View {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: true)
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
}
}
Spacer()

View File

@ -11,18 +11,20 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct UserProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State public var accountId: String
@State public var accountDisplayName: String?
@State public var accountUserName: String
@StateObject private var relationship = RelationshipModel()
@State private var relationship = RelationshipModel()
@State private var account: Account?
@State private var state: ViewState = .loading
@State private var viewId = UUID().uuidString
@ -121,7 +123,7 @@ struct UserProfileView: View {
}
if let signedInAccountId = self.applicationState.account?.id {
self.boostsDisabled = AccountRelationshipHandler.shared.isBoostedStatusesMuted(for: signedInAccountId, relation: self.accountId)
self.boostsDisabled = AccountRelationshipHandler.shared.isBoostedStatusesMuted(for: signedInAccountId, relation: self.accountId, modelContext: modelContext)
}
self.account = accountFromApi
@ -179,7 +181,8 @@ struct UserProfileView: View {
self.boostsDisabled.toggle()
AccountRelationshipHandler.shared.setBoostedStatusesMuted(for: signedInAccoountId,
relation: self.accountId,
boostedStatusesMuted: self.boostsDisabled)
boostedStatusesMuted: self.boostsDisabled,
modelContext: modelContext)
}
}
} label: {

View File

@ -10,7 +10,7 @@ import ServicesKit
import WidgetsKit
struct ImageCarouselPicture: View {
@ObservedObject public var attachment: AttachmentModel
public var attachment: AttachmentModel
@State private var blurredImageHeight: Double
@State private var blurredImageWidth: Double

Some files were not shown because too many files have changed in this diff Show More