Add account switcher feature.
This commit is contained in:
parent
9e21faed2f
commit
ea2eb0a972
|
@ -79,6 +79,8 @@
|
||||||
F89992C9296D6DC7005994BF /* CommentBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992C8296D6DC7005994BF /* CommentBody.swift */; };
|
F89992C9296D6DC7005994BF /* CommentBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992C8296D6DC7005994BF /* CommentBody.swift */; };
|
||||||
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CB296D9231005994BF /* StatusViewModel.swift */; };
|
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CB296D9231005994BF /* StatusViewModel.swift */; };
|
||||||
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CD296D92E7005994BF /* AttachmentViewModel.swift */; };
|
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CD296D92E7005994BF /* AttachmentViewModel.swift */; };
|
||||||
|
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DB296EAACE0062125F /* SettingsView.swift */; };
|
||||||
|
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DD296EABA20062125F /* StatusPlaceholder.swift */; };
|
||||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
|
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
|
||||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
|
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
|
||||||
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
|
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
|
||||||
|
@ -158,6 +160,8 @@
|
||||||
F89992C8296D6DC7005994BF /* CommentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBody.swift; sourceTree = "<group>"; };
|
F89992C8296D6DC7005994BF /* CommentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBody.swift; sourceTree = "<group>"; };
|
||||||
F89992CB296D9231005994BF /* StatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
|
F89992CB296D9231005994BF /* StatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
|
||||||
F89992CD296D92E7005994BF /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = "<group>"; };
|
F89992CD296D92E7005994BF /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
F89A46DB296EAACE0062125F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
F89A46DD296EABA20062125F /* StatusPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholder.swift; sourceTree = "<group>"; };
|
||||||
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
||||||
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
|
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
|
||||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -202,6 +206,7 @@
|
||||||
F85DBF902967385F0069BF89 /* FollowingView.swift */,
|
F85DBF902967385F0069BF89 /* FollowingView.swift */,
|
||||||
F897978E29684BCB00B22335 /* LoadingView.swift */,
|
F897978E29684BCB00B22335 /* LoadingView.swift */,
|
||||||
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
|
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
|
||||||
|
F89A46DB296EAACE0062125F /* SettingsView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -280,6 +285,7 @@
|
||||||
F86B721D296C458700EE59EC /* BlurredImage.swift */,
|
F86B721D296C458700EE59EC /* BlurredImage.swift */,
|
||||||
F86B7222296C4BF500EE59EC /* ContentWarning.swift */,
|
F86B7222296C4BF500EE59EC /* ContentWarning.swift */,
|
||||||
F89992C8296D6DC7005994BF /* CommentBody.swift */,
|
F89992C8296D6DC7005994BF /* CommentBody.swift */,
|
||||||
|
F89A46DD296EABA20062125F /* StatusPlaceholder.swift */,
|
||||||
);
|
);
|
||||||
path = Widgets;
|
path = Widgets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -509,6 +515,7 @@
|
||||||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||||
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
|
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
|
||||||
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
|
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
|
||||||
|
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */,
|
||||||
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
|
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
|
||||||
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
|
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
|
||||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
||||||
|
@ -533,6 +540,7 @@
|
||||||
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
||||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||||
|
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
||||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
||||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
||||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// https://mczachurski.dev
|
// https://mczachurski.dev
|
||||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ extension AccountData {
|
||||||
@NSManaged public var acct: String
|
@NSManaged public var acct: String
|
||||||
@NSManaged public var avatar: URL?
|
@NSManaged public var avatar: URL?
|
||||||
@NSManaged public var avatarData: Data?
|
@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 createdAt: String
|
||||||
@NSManaged public var displayName: String?
|
@NSManaged public var displayName: String?
|
||||||
@NSManaged public var followersCount: Int32
|
@NSManaged public var followersCount: Int32
|
||||||
|
@ -28,13 +31,28 @@ extension AccountData {
|
||||||
@NSManaged public var id: String
|
@NSManaged public var id: String
|
||||||
@NSManaged public var locked: Bool
|
@NSManaged public var locked: Bool
|
||||||
@NSManaged public var note: String?
|
@NSManaged public var note: String?
|
||||||
|
@NSManaged public var serverUrl: URL
|
||||||
@NSManaged public var statusesCount: Int32
|
@NSManaged public var statusesCount: Int32
|
||||||
@NSManaged public var url: URL?
|
@NSManaged public var url: URL?
|
||||||
@NSManaged public var username: String
|
@NSManaged public var username: String
|
||||||
@NSManaged public var clientId: String
|
@NSManaged public var statuses: Set<StatusData>?
|
||||||
@NSManaged public var clientSecret: String
|
|
||||||
@NSManaged public var clientVapidKey: String
|
}
|
||||||
@NSManaged public var serverUrl: URL
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
class AccountDataHandler {
|
class AccountDataHandler {
|
||||||
public static let shared = AccountDataHandler()
|
public static let shared = AccountDataHandler()
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
func getAccountsData() -> [AccountData] {
|
func getAccountsData(viewContext: NSManagedObjectContext? = nil) -> [AccountData] {
|
||||||
let context = CoreDataHandler.shared.container.viewContext
|
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||||
let fetchRequest = AccountData.fetchRequest()
|
let fetchRequest = AccountData.fetchRequest()
|
||||||
do {
|
do {
|
||||||
return try context.fetch(fetchRequest)
|
return try context.fetch(fetchRequest)
|
||||||
|
@ -22,8 +23,8 @@ class AccountDataHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentAccountData() -> AccountData? {
|
func getCurrentAccountData(viewContext: NSManagedObjectContext? = nil) -> AccountData? {
|
||||||
let accounts = self.getAccountsData()
|
let accounts = self.getAccountsData(viewContext: viewContext)
|
||||||
let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings()
|
let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings()
|
||||||
|
|
||||||
let currentAccount = accounts.first { accountData in
|
let currentAccount = accounts.first { accountData in
|
||||||
|
@ -37,6 +38,21 @@ class AccountDataHandler {
|
||||||
return accounts.first
|
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)
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try context.fetch(fetchRequest).first
|
||||||
|
} catch {
|
||||||
|
print("Error during fetching status (getAccountData)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createAccountDataEntity() -> AccountData {
|
func createAccountDataEntity() -> AccountData {
|
||||||
let context = CoreDataHandler.shared.container.viewContext
|
let context = CoreDataHandler.shared.container.viewContext
|
||||||
return AccountData(context: context)
|
return AccountData(context: context)
|
||||||
|
|
|
@ -12,17 +12,6 @@ class AttachmentDataHandler {
|
||||||
public static let shared = AttachmentDataHandler()
|
public static let shared = AttachmentDataHandler()
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
func getAttachmentsData() -> [AttachmentData] {
|
|
||||||
let context = CoreDataHandler.shared.container.viewContext
|
|
||||||
let fetchRequest = AttachmentData.fetchRequest()
|
|
||||||
do {
|
|
||||||
return try context.fetch(fetchRequest)
|
|
||||||
} catch {
|
|
||||||
print("Error during fetching attachmens (getAttachmentsData)")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAttachmnentDataEntity(viewContext: NSManagedObjectContext? = nil) -> AttachmentData {
|
func createAttachmnentDataEntity(viewContext: NSManagedObjectContext? = nil) -> AttachmentData {
|
||||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||||
return AttachmentData(context: context)
|
return AttachmentData(context: context)
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
public class CoreDataHandler {
|
public class CoreDataHandler {
|
||||||
|
|
|
@ -41,6 +41,8 @@ extension StatusData {
|
||||||
@NSManaged public var url: URL?
|
@NSManaged public var url: URL?
|
||||||
@NSManaged public var visibility: String
|
@NSManaged public var visibility: String
|
||||||
@NSManaged public var attachmentRelation: Set<AttachmentData>?
|
@NSManaged public var attachmentRelation: Set<AttachmentData>?
|
||||||
|
@NSManaged public var pixelfedAccount: AccountData
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Generated accessors for attachmentRelation
|
// MARK: Generated accessors for attachmentRelation
|
||||||
|
|
|
@ -13,23 +13,15 @@ class StatusDataHandler {
|
||||||
public static let shared = StatusDataHandler()
|
public static let shared = StatusDataHandler()
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
func getStatusesData() -> [StatusData] {
|
func getStatusData(accountId: String, statusId: String) -> StatusData? {
|
||||||
let context = CoreDataHandler.shared.container.viewContext
|
|
||||||
let fetchRequest = StatusData.fetchRequest()
|
|
||||||
do {
|
|
||||||
return try context.fetch(fetchRequest)
|
|
||||||
} catch {
|
|
||||||
print("Error during fetching statuses (getStatusesData)")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStatusData(statusId: String) -> StatusData? {
|
|
||||||
let context = CoreDataHandler.shared.container.viewContext
|
let context = CoreDataHandler.shared.container.viewContext
|
||||||
let fetchRequest = StatusData.fetchRequest()
|
let fetchRequest = StatusData.fetchRequest()
|
||||||
|
|
||||||
fetchRequest.fetchLimit = 1
|
fetchRequest.fetchLimit = 1
|
||||||
fetchRequest.predicate = NSPredicate(format: "id = %@", statusId)
|
let predicate1 = NSPredicate(format: "id = %@", statusId)
|
||||||
|
let predicate2 = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||||
|
|
||||||
|
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1,predicate2])
|
||||||
|
|
||||||
do {
|
do {
|
||||||
return try context.fetch(fetchRequest).first
|
return try context.fetch(fetchRequest).first
|
||||||
|
@ -39,13 +31,16 @@ class StatusDataHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMaximumStatus(viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
func getMaximumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||||
let fetchRequest = StatusData.fetchRequest()
|
let fetchRequest = StatusData.fetchRequest()
|
||||||
|
|
||||||
fetchRequest.fetchLimit = 1
|
fetchRequest.fetchLimit = 1
|
||||||
|
|
||||||
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
|
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
|
||||||
fetchRequest.sortDescriptors = [sortDescriptor]
|
fetchRequest.sortDescriptors = [sortDescriptor]
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let statuses = try context.fetch(fetchRequest)
|
let statuses = try context.fetch(fetchRequest)
|
||||||
return statuses.first
|
return statuses.first
|
||||||
|
@ -55,13 +50,16 @@ class StatusDataHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMinimumtatus(viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
func getMinimumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||||
let fetchRequest = StatusData.fetchRequest()
|
let fetchRequest = StatusData.fetchRequest()
|
||||||
|
|
||||||
fetchRequest.fetchLimit = 1
|
fetchRequest.fetchLimit = 1
|
||||||
|
|
||||||
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
|
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
|
||||||
fetchRequest.sortDescriptors = [sortDescriptor]
|
fetchRequest.sortDescriptors = [sortDescriptor]
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let statuses = try context.fetch(fetchRequest)
|
let statuses = try context.fetch(fetchRequest)
|
||||||
return statuses.first
|
return statuses.first
|
||||||
|
|
|
@ -8,32 +8,36 @@ import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import MastodonKit
|
import MastodonKit
|
||||||
|
|
||||||
|
public enum DatabaseError: Error {
|
||||||
|
case cannotDownloadAccount
|
||||||
|
}
|
||||||
|
|
||||||
public class TimelineService {
|
public class TimelineService {
|
||||||
public static let shared = TimelineService()
|
public static let shared = TimelineService()
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
public func onBottomOfList(for accountData: AccountData) async throws {
|
public func onBottomOfList(for accountData: AccountData) async throws -> Int {
|
||||||
// Load data from API and operate on CoreData on background context.
|
// Load data from API and operate on CoreData on background context.
|
||||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||||
|
|
||||||
// Get maximimum downloaded stauts id.
|
// Get maximimum downloaded stauts id.
|
||||||
let oldestStatus = StatusDataHandler.shared.getMinimumtatus(viewContext: backgroundContext)
|
let oldestStatus = StatusDataHandler.shared.getMinimumStatus(accountId: accountData.id, viewContext: backgroundContext)
|
||||||
|
|
||||||
guard let oldestStatus = oldestStatus else {
|
guard let oldestStatus = oldestStatus else {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
return try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onTopOfList(for accountData: AccountData) async throws {
|
public func onTopOfList(for accountData: AccountData) async throws -> Int {
|
||||||
// Load data from API and operate on CoreData on background context.
|
// Load data from API and operate on CoreData on background context.
|
||||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||||
|
|
||||||
// Get maximimum downloaded stauts id.
|
// Get maximimum downloaded stauts id.
|
||||||
let newestStatus = StatusDataHandler.shared.getMaximumStatus(viewContext: backgroundContext)
|
let newestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: accountData.id, viewContext: backgroundContext)
|
||||||
|
|
||||||
try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
|
return try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? {
|
public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? {
|
||||||
|
@ -50,9 +54,9 @@ public class TimelineService {
|
||||||
return try await client.getContext(for: statusId)
|
return try await client.getContext(for: statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
|
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws -> Int {
|
||||||
guard let accessToken = accountData.accessToken else {
|
guard let accessToken = accountData.accessToken else {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve statuses from API.
|
// Retrieve statuses from API.
|
||||||
|
@ -76,13 +80,22 @@ public class TimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
|
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
|
||||||
|
|
||||||
|
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountData.id, viewContext: backgroundContext) else {
|
||||||
|
throw DatabaseError.cannotDownloadAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
statusData.pixelfedAccount = dbAccount
|
||||||
|
dbAccount.addToStatuses(statusData)
|
||||||
|
|
||||||
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
|
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
try backgroundContext.save()
|
try backgroundContext.save()
|
||||||
|
return statuses.count
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateStatus(_ statusData: StatusData, basedOn status: Status) async throws -> StatusData? {
|
public func updateStatus(_ statusData: StatusData, accountData: AccountData, basedOn status: Status) async throws -> StatusData? {
|
||||||
// Load data from API and operate on CoreData on background context.
|
// Load data from API and operate on CoreData on background context.
|
||||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
|
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
||||||
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
||||||
|
@ -68,5 +69,6 @@
|
||||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="visibility" attributeType="String"/>
|
<attribute name="visibility" attributeType="String"/>
|
||||||
<relationship name="attachmentRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
<relationship name="attachmentRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
||||||
|
<relationship name="pixelfedAccount" maxCount="1" deletionRule="No Action" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
|
||||||
</entity>
|
</entity>
|
||||||
</model>
|
</model>
|
|
@ -9,6 +9,7 @@ import MastodonKit
|
||||||
|
|
||||||
public class StatusViewModel {
|
public class StatusViewModel {
|
||||||
|
|
||||||
|
public let uniqueId: UUID
|
||||||
public let id: StatusId
|
public let id: StatusId
|
||||||
public let content: Html
|
public let content: Html
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ public class StatusViewModel {
|
||||||
tags: [Tag] = [],
|
tags: [Tag] = [],
|
||||||
place: Place? = nil
|
place: Place? = nil
|
||||||
) {
|
) {
|
||||||
|
self.uniqueId = UUID()
|
||||||
self.id = id
|
self.id = id
|
||||||
self.content = content
|
self.content = content
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
@ -94,6 +96,7 @@ public class StatusViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
init(status: Status) {
|
init(status: Status) {
|
||||||
|
self.uniqueId = UUID()
|
||||||
self.id = status.id
|
self.id = status.id
|
||||||
self.content = status.content
|
self.content = status.content
|
||||||
self.uri = status.uri
|
self.uri = status.uri
|
||||||
|
|
|
@ -33,12 +33,10 @@ struct FollowersView: View {
|
||||||
|
|
||||||
if allItemsLoaded == false && firstLoadFinished == true {
|
if allItemsLoaded == false && firstLoadFinished == true {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
self.page = self.page + 1
|
self.page = self.page + 1
|
||||||
await self.loadAccounts(page: self.page)
|
await self.loadAccounts(page: self.page)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}.overlay {
|
}.overlay {
|
||||||
|
|
|
@ -33,12 +33,10 @@ struct FollowingView: View {
|
||||||
|
|
||||||
if allItemsLoaded == false && firstLoadFinished == true {
|
if allItemsLoaded == false && firstLoadFinished == true {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
self.page = self.page + 1
|
self.page = self.page + 1
|
||||||
await self.loadAccounts(page: self.page)
|
await self.loadAccounts(page: self.page)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}.overlay {
|
}.overlay {
|
||||||
|
|
|
@ -10,15 +10,21 @@ struct HomeFeedView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
@EnvironmentObject var applicationState: ApplicationState
|
@EnvironmentObject var applicationState: ApplicationState
|
||||||
|
|
||||||
@State private var showLoading = false
|
@State private var firstLoadFinished = false
|
||||||
|
@State private var allItemsBottomLoaded = false
|
||||||
|
|
||||||
private static let initialColumns = 1
|
private static let initialColumns = 1
|
||||||
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
|
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
|
||||||
|
|
||||||
@FetchRequest(sortDescriptors: [SortDescriptor(\.id, order: .reverse)]) var dbStatuses: FetchedResults<StatusData>
|
@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 {
|
var body: some View {
|
||||||
ZStack {
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVGrid(columns: gridColumns) {
|
LazyVGrid(columns: gridColumns) {
|
||||||
ForEach(dbStatuses, id: \.self) { item in
|
ForEach(dbStatuses, id: \.self) { item in
|
||||||
|
@ -32,12 +38,15 @@ struct HomeFeedView: View {
|
||||||
.buttonStyle(EmptyButtonStyle())
|
.buttonStyle(EmptyButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if allItemsBottomLoaded == false && firstLoadFinished == true {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
if let accountData = self.applicationState.accountData {
|
if let accountData = self.applicationState.accountData {
|
||||||
try await TimelineService.shared.onBottomOfList(for: accountData)
|
let newStatusesCount = try await TimelineService.shared.onBottomOfList(for: accountData)
|
||||||
|
if newStatusesCount == 0 {
|
||||||
|
allItemsBottomLoaded = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Error", error)
|
print("Error", error)
|
||||||
|
@ -46,15 +55,25 @@ struct HomeFeedView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.overlay(alignment: .center) {
|
||||||
if showLoading {
|
if firstLoadFinished == false {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
|
} else {
|
||||||
|
if self.dbStatuses.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "photo.on.rectangle.angled")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
Text("Unfortunately, there are no photos here.")
|
||||||
|
.font(.title3)
|
||||||
|
}.foregroundColor(.lightGrayColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
do {
|
do {
|
||||||
if let accountData = self.applicationState.accountData {
|
if let accountData = self.applicationState.accountData {
|
||||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
_ = try await TimelineService.shared.onTopOfList(for: accountData)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Error", error)
|
print("Error", error)
|
||||||
|
@ -62,15 +81,20 @@ struct HomeFeedView: View {
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
do {
|
do {
|
||||||
if self.dbStatuses.isEmpty {
|
defer {
|
||||||
self.showLoading = true
|
Task { @MainActor in
|
||||||
if let accountData = self.applicationState.accountData {
|
self.firstLoadFinished = true
|
||||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
|
||||||
}
|
}
|
||||||
self.showLoading = false
|
}
|
||||||
|
|
||||||
|
if self.dbStatuses.isEmpty == false {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let accountData = self.applicationState.accountData {
|
||||||
|
_ = try await TimelineService.shared.onTopOfList(for: accountData)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self.showLoading = false
|
|
||||||
print("Error", error)
|
print("Error", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +103,6 @@ struct HomeFeedView: View {
|
||||||
|
|
||||||
struct HomeFeedView_Previews: PreviewProvider {
|
struct HomeFeedView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HomeFeedView()
|
HomeFeedView(accountId: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ struct MainView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
@EnvironmentObject var applicationState: ApplicationState
|
@EnvironmentObject var applicationState: ApplicationState
|
||||||
|
|
||||||
|
@State private var showSettings = false
|
||||||
@State private var navBarTitle: String = "Home"
|
@State private var navBarTitle: String = "Home"
|
||||||
@State private var viewMode: ViewMode = .home {
|
@State private var viewMode: ViewMode = .home {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -20,6 +21,8 @@ struct MainView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults<AccountData>
|
||||||
|
|
||||||
private enum ViewMode {
|
private enum ViewMode {
|
||||||
case home, local, federated, profile, notifications
|
case home, local, federated, profile, notifications
|
||||||
}
|
}
|
||||||
|
@ -28,6 +31,9 @@ struct MainView: View {
|
||||||
self.getMainView()
|
self.getMainView()
|
||||||
.navigationBarTitle(navBarTitle)
|
.navigationBarTitle(navBarTitle)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.sheet(isPresented: $showSettings, content: {
|
||||||
|
SettingsView()
|
||||||
|
})
|
||||||
.toolbar {
|
.toolbar {
|
||||||
self.getLeadingToolbar()
|
self.getLeadingToolbar()
|
||||||
self.getPrincipalToolbar()
|
self.getPrincipalToolbar()
|
||||||
|
@ -38,19 +44,24 @@ struct MainView: View {
|
||||||
private func getMainView() -> some View {
|
private func getMainView() -> some View {
|
||||||
switch self.viewMode {
|
switch self.viewMode {
|
||||||
case .home:
|
case .home:
|
||||||
HomeFeedView()
|
HomeFeedView(accountId: applicationState.accountData?.id ?? "")
|
||||||
|
.id(applicationState.accountData?.id ?? "")
|
||||||
case .local:
|
case .local:
|
||||||
LocalFeedView()
|
LocalFeedView()
|
||||||
|
.id(applicationState.accountData?.id ?? "")
|
||||||
case .federated:
|
case .federated:
|
||||||
FederatedFeedView()
|
FederatedFeedView()
|
||||||
|
.id(applicationState.accountData?.id ?? "")
|
||||||
case .profile:
|
case .profile:
|
||||||
if let accountData = self.applicationState.accountData {
|
if let accountData = self.applicationState.accountData {
|
||||||
UserProfileView(accountId: accountData.id,
|
UserProfileView(accountId: accountData.id,
|
||||||
accountDisplayName: accountData.displayName,
|
accountDisplayName: accountData.displayName,
|
||||||
accountUserName: accountData.acct)
|
accountUserName: accountData.acct)
|
||||||
|
.id(applicationState.accountData?.id ?? "")
|
||||||
}
|
}
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsView()
|
NotificationsView()
|
||||||
|
.id(applicationState.accountData?.id ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,27 +132,24 @@ struct MainView: View {
|
||||||
private func getLeadingToolbar() -> some ToolbarContent {
|
private func getLeadingToolbar() -> some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Menu {
|
Menu {
|
||||||
|
ForEach(self.dbAccounts) { account in
|
||||||
Button {
|
Button {
|
||||||
// TODO: Switch accounts.
|
self.applicationState.accountData = account
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
if self.applicationState.accountData?.id == account.id {
|
||||||
Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.acct ?? "")
|
Label(account.displayName ?? account.acct, systemImage: "checkmark")
|
||||||
Image(systemName: "person.circle.fill")
|
} else {
|
||||||
.resizable()
|
Text(account.displayName ?? account.acct)
|
||||||
.foregroundColor(.mainTextColor)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
// TODO: Open settings.
|
self.showSettings.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
Label("Settings", systemImage: "gear")
|
||||||
Text("Settings")
|
|
||||||
Image(systemName: "gear")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
if let avatarData = self.applicationState.accountData?.avatarData, let uiImage = UIImage(data: avatarData) {
|
if let avatarData = self.applicationState.accountData?.avatarData, let uiImage = UIImage(data: avatarData) {
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
@EnvironmentObject var applicationState: ApplicationState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State var accounts: [AccountData] = []
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
Section("Accounts") {
|
||||||
|
ForEach(self.accounts) { account in
|
||||||
|
UsernameRow(accountAvatar: account.avatar,
|
||||||
|
accountDisplayName: account.displayName,
|
||||||
|
accountUsername: account.username,
|
||||||
|
cachedAvatar: CacheAvatarService.shared.getImage(for: account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(destination: SignInView()) {
|
||||||
|
HStack {
|
||||||
|
Text("New account")
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "person.crop.circle.badge.plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("General") {
|
||||||
|
Text("Accent")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("About") {
|
||||||
|
Text("Website")
|
||||||
|
Text("License")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(alignment: .topLeading)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Close", role: .cancel) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
self.accounts = AccountDataHandler.shared.getAccountsData()
|
||||||
|
}
|
||||||
|
.navigationBarTitle(Text("Settings"), displayMode: .inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,10 +8,12 @@ import SwiftUI
|
||||||
|
|
||||||
struct SignInView: View {
|
struct SignInView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
@EnvironmentObject var applicationState: ApplicationState
|
@EnvironmentObject var applicationState: ApplicationState
|
||||||
|
|
||||||
@State private var serverAddress: String = ""
|
@State private var serverAddress: String = ""
|
||||||
|
|
||||||
|
|
||||||
var onSignInStateChenge: ((_ applicationViewMode: ApplicationViewMode) -> Void)?
|
var onSignInStateChenge: ((_ applicationViewMode: ApplicationViewMode) -> Void)?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -33,6 +35,7 @@ struct SignInView: View {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.applicationState.accountData = accountData
|
self.applicationState.accountData = accountData
|
||||||
onSignInStateChenge?(.mainView)
|
onSignInStateChenge?(.mainView)
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,47 +88,14 @@ struct StatusView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
VStack (alignment: .leading) {
|
StatusPlaceholder(imageHeight: self.getImageHeight(), imageBlurhash: self.imageBlurhash)
|
||||||
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
|
|
||||||
Image(uiImage: uiImage)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: UIScreen.main.bounds.width, height: self.getImageHeight())
|
|
||||||
} else {
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color.placeholderText)
|
|
||||||
.frame(width: UIScreen.main.bounds.width, height: self.getImageHeight())
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
UsernameRow(accountDisplayName: "Verylong Displayname",
|
|
||||||
accountUsername: "@username")
|
|
||||||
|
|
||||||
Text("Lorem ispum text something")
|
|
||||||
.foregroundColor(.lightGrayColor)
|
|
||||||
.font(.footnote)
|
|
||||||
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
|
|
||||||
.foregroundColor(.lightGrayColor)
|
|
||||||
.font(.footnote)
|
|
||||||
|
|
||||||
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
|
|
||||||
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
|
||||||
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
|
||||||
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
|
||||||
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
.animatePlaceholder(isLoading: .constant(true))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Details")
|
.navigationBarTitle("Details")
|
||||||
.sheet(isPresented: $showCompose, content: {
|
.sheet(isPresented: $showCompose, content: {
|
||||||
ComposeView(statusViewModel: $messageForStatus)
|
ComposeView(statusViewModel: $messageForStatus)
|
||||||
})
|
})
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
// Get status from API.
|
// Get status from API.
|
||||||
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
|
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
|
||||||
|
@ -144,12 +111,10 @@ struct StatusView: View {
|
||||||
|
|
||||||
self.statusViewModel = statusViewModel
|
self.statusViewModel = statusViewModel
|
||||||
|
|
||||||
// Get status from database.
|
|
||||||
let statusDataFromDatabase = StatusDataHandler.shared.getStatusData(statusId: self.statusId)
|
|
||||||
|
|
||||||
// If we have status in database then we can update data.
|
// If we have status in database then we can update data.
|
||||||
if let statusDataFromDatabase {
|
if let accountData = self.applicationState.accountData,
|
||||||
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, basedOn: status)
|
let statusDataFromDatabase = StatusDataHandler.shared.getStatusData(accountId: accountData.id, statusId: self.statusId) {
|
||||||
|
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, accountData: accountData, basedOn: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -157,7 +122,6 @@ struct StatusView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func setAttachment(_ attachmentData: AttachmentData) {
|
private func setAttachment(_ attachmentData: AttachmentData) {
|
||||||
exifCamera = attachmentData.exifCamera
|
exifCamera = attachmentData.exifCamera
|
||||||
|
|
|
@ -30,8 +30,7 @@ struct UserProfileView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle(self.accountDisplayName ?? self.accountUserName)
|
.navigationBarTitle(self.accountDisplayName ?? self.accountUserName)
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
try await self.loadData()
|
try await self.loadData()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -39,7 +38,6 @@ struct UserProfileView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func loadData() async throws {
|
private func loadData() async throws {
|
||||||
async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData)
|
async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData)
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct StatusPlaceholder: View {
|
||||||
|
@State var imageHeight: Double
|
||||||
|
@State var imageBlurhash: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack (alignment: .leading) {
|
||||||
|
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
|
||||||
|
Image(uiImage: uiImage)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.placeholderText)
|
||||||
|
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
UsernameRow(accountDisplayName: "Verylong Displayname",
|
||||||
|
accountUsername: "@username")
|
||||||
|
|
||||||
|
Text("Lorem ispum text something")
|
||||||
|
.foregroundColor(.lightGrayColor)
|
||||||
|
.font(.footnote)
|
||||||
|
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
|
||||||
|
.foregroundColor(.lightGrayColor)
|
||||||
|
.font(.footnote)
|
||||||
|
|
||||||
|
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
|
||||||
|
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
||||||
|
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
||||||
|
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
||||||
|
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.animatePlaceholder(isLoading: .constant(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StatusPlaceholder_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
StatusPlaceholder(imageHeight: 100.0)
|
||||||
|
.previewLayout(.fixed(width: 320, height: 320))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ struct UserProfileStatuses: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if firstLoadFinished == true {
|
if firstLoadFinished == true {
|
||||||
ForEach(self.statusViewModels, id: \.id) { item in
|
ForEach(self.statusViewModels, id: \.uniqueId) { item in
|
||||||
NavigationLink(destination: StatusView(statusId: item.id,
|
NavigationLink(destination: StatusView(statusId: item.id,
|
||||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||||
imageWidth: item.getImageWidth(),
|
imageWidth: item.getImageWidth(),
|
||||||
|
@ -35,15 +35,13 @@ struct UserProfileStatuses: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
if allItemsLoaded == false && firstLoadFinished == true {
|
if allItemsLoaded == false && firstLoadFinished == true {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
.onAppear {
|
.task {
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
try await self.loadMoreStatuses()
|
try await self.loadMoreStatuses()
|
||||||
} catch {
|
} catch {
|
||||||
print("Error \(error.localizedDescription)")
|
print("Error \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,8 +49,7 @@ struct UserProfileStatuses: View {
|
||||||
} else {
|
} else {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
}
|
}
|
||||||
}.onAppear {
|
}.task {
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
try await self.loadStatuses()
|
try await self.loadStatuses()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -60,7 +57,6 @@ struct UserProfileStatuses: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func loadStatuses() async throws {
|
private func loadStatuses() async throws {
|
||||||
let statuses = try await AccountService.shared.getStatuses(
|
let statuses = try await AccountService.shared.getStatuses(
|
||||||
|
|
Loading…
Reference in New Issue