Add StatusViewModel

This commit is contained in:
Marcin Czachursk 2023-01-10 20:38:02 +01:00
parent c293ab8e84
commit 16dd600059
19 changed files with 470 additions and 278 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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