Add StatusViewModel
This commit is contained in:
parent
c293ab8e84
commit
16dd600059
|
@ -1,11 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public class Application: Codable {
|
||||
public struct Application: Codable {
|
||||
public let name: String
|
||||
public let website: URL?
|
||||
|
||||
public init(name: String, website: URL? = nil) {
|
||||
self.name = name
|
||||
self.website = website
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ public class Status: Codable {
|
|||
|
||||
public let uri: String?
|
||||
public let url: URL?
|
||||
public let account: Account?
|
||||
public let account: Account
|
||||
public let inReplyToId: AccountId?
|
||||
public let inReplyToAccount: StatusId?
|
||||
public let reblog: Status?
|
||||
|
@ -65,60 +65,6 @@ public class Status: Codable {
|
|||
case tags
|
||||
case application
|
||||
}
|
||||
|
||||
public init(
|
||||
id: StatusId,
|
||||
content: Html,
|
||||
uri: String? = nil,
|
||||
url: URL? = nil,
|
||||
account: Account? = nil,
|
||||
inReplyToId: AccountId? = nil,
|
||||
inReplyToAccount: StatusId? = nil,
|
||||
reblog: Status? = nil,
|
||||
createdAt: String? = nil,
|
||||
reblogsCount: Int = 0,
|
||||
favouritesCount: Int = 0,
|
||||
repliesCount: Int = 0,
|
||||
reblogged: Bool = false,
|
||||
favourited: Bool = false,
|
||||
sensitive: Bool = false,
|
||||
bookmarked: Bool = false,
|
||||
pinned: Bool = false,
|
||||
muted: Bool = false,
|
||||
spoilerText: String? = nil,
|
||||
visibility: Visibility = .pub,
|
||||
mediaAttachments: [Attachment] = [],
|
||||
card: Card? = nil,
|
||||
mentions: [Mention] = [],
|
||||
tags: [Tag] = [],
|
||||
application: Application
|
||||
) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
self.url = url
|
||||
self.account = account
|
||||
self.inReplyToId = inReplyToId
|
||||
self.inReplyToAccount = inReplyToAccount
|
||||
self.reblog = reblog
|
||||
self.createdAt = createdAt ?? Date().formatted(.iso8601)
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
self.repliesCount = repliesCount
|
||||
self.reblogged = reblogged
|
||||
self.favourited = favourited
|
||||
self.sensitive = sensitive
|
||||
self.bookmarked = bookmarked
|
||||
self.pinned = pinned
|
||||
self.muted = muted
|
||||
self.spoilerText = spoilerText
|
||||
self.visibility = visibility
|
||||
self.mediaAttachments = mediaAttachments
|
||||
self.card = card
|
||||
self.mentions = mentions
|
||||
self.tags = tags
|
||||
self.application = application
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
@ -126,7 +72,7 @@ public class Status: Codable {
|
|||
self.id = try container.decode(StatusId.self, forKey: .id)
|
||||
self.uri = try container.decode(String.self, forKey: .uri)
|
||||
self.url = try? container.decode(URL.self, forKey: .url)
|
||||
self.account = try? container.decode(Account.self, forKey: .account)
|
||||
self.account = try container.decode(Account.self, forKey: .account)
|
||||
self.content = try container.decode(Html.self, forKey: .content)
|
||||
self.createdAt = try container.decode(String.self, forKey: .createdAt)
|
||||
self.inReplyToId = try? container.decode(AccountId.self, forKey: .inReplyToId)
|
||||
|
@ -158,9 +104,8 @@ public class Status: Codable {
|
|||
if let url {
|
||||
try container.encode(url, forKey: .url)
|
||||
}
|
||||
if let account {
|
||||
try container.encode(account, forKey: .account)
|
||||
}
|
||||
|
||||
try container.encode(account, forKey: .account)
|
||||
try container.encode(content, forKey: .content)
|
||||
try container.encode(createdAt, forKey: .createdAt)
|
||||
if let inReplyToId {
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
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 */; };
|
||||
F8210DE12966D0C4001D9973 /* StatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE02966D0C4001D9973 /* StatusService.swift */; };
|
||||
F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE22966D256001D9973 /* Status+StatusData.swift */; };
|
||||
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; };
|
||||
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
|
||||
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
|
||||
|
@ -50,7 +49,6 @@
|
|||
F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */; };
|
||||
F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */; };
|
||||
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7217296C27C100EE59EC /* ActionButton.swift */; };
|
||||
F86B721C296C394000EE59EC /* Status+ImageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B721B296C394000EE59EC /* Status+ImageSize.swift */; };
|
||||
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B721D296C458700EE59EC /* BlurredImage.swift */; };
|
||||
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */; };
|
||||
F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7222296C4BF500EE59EC /* ContentWarning.swift */; };
|
||||
|
@ -79,6 +77,8 @@
|
|||
F8984E4D296B648000A2610F /* UIImage+Blurhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */; };
|
||||
F89992C7296D3DF8005994BF /* MastodonKit in Frameworks */ = {isa = PBXBuildFile; productRef = F89992C6296D3DF8005994BF /* MastodonKit */; };
|
||||
F89992C9296D6DC7005994BF /* CommentBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992C8296D6DC7005994BF /* CommentBody.swift */; };
|
||||
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CB296D9231005994BF /* StatusViewModel.swift */; };
|
||||
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CD296D92E7005994BF /* AttachmentViewModel.swift */; };
|
||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.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 */; };
|
||||
|
@ -96,7 +96,6 @@
|
|||
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>"; };
|
||||
F8210DE02966D0C4001D9973 /* StatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusService.swift; sourceTree = "<group>"; };
|
||||
F8210DE22966D256001D9973 /* Status+StatusData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+StatusData.swift"; sourceTree = "<group>"; };
|
||||
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
|
||||
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
|
||||
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
|
||||
|
@ -128,7 +127,6 @@
|
|||
F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeader.swift; sourceTree = "<group>"; };
|
||||
F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatuses.swift; sourceTree = "<group>"; };
|
||||
F86B7217296C27C100EE59EC /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
|
||||
F86B721B296C394000EE59EC /* Status+ImageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+ImageSize.swift"; sourceTree = "<group>"; };
|
||||
F86B721D296C458700EE59EC /* BlurredImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredImage.swift; sourceTree = "<group>"; };
|
||||
F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyButtonStyle.swift; sourceTree = "<group>"; };
|
||||
F86B7222296C4BF500EE59EC /* ContentWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarning.swift; sourceTree = "<group>"; };
|
||||
|
@ -158,6 +156,8 @@
|
|||
F897978E29684BCB00B22335 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Blurhash.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>"; };
|
||||
F89992CD296D92E7005994BF /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.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>"; };
|
||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
||||
|
@ -212,12 +212,10 @@
|
|||
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */,
|
||||
F85D49862964334100751DF7 /* String+Date.swift */,
|
||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */,
|
||||
F8210DE22966D256001D9973 /* Status+StatusData.swift */,
|
||||
F8210DE42966E160001D9973 /* Color+SystemColors.swift */,
|
||||
F8210DE62966E1D1001D9973 /* Color+Assets.swift */,
|
||||
F8C14393296AF21B001FE31D /* Double+Round.swift */,
|
||||
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */,
|
||||
F86B721B296C394000EE59EC /* Status+ImageSize.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -325,6 +323,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
F866F6A829604FFF002E8F88 /* Info.plist */,
|
||||
F89992CA296D9211005994BF /* ViewModels */,
|
||||
F86B721F296C498B00EE59EC /* Styles */,
|
||||
F88ABD9029686F00004EF61E /* Cache */,
|
||||
F897978B2968367E00B22335 /* Haptics */,
|
||||
|
@ -381,6 +380,15 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F89992CA296D9211005994BF /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F89992CB296D9231005994BF /* StatusViewModel.swift */,
|
||||
F89992CD296D92E7005994BF /* AttachmentViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -465,7 +473,6 @@
|
|||
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
|
||||
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
|
||||
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */,
|
||||
F86B721C296C394000EE59EC /* Status+ImageSize.swift in Sources */,
|
||||
F8984E4D296B648000A2610F /* UIImage+Blurhash.swift in Sources */,
|
||||
F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */,
|
||||
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */,
|
||||
|
@ -486,12 +493,14 @@
|
|||
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
|
||||
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */,
|
||||
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
|
||||
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */,
|
||||
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
|
||||
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
|
||||
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
|
||||
F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */,
|
||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */,
|
||||
F85DBF912967385F0069BF89 /* FollowingView.swift in Sources */,
|
||||
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */,
|
||||
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
|
||||
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
|
||||
F897978D2968369600B22335 /* HapticService.swift in Sources */,
|
||||
|
@ -506,7 +515,6 @@
|
|||
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
|
||||
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
|
||||
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */,
|
||||
F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */,
|
||||
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
|
||||
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */,
|
||||
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
|
||||
|
|
|
@ -11,10 +11,10 @@ extension StatusData {
|
|||
func copyFrom(_ status: Status) {
|
||||
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.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
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
extension Status {
|
||||
public func getImageWidth() -> Int32? {
|
||||
if let width = (self.mediaAttachments.first?.meta as? ImageMetadata)?.original?.width {
|
||||
return Int32(width)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func getImageHeight() -> Int32? {
|
||||
if let height = (self.mediaAttachments.first?.meta as? ImageMetadata)?.original?.height {
|
||||
return Int32(height)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
extension Status {
|
||||
func createStatusData() async throws -> StatusData {
|
||||
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: CoreDataHandler.memory.container.viewContext)
|
||||
statusData.copyFrom(self)
|
||||
|
||||
for attachment in self.mediaAttachments {
|
||||
let imageData = try await RemoteFileService.shared.fetchData(url: attachment.url)
|
||||
|
||||
guard let imageData = imageData else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Save attachment in database.
|
||||
let attachmentData = AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: CoreDataHandler.memory.container.viewContext)
|
||||
|
||||
attachmentData.copyFrom(attachment)
|
||||
attachmentData.statusId = statusData.id
|
||||
attachmentData.data = imageData
|
||||
|
||||
// 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)"
|
||||
}
|
||||
}
|
||||
|
||||
attachmentData.statusRelation = statusData
|
||||
statusData.addToAttachmentRelation(attachmentData)
|
||||
}
|
||||
|
||||
return statusData
|
||||
}
|
||||
}
|
|
@ -4,13 +4,13 @@
|
|||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class ApplicationState: ObservableObject {
|
||||
public static let shared = ApplicationState()
|
||||
private init() { }
|
||||
|
||||
|
||||
@Published var accountData: AccountData?
|
||||
@Published var showInteractionStatusId = ""
|
||||
}
|
||||
|
|
|
@ -64,4 +64,13 @@ public class StatusService {
|
|||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.unbookmark(statusId: statusId)
|
||||
}
|
||||
|
||||
func new(status: Mastodon.Statuses.Components, accountData: AccountData?) async throws -> Status? {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.new(statusComponents: status)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ public class TimelineService {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchAllImages(statuses: [Status]) async -> Dictionary<String, Data> {
|
||||
public func fetchAllImages(statuses: [Status]) async -> Dictionary<String, Data> {
|
||||
var attachmentUrls: Dictionary<String, URL> = [:]
|
||||
|
||||
statuses.forEach { status in
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class AttachmentViewModel {
|
||||
public let id: String
|
||||
public let type: Attachment.AttachmentType
|
||||
public let url: URL
|
||||
|
||||
public let previewUrl: URL?
|
||||
public let remoteUrl: URL?
|
||||
public let description: String?
|
||||
public let blurhash: String?
|
||||
public let meta: Metadata?
|
||||
|
||||
public let metaImageWidth: Int32?
|
||||
public let metaImageHeight: Int32?
|
||||
|
||||
public var data: Data?
|
||||
public var exifCamera: String?
|
||||
public var exifCreatedDate: String?
|
||||
public var exifExposure: String?
|
||||
public var exifLens: String?
|
||||
|
||||
init(id: String,
|
||||
type: Attachment.AttachmentType,
|
||||
url: URL,
|
||||
previewUrl: URL? = nil,
|
||||
remoteUrl: URL? = nil,
|
||||
description: String? = nil,
|
||||
blurhash: String? = nil,
|
||||
meta: Metadata? = nil,
|
||||
exifCamera: String? = nil,
|
||||
exifCreatedDate: String? = nil,
|
||||
exifExposure: String? = nil,
|
||||
exifLens: String? = nil,
|
||||
metaImageWidth: Int32? = nil,
|
||||
metaImageHeight: Int32? = nil,
|
||||
data: Data? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.previewUrl = previewUrl
|
||||
self.remoteUrl = remoteUrl
|
||||
self.description = description
|
||||
self.blurhash = blurhash
|
||||
self.meta = meta
|
||||
self.exifCamera = exifCamera
|
||||
self.exifCreatedDate = exifCreatedDate
|
||||
self.exifExposure = exifExposure
|
||||
self.exifLens = exifLens
|
||||
self.metaImageWidth = metaImageWidth
|
||||
self.metaImageHeight = metaImageHeight
|
||||
self.data = data
|
||||
}
|
||||
|
||||
init(attachment: Attachment) {
|
||||
self.id = attachment.id
|
||||
self.type = attachment.type
|
||||
self.url = attachment.url
|
||||
self.previewUrl = attachment.previewUrl
|
||||
self.remoteUrl = attachment.remoteUrl
|
||||
self.description = attachment.description
|
||||
self.blurhash = attachment.blurhash
|
||||
self.meta = attachment.meta
|
||||
|
||||
self.data = nil
|
||||
self.exifCamera = nil
|
||||
self.exifCreatedDate = nil
|
||||
self.exifExposure = nil
|
||||
self.exifLens = nil
|
||||
|
||||
if let width = (attachment.meta as? ImageMetadata)?.original?.width {
|
||||
self.metaImageWidth = Int32(width)
|
||||
} else {
|
||||
self.metaImageWidth = nil
|
||||
}
|
||||
|
||||
if let height = (attachment.meta as? ImageMetadata)?.original?.height {
|
||||
self.metaImageHeight = Int32(height)
|
||||
} else {
|
||||
self.metaImageHeight = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func set(data: Data) {
|
||||
self.data = data
|
||||
|
||||
// Read exif information.
|
||||
if let exifProperties = self.data?.getExifData() {
|
||||
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
|
||||
self.exifCamera = "\(make) \(model)"
|
||||
}
|
||||
|
||||
// "Lens" or "Lens Model"
|
||||
if let lens = exifProperties.getExifValue("Lens") {
|
||||
self.exifLens = lens
|
||||
}
|
||||
|
||||
if let createData = exifProperties.getExifValue("CreateDate") {
|
||||
self.exifCreatedDate = createData
|
||||
}
|
||||
|
||||
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
|
||||
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
|
||||
let exposureTime = exifProperties.getExifValue("ExposureTime"),
|
||||
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
|
||||
self.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class StatusViewModel {
|
||||
|
||||
public let id: StatusId
|
||||
public let content: Html
|
||||
|
||||
public let uri: String?
|
||||
public let url: URL?
|
||||
public let account: Account
|
||||
public let inReplyToId: AccountId?
|
||||
public let inReplyToAccount: StatusId?
|
||||
public let reblog: Status?
|
||||
public let createdAt: String
|
||||
public let reblogsCount: Int
|
||||
public let favouritesCount: Int
|
||||
public let repliesCount: Int
|
||||
public let reblogged: Bool
|
||||
public let favourited: Bool
|
||||
public let sensitive: Bool
|
||||
public let bookmarked: Bool
|
||||
public let pinned: Bool
|
||||
public let muted: Bool
|
||||
public let spoilerText: String?
|
||||
public let visibility: Status.Visibility
|
||||
public let mediaAttachments: [AttachmentViewModel]
|
||||
public let card: Card?
|
||||
public let mentions: [Mention]
|
||||
public let tags: [Tag]
|
||||
public let application: Application?
|
||||
|
||||
public init(
|
||||
id: StatusId,
|
||||
content: Html,
|
||||
uri: String,
|
||||
account: Account,
|
||||
url: URL? = nil,
|
||||
inReplyToId: AccountId? = nil,
|
||||
inReplyToAccount: StatusId? = nil,
|
||||
reblog: Status? = nil,
|
||||
createdAt: String? = nil,
|
||||
reblogsCount: Int = 0,
|
||||
favouritesCount: Int = 0,
|
||||
repliesCount: Int = 0,
|
||||
reblogged: Bool = false,
|
||||
favourited: Bool = false,
|
||||
sensitive: Bool = false,
|
||||
bookmarked: Bool = false,
|
||||
pinned: Bool = false,
|
||||
muted: Bool = false,
|
||||
spoilerText: String? = nil,
|
||||
visibility: Status.Visibility = Status.Visibility.pub,
|
||||
mediaAttachments: [AttachmentViewModel] = [],
|
||||
card: Card? = nil,
|
||||
mentions: [Mention] = [],
|
||||
tags: [Tag] = [],
|
||||
application: Application
|
||||
) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
self.url = url
|
||||
self.account = account
|
||||
self.inReplyToId = inReplyToId
|
||||
self.inReplyToAccount = inReplyToAccount
|
||||
self.reblog = reblog
|
||||
self.createdAt = createdAt ?? Date().formatted(.iso8601)
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
self.repliesCount = repliesCount
|
||||
self.reblogged = reblogged
|
||||
self.favourited = favourited
|
||||
self.sensitive = sensitive
|
||||
self.bookmarked = bookmarked
|
||||
self.pinned = pinned
|
||||
self.muted = muted
|
||||
self.spoilerText = spoilerText
|
||||
self.visibility = visibility
|
||||
self.mediaAttachments = mediaAttachments
|
||||
self.card = card
|
||||
self.mentions = mentions
|
||||
self.tags = tags
|
||||
self.application = application
|
||||
}
|
||||
|
||||
init(status: Status) {
|
||||
self.id = status.id
|
||||
self.content = status.content
|
||||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.account = status.account
|
||||
self.inReplyToId = status.inReplyToId
|
||||
self.inReplyToAccount = status.inReplyToAccount
|
||||
self.reblog = status.reblog
|
||||
self.createdAt = status.createdAt
|
||||
self.reblogsCount = status.reblogsCount
|
||||
self.favouritesCount = status.favouritesCount
|
||||
self.repliesCount = status.repliesCount
|
||||
self.reblogged = status.reblogged
|
||||
self.favourited = status.favourited
|
||||
self.sensitive = status.sensitive
|
||||
self.bookmarked = status.bookmarked
|
||||
self.pinned = status.pinned
|
||||
self.muted = status.muted
|
||||
self.spoilerText = status.spoilerText
|
||||
self.visibility = status.visibility
|
||||
self.card = status.card
|
||||
self.mentions = status.mentions
|
||||
self.tags = status.tags
|
||||
self.application = status.application
|
||||
|
||||
var mediaAttachments: [AttachmentViewModel] = []
|
||||
for item in status.mediaAttachments {
|
||||
mediaAttachments.append(AttachmentViewModel(attachment: item))
|
||||
}
|
||||
|
||||
self.mediaAttachments = mediaAttachments
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusViewModel {
|
||||
func getImageWidth() -> Int32? {
|
||||
if let width = (self.mediaAttachments.first?.meta as? ImageMetadata)?.original?.width {
|
||||
return Int32(width)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getImageHeight() -> Int32? {
|
||||
if let height = (self.mediaAttachments.first?.meta as? ImageMetadata)?.original?.height {
|
||||
return Int32(height)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension [Status] {
|
||||
func toStatusViewModel() -> [StatusViewModel] {
|
||||
self.map { status in
|
||||
StatusViewModel(status: status)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,11 +8,17 @@ import SwiftUI
|
|||
import MastodonKit
|
||||
|
||||
struct ComposeView: View {
|
||||
enum FocusField: Hashable {
|
||||
case content
|
||||
}
|
||||
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Binding var status: Status?
|
||||
@Binding var statusViewModel: StatusViewModel?
|
||||
@State private var text = ""
|
||||
|
||||
@FocusState private var focusedField: FocusField?
|
||||
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
||||
|
||||
|
@ -31,14 +37,18 @@ struct ComposeView: View {
|
|||
}
|
||||
.padding(8)
|
||||
}
|
||||
|
||||
|
||||
TextField("Type what's on your mind", text: $text)
|
||||
.padding(8)
|
||||
|
||||
if let status = self.status {
|
||||
.focused($focusedField, equals: .content)
|
||||
.task {
|
||||
self.focusedField = .content
|
||||
}
|
||||
|
||||
if let status = self.statusViewModel {
|
||||
HStack (alignment: .top) {
|
||||
|
||||
AsyncImage(url: status.account?.avatar) { image in
|
||||
AsyncImage(url: status.account.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
|
@ -52,14 +62,14 @@ struct ComposeView: View {
|
|||
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
HStack (alignment: .top) {
|
||||
Text(self.getUserName(status: status))
|
||||
Text(self.getUserName(statusViewModel: status))
|
||||
.foregroundColor(.mainTextColor)
|
||||
.font(.footnote)
|
||||
.fontWeight(.bold)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
|
||||
.padding(.top, -4)
|
||||
.padding(.leading, -4)
|
||||
|
@ -68,7 +78,7 @@ struct ComposeView: View {
|
|||
.padding(8)
|
||||
.background(Color.selectedRowColor)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
@ -76,11 +86,15 @@ struct ComposeView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
dismiss()
|
||||
Task {
|
||||
await self.publishStatus()
|
||||
dismiss()
|
||||
}
|
||||
} label: {
|
||||
Text("Publish")
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.disabled(self.text.isEmpty)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
|
@ -95,13 +109,24 @@ struct ComposeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func getUserName(status: Status) -> String {
|
||||
return status.account?.displayName ?? status.account?.acct ?? status.account?.username ?? ""
|
||||
private func publishStatus() async {
|
||||
do {
|
||||
_ = try await StatusService.shared.new(
|
||||
status: Mastodon.Statuses.Components(inReplyToId: self.statusViewModel?.id, text: self.text),
|
||||
accountData: self.applicationState.accountData)
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func getUserName(statusViewModel: StatusViewModel) -> String {
|
||||
return self.statusViewModel?.account.displayName ?? self.statusViewModel?.account.acct ?? self.statusViewModel?.account.username ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ComposeView(status: .constant(Status(id: "", content: "", application: Application(name: ""))))
|
||||
Text("")
|
||||
// ComposeView(status: .constant(Status(id: "", content: "", application: Application(name: ""))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,10 @@ struct StatusView: View {
|
|||
@State var imageWidth: Int32?
|
||||
@State var imageHeight: Int32?
|
||||
|
||||
@State private var messageForStatus: Status?
|
||||
@State private var messageForStatus: StatusViewModel?
|
||||
@State private var showCompose = false
|
||||
|
||||
@State private var statusData: StatusData?
|
||||
@State private var status: Status?
|
||||
@State private var statusViewModel: StatusViewModel?
|
||||
|
||||
@State private var exifCamera: String?
|
||||
@State private var exifExposure: String?
|
||||
|
@ -28,9 +27,9 @@ struct StatusView: View {
|
|||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
if let statusData = self.statusData, let status = self.status {
|
||||
if let statusViewModel = self.statusViewModel {
|
||||
VStack (alignment: .leading) {
|
||||
ImagesCarousel(attachments: statusData.attachments(),
|
||||
ImagesCarousel(attachments: statusViewModel.mediaAttachments,
|
||||
exifCamera: $exifCamera,
|
||||
exifExposure: $exifExposure,
|
||||
exifCreatedDate: $exifCreatedDate,
|
||||
|
@ -38,16 +37,16 @@ struct StatusView: View {
|
|||
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(destination: UserProfileView(
|
||||
accountId: statusData.accountId,
|
||||
accountDisplayName: statusData.accountDisplayName,
|
||||
accountUserName: statusData.accountUsername)
|
||||
accountId: statusViewModel.account.id,
|
||||
accountDisplayName: statusViewModel.account.displayName,
|
||||
accountUserName: statusViewModel.account.username)
|
||||
.environmentObject(applicationState)) {
|
||||
UsernameRow(accountAvatar: statusData.accountAvatar,
|
||||
accountDisplayName: statusData.accountDisplayName,
|
||||
accountUsername: statusData.accountUsername)
|
||||
UsernameRow(accountAvatar: statusViewModel.account.avatar,
|
||||
accountDisplayName: statusViewModel.account.displayName,
|
||||
accountUsername: statusViewModel.account.username)
|
||||
}
|
||||
|
||||
HTMLFormattedText(statusData.content)
|
||||
HTMLFormattedText(statusViewModel.content)
|
||||
.padding(.leading, -4)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
|
@ -61,17 +60,17 @@ struct StatusView: View {
|
|||
|
||||
HStack {
|
||||
Text("Uploaded")
|
||||
Text(statusData.createdAt.toRelative(.isoDateTimeMilliSec))
|
||||
Text(statusViewModel.createdAt.toRelative(.isoDateTimeMilliSec))
|
||||
.padding(.horizontal, -4)
|
||||
if let applicationName = statusData.applicationName {
|
||||
if let applicationName = statusViewModel.application?.name {
|
||||
Text("via \(applicationName)")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
|
||||
InteractionRow(status: status) {
|
||||
self.messageForStatus = status
|
||||
InteractionRow(statusViewModel: statusViewModel) {
|
||||
self.messageForStatus = statusViewModel
|
||||
self.showCompose.toggle()
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
|
@ -79,7 +78,7 @@ struct StatusView: View {
|
|||
}
|
||||
.padding(8)
|
||||
|
||||
CommentsSection(statusId: statusData.id) { messageForStatus in
|
||||
CommentsSection(statusId: statusViewModel.id) { messageForStatus in
|
||||
self.messageForStatus = messageForStatus
|
||||
self.showCompose.toggle()
|
||||
}
|
||||
|
@ -121,23 +120,31 @@ struct StatusView: View {
|
|||
}
|
||||
.navigationBarTitle("Details")
|
||||
.sheet(isPresented: $showCompose, content: {
|
||||
ComposeView(status: $messageForStatus)
|
||||
ComposeView(statusViewModel: $messageForStatus)
|
||||
})
|
||||
.onAppear {
|
||||
Task {
|
||||
do {
|
||||
// Get status from API.
|
||||
self.status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData)
|
||||
|
||||
if let status {
|
||||
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
|
||||
let statusViewModel = StatusViewModel(status: status)
|
||||
|
||||
// Download images and recalculate exif data.
|
||||
let allImages = await TimelineService.shared.fetchAllImages(statuses: [status])
|
||||
for attachment in statusViewModel.mediaAttachments {
|
||||
if let data = allImages[attachment.id] {
|
||||
attachment.set(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
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 let statusDataFromDatabase {
|
||||
self.statusData = try await TimelineService.shared.updateStatus(statusDataFromDatabase, basedOn: status)
|
||||
} else {
|
||||
self.statusData = try await status.createStatusData()
|
||||
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, basedOn: status)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
@ -11,63 +11,61 @@ import MastodonKit
|
|||
struct CommentBody: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State var status: Status
|
||||
@State var statusViewModel: StatusViewModel
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
|
||||
if let account = status.account {
|
||||
NavigationLink(destination: UserProfileView(
|
||||
accountId: account.id,
|
||||
accountDisplayName: account.displayName,
|
||||
accountUserName: account.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
AsyncImage(url: account.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.foregroundColor(.mainTextColor)
|
||||
}
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
NavigationLink(destination: UserProfileView(
|
||||
accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayName,
|
||||
accountUserName: self.statusViewModel.account.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
AsyncImage(url: self.statusViewModel.account.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.foregroundColor(.mainTextColor)
|
||||
}
|
||||
}
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
}
|
||||
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
HStack (alignment: .top) {
|
||||
Text(self.getUserName(status: status))
|
||||
Text(self.getUserName(statusViewModel: statusViewModel))
|
||||
.foregroundColor(.mainTextColor)
|
||||
.font(.footnote)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(status.createdAt.toRelative(.isoDateTimeMilliSec))
|
||||
Text(self.statusViewModel.createdAt.toRelative(.isoDateTimeMilliSec))
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
}
|
||||
|
||||
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
|
||||
HTMLFormattedText(self.statusViewModel.content, withFontSize: 14, andWidth: contentWidth)
|
||||
.padding(.top, -4)
|
||||
.padding(.leading, -4)
|
||||
|
||||
if status.mediaAttachments.count > 0 {
|
||||
if self.statusViewModel.mediaAttachments.count > 0 {
|
||||
LazyVGrid(
|
||||
columns: status.mediaAttachments.count == 1 ? [GridItem(.flexible())]: [GridItem(.flexible()), GridItem(.flexible())],
|
||||
columns: self.statusViewModel.mediaAttachments.count == 1 ? [GridItem(.flexible())]: [GridItem(.flexible()), GridItem(.flexible())],
|
||||
alignment: .center,
|
||||
spacing: 4
|
||||
) {
|
||||
ForEach(status.mediaAttachments, id: \.id) { attachment in
|
||||
ForEach(self.statusViewModel.mediaAttachments, id: \.id) { attachment in
|
||||
AsyncImage(url: attachment.url) { image in
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
|
||||
.frame(height: self.statusViewModel.mediaAttachments.count == 1 ? 200 : 100)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: .mainTextColor.opacity(0.3), radius: 2)
|
||||
} placeholder: {
|
||||
|
@ -75,7 +73,7 @@ struct CommentBody: View {
|
|||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
|
||||
.frame(height: self.statusViewModel.mediaAttachments.count == 1 ? 200 : 100)
|
||||
.foregroundColor(.mainTextColor)
|
||||
.opacity(0.05)
|
||||
}
|
||||
|
@ -86,29 +84,30 @@ struct CommentBody: View {
|
|||
}
|
||||
.onTapGesture {
|
||||
withAnimation(.linear(duration: 0.3)) {
|
||||
if status.id == self.applicationState.showInteractionStatusId {
|
||||
if self.statusViewModel.id == self.applicationState.showInteractionStatusId {
|
||||
self.applicationState.showInteractionStatusId = ""
|
||||
} else {
|
||||
self.applicationState.showInteractionStatusId = status.id
|
||||
self.applicationState.showInteractionStatusId = self.statusViewModel.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.background(self.getSelectedRowColor(status: status))
|
||||
.background(self.getSelectedRowColor(statusViewModel: statusViewModel))
|
||||
}
|
||||
|
||||
private func getUserName(status: Status) -> String {
|
||||
return status.account?.displayName ?? status.account?.acct ?? status.account?.username ?? ""
|
||||
private func getUserName(statusViewModel: StatusViewModel) -> String {
|
||||
return statusViewModel.account.displayName ?? statusViewModel.account.acct
|
||||
}
|
||||
|
||||
private func getSelectedRowColor(status: Status) -> Color {
|
||||
return self.applicationState.showInteractionStatusId == status.id ? Color.selectedRowColor : Color.systemBackground
|
||||
private func getSelectedRowColor(statusViewModel: StatusViewModel) -> Color {
|
||||
return self.applicationState.showInteractionStatusId == statusViewModel.id ? Color.selectedRowColor : Color.systemBackground
|
||||
}
|
||||
}
|
||||
|
||||
struct CommentBody_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CommentBody(status: Status(id: "", content: "", application: Application(name: "")))
|
||||
Text("")
|
||||
// CommentBody(status: Status(id: "", content: "", application: Application(name: "")))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ struct CommentsSection: View {
|
|||
@State public var withDivider = true
|
||||
@State private var context: Context?
|
||||
|
||||
var onNewStatus: ((_ context: Status) -> Void)?
|
||||
var onNewStatus: ((_ context: StatusViewModel) -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let context = context {
|
||||
ForEach(context.descendants, id: \.id) { status in
|
||||
ForEach(context.descendants.toStatusViewModel(), id: \.id) { statusViewModel in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
if withDivider {
|
||||
|
@ -29,12 +29,12 @@ struct CommentsSection: View {
|
|||
.padding(0)
|
||||
}
|
||||
|
||||
CommentBody(status: status)
|
||||
CommentBody(statusViewModel: statusViewModel)
|
||||
|
||||
if self.applicationState.showInteractionStatusId == status.id {
|
||||
if self.applicationState.showInteractionStatusId == statusViewModel.id {
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
InteractionRow(status: status) {
|
||||
self.onNewStatus?(status)
|
||||
InteractionRow(statusViewModel: statusViewModel) {
|
||||
self.onNewStatus?(statusViewModel)
|
||||
}
|
||||
.foregroundColor(self.getInteractionRowTextColor())
|
||||
.padding(.horizontal, 16)
|
||||
|
@ -44,7 +44,7 @@ struct CommentsSection: View {
|
|||
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
|
||||
CommentsSection(statusId: status.id, withDivider: false) { context in
|
||||
CommentsSection(statusId: statusViewModel.id, withDivider: false) { context in
|
||||
self.onNewStatus?(context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,19 +9,19 @@ import MastodonKit
|
|||
import NukeUI
|
||||
|
||||
struct ImageRowAsync: View {
|
||||
@State public var status: Status
|
||||
@State public var statusViewModel: StatusViewModel
|
||||
|
||||
@State private var imageHeight = UIScreen.main.bounds.width
|
||||
@State private var imageWidth = UIScreen.main.bounds.width
|
||||
@State private var heightWasPrecalculated = true
|
||||
|
||||
var body: some View {
|
||||
if let attachment = status.mediaAttachments.first {
|
||||
if let attachment = statusViewModel.mediaAttachments.first {
|
||||
ZStack {
|
||||
LazyImage(url: attachment.url) { state in
|
||||
if let image = state.image {
|
||||
if self.status.sensitive {
|
||||
ContentWarning(blurhash: attachment.blurhash, spoilerText: self.status.spoilerText) {
|
||||
if self.statusViewModel.sensitive {
|
||||
ContentWarning(blurhash: attachment.blurhash, spoilerText: self.statusViewModel.spoilerText) {
|
||||
image
|
||||
}
|
||||
} else {
|
||||
|
@ -52,7 +52,7 @@ struct ImageRowAsync: View {
|
|||
self.recalculateSizeOfDownloadedImage(imageResponse: imageResponse)
|
||||
}
|
||||
|
||||
if let count = status.mediaAttachments.count, count > 1 {
|
||||
if let count = self.statusViewModel.mediaAttachments.count, count > 1 {
|
||||
BottomRight {
|
||||
Text("1 / \(count)")
|
||||
.padding(.horizontal, 6)
|
||||
|
@ -82,7 +82,7 @@ struct ImageRowAsync: View {
|
|||
}
|
||||
|
||||
private func recalculateSizeFromMetadata() {
|
||||
if let firstAttachment = self.status.mediaAttachments.first,
|
||||
if let firstAttachment = self.statusViewModel.mediaAttachments.first,
|
||||
let imgHeight = (firstAttachment.meta as? ImageMetadata)?.original?.height,
|
||||
let imgWidth = (firstAttachment.meta as? ImageMetadata)?.original?.width {
|
||||
let calculatedHeight = self.calculateHeight(width: Double(imgWidth), height: Double(imgHeight))
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ImagesCarousel: View {
|
||||
@State public var attachments: [AttachmentData]
|
||||
@State public var attachments: [AttachmentViewModel]
|
||||
@State private var height: Double = 0.0
|
||||
@State private var selectedAttachmentId = ""
|
||||
|
||||
|
@ -19,7 +19,7 @@ struct ImagesCarousel: View {
|
|||
var body: some View {
|
||||
TabView() {
|
||||
ForEach(attachments, id: \.id) { attachment in
|
||||
if let image = UIImage(data: attachment.data) {
|
||||
if let data = attachment.data, let image = UIImage(data: data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
@ -48,7 +48,7 @@ struct ImagesCarousel: View {
|
|||
var imageWidth = 0.0
|
||||
|
||||
for item in attachments {
|
||||
if let image = UIImage(data: item.data) {
|
||||
if let data = item.data, let image = UIImage(data: data) {
|
||||
if image.size.height > imageHeight {
|
||||
imageHeight = image.size.height
|
||||
imageWidth = image.size.width
|
||||
|
|
|
@ -10,7 +10,7 @@ import MastodonKit
|
|||
struct InteractionRow: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State var status: Status
|
||||
@State var statusViewModel: StatusViewModel
|
||||
|
||||
@State private var repliesCount = 0
|
||||
@State private var reblogged = false
|
||||
|
@ -38,8 +38,8 @@ struct InteractionRow: View {
|
|||
ActionButton {
|
||||
do {
|
||||
let status = self.reblogged
|
||||
? try await StatusService.shared.unboost(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.boost(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
? try await StatusService.shared.unboost(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.boost(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
|
||||
if let status {
|
||||
self.reblogsCount = status.reblogsCount == self.reblogsCount
|
||||
|
@ -64,8 +64,8 @@ struct InteractionRow: View {
|
|||
ActionButton {
|
||||
do {
|
||||
let status = self.favourited
|
||||
? try await StatusService.shared.unfavourite(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.favourite(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
? try await StatusService.shared.unfavourite(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.favourite(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
|
||||
if let status {
|
||||
self.favouritesCount = status.favouritesCount == self.favouritesCount
|
||||
|
@ -90,8 +90,8 @@ struct InteractionRow: View {
|
|||
ActionButton {
|
||||
do {
|
||||
_ = self.bookmarked
|
||||
? try await StatusService.shared.unbookmark(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.bookmark(statusId: self.status.id, accountData: self.applicationState.accountData)
|
||||
? try await StatusService.shared.unbookmark(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.bookmark(statusId: self.statusViewModel.id, accountData: self.applicationState.accountData)
|
||||
|
||||
self.bookmarked.toggle()
|
||||
} catch {
|
||||
|
@ -117,18 +117,19 @@ struct InteractionRow: View {
|
|||
}
|
||||
|
||||
private func refreshCounters() {
|
||||
self.repliesCount = self.status.repliesCount
|
||||
self.reblogged = self.status.reblogged
|
||||
self.reblogsCount = self.status.reblogsCount
|
||||
self.favourited = self.status.favourited
|
||||
self.favouritesCount = self.status.favouritesCount
|
||||
self.bookmarked = self.status.bookmarked
|
||||
self.repliesCount = self.statusViewModel.repliesCount
|
||||
self.reblogged = self.statusViewModel.reblogged
|
||||
self.reblogsCount = self.statusViewModel.reblogsCount
|
||||
self.favourited = self.statusViewModel.favourited
|
||||
self.favouritesCount = self.statusViewModel.favouritesCount
|
||||
self.bookmarked = self.statusViewModel.bookmarked
|
||||
}
|
||||
}
|
||||
|
||||
struct InteractionRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InteractionRow(status: Status(id: "", content: "", application: Application(name: "")))
|
||||
.previewLayout(.fixed(width: 300, height: 70))
|
||||
Text("")
|
||||
// InteractionRow(status: Status(id: "", content: "", application: Application(name: "")))
|
||||
// .previewLayout(.fixed(width: 300, height: 70))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,18 +14,19 @@ struct UserProfileStatuses: View {
|
|||
@State private var allItemsLoaded = false
|
||||
@State private var firstLoadFinished = false
|
||||
|
||||
@State private var statuses: [Status] = []
|
||||
@State private var statusViewModels: [StatusViewModel] = []
|
||||
private let defaultLimit = 20
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
if firstLoadFinished == true {
|
||||
ForEach(self.statuses, id: \.id) { item in
|
||||
ForEach(self.statusViewModels, id: \.id) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||
imageWidth: item.getImageWidth(),
|
||||
imageHeight: item.getImageHeight())
|
||||
.environmentObject(applicationState)) {
|
||||
ImageRowAsync(status: item)
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
|
||||
|
@ -62,26 +63,42 @@ struct UserProfileStatuses: View {
|
|||
}
|
||||
|
||||
private func loadStatuses() async throws {
|
||||
self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData)
|
||||
self.firstLoadFinished = true
|
||||
let statuses = try await AccountService.shared.getStatuses(
|
||||
forAccountId: self.accountId,
|
||||
andContext: self.applicationState.accountData,
|
||||
limit: self.defaultLimit)
|
||||
var inPlaceStatuses: [StatusViewModel] = []
|
||||
|
||||
for item in statuses {
|
||||
inPlaceStatuses.append(StatusViewModel(status: item))
|
||||
}
|
||||
|
||||
if self.statuses.count < 40 {
|
||||
self.firstLoadFinished = true
|
||||
self.statusViewModels.append(contentsOf: inPlaceStatuses)
|
||||
|
||||
if statuses.count < self.defaultLimit {
|
||||
self.allItemsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private func loadMoreStatuses() async throws {
|
||||
if let lastStatusId = self.statuses.last?.id {
|
||||
if let lastStatusId = self.statusViewModels.last?.id {
|
||||
let previousStatuses = try await AccountService.shared.getStatuses(
|
||||
forAccountId: self.accountId,
|
||||
andContext: self.applicationState.accountData,
|
||||
maxId: lastStatusId)
|
||||
maxId: lastStatusId,
|
||||
limit: self.defaultLimit)
|
||||
|
||||
if previousStatuses.count < 40 {
|
||||
if previousStatuses.count < self.defaultLimit {
|
||||
self.allItemsLoaded = true
|
||||
}
|
||||
|
||||
self.statuses.append(contentsOf: previousStatuses)
|
||||
|
||||
var inPlaceStatuses: [StatusViewModel] = []
|
||||
for item in previousStatuses {
|
||||
inPlaceStatuses.append(StatusViewModel(status: item))
|
||||
}
|
||||
|
||||
self.statusViewModels.append(contentsOf: inPlaceStatuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue