parent
8021868599
commit
964cef0ff5
@ -25,6 +25,7 @@ public struct Card: Codable, Sendable {
|
|||||||
public let blurhash: String?
|
public let blurhash: String?
|
||||||
/// Only present when returned from the trending links endpoint
|
/// Only present when returned from the trending links endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
public let authors: [Author]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
url: WebURL,
|
url: WebURL,
|
||||||
@ -40,7 +41,8 @@ public struct Card: Codable, Sendable {
|
|||||||
width: Int? = nil,
|
width: Int? = nil,
|
||||||
height: Int? = nil,
|
height: Int? = nil,
|
||||||
blurhash: String? = nil,
|
blurhash: String? = nil,
|
||||||
history: [History]? = nil
|
history: [History]? = nil,
|
||||||
|
authors: [Author] = []
|
||||||
) {
|
) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -56,6 +58,7 @@ public struct Card: Codable, Sendable {
|
|||||||
self.height = height
|
self.height = height
|
||||||
self.blurhash = blurhash
|
self.blurhash = blurhash
|
||||||
self.history = history
|
self.history = history
|
||||||
|
self.authors = authors
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -75,6 +78,7 @@ public struct Card: Codable, Sendable {
|
|||||||
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
||||||
self.blurhash = try? container.decodeIfPresent(String.self, forKey: .blurhash)
|
self.blurhash = try? container.decodeIfPresent(String.self, forKey: .blurhash)
|
||||||
self.history = try? container.decodeIfPresent([History].self, forKey: .history)
|
self.history = try? container.decodeIfPresent([History].self, forKey: .history)
|
||||||
|
self.authors = try container.decodeIfPresent([Author].self, forKey: .authors) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
@ -103,6 +107,7 @@ public struct Card: Codable, Sendable {
|
|||||||
case height
|
case height
|
||||||
case blurhash
|
case blurhash
|
||||||
case history
|
case history
|
||||||
|
case authors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,3 +119,29 @@ extension Card {
|
|||||||
case rich
|
case rich
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Card {
|
||||||
|
public struct Author: Decodable, Sendable {
|
||||||
|
public let name: String
|
||||||
|
public let url: WebURL?
|
||||||
|
public let account: Account?
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case name
|
||||||
|
case url
|
||||||
|
case account
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
|
let s = try container.decode(String.self, forKey: .url)
|
||||||
|
if s.isEmpty {
|
||||||
|
self.url = nil
|
||||||
|
} else {
|
||||||
|
self.url = WebURL(s)
|
||||||
|
}
|
||||||
|
self.account = try container.decodeIfPresent(Account.self, forKey: .account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -366,6 +366,7 @@
|
|||||||
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
|
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
||||||
|
D6F3BE142D6E133E00F5E92D /* StatusCardMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F3BE132D6E133C00F5E92D /* StatusCardMO.swift */; };
|
||||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */; };
|
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */; };
|
||||||
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; };
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; };
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; };
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; };
|
||||||
@ -806,6 +807,7 @@
|
|||||||
D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenSuggestionCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenSuggestionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
|
D6F3BE132D6E133C00F5E92D /* StatusCardMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardMO.swift; sourceTree = "<group>"; };
|
||||||
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBackgroundConfiguration+AppColors.swift"; sourceTree = "<group>"; };
|
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBackgroundConfiguration+AppColors.swift"; sourceTree = "<group>"; };
|
||||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = "<group>"; };
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = "<group>"; };
|
||||||
D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = "<group>"; };
|
D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = "<group>"; };
|
||||||
@ -1032,6 +1034,7 @@
|
|||||||
children = (
|
children = (
|
||||||
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */,
|
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */,
|
||||||
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
||||||
|
D6F3BE132D6E133C00F5E92D /* StatusCardMO.swift */,
|
||||||
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
||||||
@ -2279,6 +2282,7 @@
|
|||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
D659F36229541065002D944A /* TTTView.swift in Sources */,
|
D659F36229541065002D944A /* TTTView.swift in Sources */,
|
||||||
D65B4B6229771A3F00DABDFB /* FetchStatusService.swift in Sources */,
|
D65B4B6229771A3F00DABDFB /* FetchStatusService.swift in Sources */,
|
||||||
|
D6F3BE142D6E133E00F5E92D /* StatusCardMO.swift in Sources */,
|
||||||
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */,
|
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */,
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
||||||
|
68
Tusker/CoreData/StatusCardMO.swift
Normal file
68
Tusker/CoreData/StatusCardMO.swift
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// StatusCardMO.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/25/25.
|
||||||
|
// Copyright © 2025 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
|
@objc(StatusCardMO)
|
||||||
|
public final class StatusCardMO: NSManagedObject {
|
||||||
|
|
||||||
|
@NSManaged public var url: URL
|
||||||
|
@NSManaged public var title: String
|
||||||
|
@NSManaged public var cardDescription: String
|
||||||
|
@NSManaged private var kindString: String
|
||||||
|
@NSManaged public var image: URL?
|
||||||
|
@NSManaged public var blurhash: String?
|
||||||
|
|
||||||
|
@NSManaged public var authors: NSSet
|
||||||
|
@NSManaged public var status: StatusMO
|
||||||
|
|
||||||
|
public var authorAccounts: Set<AccountMO> {
|
||||||
|
authors as! Set<AccountMO>
|
||||||
|
}
|
||||||
|
|
||||||
|
public var kind: Card.Kind {
|
||||||
|
get { .init(rawValue: kindString) ?? .link }
|
||||||
|
set { kindString = newValue.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusCardMO {
|
||||||
|
convenience init(apiCard card: Card, status: StatusMO, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
||||||
|
self.init(context: context)
|
||||||
|
self.updateFrom(apiCard: card, container: container)
|
||||||
|
self.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFrom(apiCard card: Card, container: MastodonCachePersistentStore) {
|
||||||
|
guard let context = managedObjectContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.url = URL(card.url)!
|
||||||
|
self.title = card.title
|
||||||
|
self.cardDescription = card.description
|
||||||
|
self.kind = card.kind
|
||||||
|
self.image = card.image.flatMap { URL($0) }
|
||||||
|
self.blurhash = card.blurhash
|
||||||
|
|
||||||
|
let authors = NSMutableSet()
|
||||||
|
for account in card.authors.compactMap(\.account) {
|
||||||
|
if let existing = container.account(for: account.id, in: context) {
|
||||||
|
authors.add(existing)
|
||||||
|
} else {
|
||||||
|
let new = AccountMO(apiAccount: account, container: container, context: context)
|
||||||
|
authors.add(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.authors = authors
|
||||||
|
}
|
||||||
|
}
|
@ -50,12 +50,14 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||||||
@NSManaged public var url: URL?
|
@NSManaged public var url: URL?
|
||||||
@NSManaged private var visibilityString: String
|
@NSManaged private var visibilityString: String
|
||||||
@NSManaged private var pollData: Data?
|
@NSManaged private var pollData: Data?
|
||||||
@NSManaged public var account: AccountMO
|
|
||||||
@NSManaged public var reblog: StatusMO?
|
|
||||||
@NSManaged public var localOnly: Bool
|
@NSManaged public var localOnly: Bool
|
||||||
@NSManaged public var lastFetchedAt: Date?
|
@NSManaged public var lastFetchedAt: Date?
|
||||||
@NSManaged public var language: String?
|
@NSManaged public var language: String?
|
||||||
|
|
||||||
|
@NSManaged public var account: AccountMO
|
||||||
|
@NSManaged public var reblog: StatusMO?
|
||||||
|
@NSManaged public var card: StatusCardMO?
|
||||||
|
|
||||||
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
||||||
public var attachments: [Attachment]
|
public var attachments: [Attachment]
|
||||||
|
|
||||||
@ -68,8 +70,9 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||||||
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
|
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
|
||||||
public var mentions: [Mention]
|
public var mentions: [Mention]
|
||||||
|
|
||||||
|
// The card deserialized from cardData. This is only kept around for when migrating forward to the version that added StatusCardMO.
|
||||||
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
||||||
public var card: Card?
|
public var deprecatedCard: Card?
|
||||||
|
|
||||||
@LazilyDecoding(from: \StatusMO.pollData, fallback: nil)
|
@LazilyDecoding(from: \StatusMO.pollData, fallback: nil)
|
||||||
public var poll: Poll?
|
public var poll: Poll?
|
||||||
@ -117,7 +120,6 @@ extension StatusMO {
|
|||||||
self.applicationName = status.application?.name
|
self.applicationName = status.application?.name
|
||||||
self.attachments = status.attachments
|
self.attachments = status.attachments
|
||||||
self.bookmarkedInternal = status.bookmarked ?? false
|
self.bookmarkedInternal = status.bookmarked ?? false
|
||||||
self.card = status.card
|
|
||||||
self.content = status.content
|
self.content = status.content
|
||||||
self.createdAt = status.createdAt
|
self.createdAt = status.createdAt
|
||||||
self.editedAt = status.editedAt
|
self.editedAt = status.editedAt
|
||||||
@ -158,5 +160,19 @@ extension StatusMO {
|
|||||||
} else {
|
} else {
|
||||||
self.reblog = nil
|
self.reblog = nil
|
||||||
}
|
}
|
||||||
|
if let card = status.card {
|
||||||
|
if let existing = self.card {
|
||||||
|
existing.updateFrom(apiCard: card, container: container)
|
||||||
|
} else {
|
||||||
|
let new = StatusCardMO(apiCard: card, status: self, container: container, context: context)
|
||||||
|
self.card = new
|
||||||
|
}
|
||||||
|
self.deprecatedCard = nil
|
||||||
|
} else {
|
||||||
|
if let existing = self.card {
|
||||||
|
context.delete(existing)
|
||||||
|
self.card = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23B92" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D70" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="active" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="active" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
@ -20,6 +20,7 @@
|
|||||||
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="url" attributeType="URI"/>
|
<attribute name="url" attributeType="URI"/>
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
|
<relationship name="authoredCards" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StatusCard" inverseName="authors" inverseEntity="StatusCard"/>
|
||||||
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
|
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
|
||||||
<relationship name="relationship" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/>
|
<relationship name="relationship" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/>
|
||||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="account" inverseEntity="Status"/>
|
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="account" inverseEntity="Status"/>
|
||||||
@ -125,6 +126,7 @@
|
|||||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="visibilityString" attributeType="String"/>
|
<attribute name="visibilityString" attributeType="String"/>
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
||||||
|
<relationship name="card" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="StatusCard" inverseName="status" inverseEntity="StatusCard"/>
|
||||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogs" inverseEntity="Status"/>
|
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogs" inverseEntity="Status"/>
|
||||||
<relationship name="reblogs" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
|
<relationship name="reblogs" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
|
||||||
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineState" inverseName="statuses" inverseEntity="TimelineState"/>
|
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineState" inverseName="statuses" inverseEntity="TimelineState"/>
|
||||||
@ -134,6 +136,16 @@
|
|||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="StatusCard" representedClassName="StatusCardMO" syncable="YES">
|
||||||
|
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="cardDescription" attributeType="String"/>
|
||||||
|
<attribute name="image" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="kindString" attributeType="String"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<attribute name="url" attributeType="URI"/>
|
||||||
|
<relationship name="authors" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Account" inverseName="authoredCards" inverseEntity="Account"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
<entity name="TimelinePosition" representedClassName="TimelinePosition" syncable="YES">
|
<entity name="TimelinePosition" representedClassName="TimelinePosition" syncable="YES">
|
||||||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="centerStatusID" optional="YES" attributeType="String"/>
|
<attribute name="centerStatusID" optional="YES" attributeType="String"/>
|
||||||
@ -162,5 +174,6 @@
|
|||||||
<memberEntity name="List"/>
|
<memberEntity name="List"/>
|
||||||
<memberEntity name="Account"/>
|
<memberEntity name="Account"/>
|
||||||
<memberEntity name="ActiveInstance"/>
|
<memberEntity name="ActiveInstance"/>
|
||||||
|
<memberEntity name="StatusCard"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
</model>
|
</model>
|
@ -254,7 +254,6 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||||||
|
|
||||||
private func showRecommendationsError(_ error: Client.ErrorType) {
|
private func showRecommendationsError(_ error: Client.ErrorType) {
|
||||||
let footer = UITableViewHeaderFooterView()
|
let footer = UITableViewHeaderFooterView()
|
||||||
footer.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -46,7 +46,6 @@ struct MockStatusView: View {
|
|||||||
|
|
||||||
if preferences.showLinkPreviews {
|
if preferences.showLinkPreviews {
|
||||||
MockStatusCardView()
|
MockStatusCardView()
|
||||||
.frame(height: StatusContentContainer.cardViewHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MockAttachmentsContainerView()
|
MockAttachmentsContainerView()
|
||||||
@ -136,8 +135,8 @@ private struct MockStatusCardView: UIViewRepresentable {
|
|||||||
let view = StatusCardView()
|
let view = StatusCardView()
|
||||||
view.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
let card = StatusCardView.CardData(
|
let card = StatusCardView.CardData(
|
||||||
url: WebURL("https://vaccor.space/tusker")!,
|
url: URL(string: "https://vaccor.space/tusker")!,
|
||||||
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
image: URL(string: "https://vaccor.space/tusker/img/icon.png")!,
|
||||||
title: "Tusker",
|
title: "Tusker",
|
||||||
description: "Tusker is an iOS app for Mastodon"
|
description: "Tusker is an iOS app for Mastodon"
|
||||||
)
|
)
|
||||||
|
@ -79,9 +79,7 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
|||||||
$0.isSelectable = false
|
$0.isSelectable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private let cardView = StatusCardView().configure {
|
private let cardView = StatusCardView()
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private let attachmentsView = AttachmentsContainerView()
|
private let attachmentsView = AttachmentsContainerView()
|
||||||
|
|
||||||
|
@ -151,9 +151,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||||||
|
|
||||||
private var translateButton: TranslateButton?
|
private var translateButton: TranslateButton?
|
||||||
|
|
||||||
let cardView = StatusCardView().configure {
|
let cardView = StatusCardView()
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachmentsView = AttachmentsContainerView()
|
let attachmentsView = AttachmentsContainerView()
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import HTMLStreamer
|
|||||||
|
|
||||||
class StatusCardView: UIView {
|
class StatusCardView: UIView {
|
||||||
|
|
||||||
|
fileprivate static var cardHeight: CGFloat { 80 }
|
||||||
|
|
||||||
weak var navigationDelegate: TuskerNavigationDelegate?
|
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||||
weak var actionProvider: MenuActionProvider?
|
weak var actionProvider: MenuActionProvider?
|
||||||
|
|
||||||
@ -35,6 +37,10 @@ class StatusCardView: UIView {
|
|||||||
private var placeholderImageView: UIImageView!
|
private var placeholderImageView: UIImageView!
|
||||||
private var leadingSpacer: UIView!
|
private var leadingSpacer: UIView!
|
||||||
private var trailingSpacer: UIView!
|
private var trailingSpacer: UIView!
|
||||||
|
private var authorContainerVStack: UIStackView!
|
||||||
|
private var authorHStack: UIStackView?
|
||||||
|
private var authorAvatarImageView: CachedImageView?
|
||||||
|
private var authorDisplayNameLabel: EmojiLabel?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -47,24 +53,13 @@ class StatusCardView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func commonInit() {
|
private func commonInit() {
|
||||||
self.layer.shadowColor = UIColor.black.cgColor
|
|
||||||
self.layer.shadowRadius = 5
|
|
||||||
self.layer.shadowOpacity = 0.2
|
|
||||||
self.layer.shadowOffset = .zero
|
|
||||||
|
|
||||||
self.addInteraction(UIContextMenuInteraction(delegate: self))
|
|
||||||
|
|
||||||
titleLabel = UILabel()
|
titleLabel = UILabel()
|
||||||
titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0)
|
titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
titleLabel.adjustsFontForContentSizeCategory = true
|
titleLabel.adjustsFontForContentSizeCategory = true
|
||||||
titleLabel.numberOfLines = 2
|
|
||||||
titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
||||||
|
|
||||||
descriptionLabel = UILabel()
|
descriptionLabel = UILabel()
|
||||||
descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0)
|
descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0)
|
||||||
descriptionLabel.adjustsFontForContentSizeCategory = true
|
descriptionLabel.adjustsFontForContentSizeCategory = true
|
||||||
descriptionLabel.numberOfLines = 3
|
|
||||||
descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
||||||
|
|
||||||
domainLabel = UILabel()
|
domainLabel = UILabel()
|
||||||
domainLabel.font = .preferredFont(forTextStyle: .caption2)
|
domainLabel.font = .preferredFont(forTextStyle: .caption2)
|
||||||
@ -83,7 +78,8 @@ class StatusCardView: UIView {
|
|||||||
])
|
])
|
||||||
vStack.axis = .vertical
|
vStack.axis = .vertical
|
||||||
vStack.alignment = .leading
|
vStack.alignment = .leading
|
||||||
vStack.spacing = 0
|
vStack.distribution = .equalSpacing
|
||||||
|
vStack.spacing = 2
|
||||||
|
|
||||||
imageView = StatusCardImageView(cache: .attachments)
|
imageView = StatusCardImageView(cache: .attachments)
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
@ -100,29 +96,38 @@ class StatusCardView: UIView {
|
|||||||
vStack,
|
vStack,
|
||||||
trailingSpacer,
|
trailingSpacer,
|
||||||
])
|
])
|
||||||
hStack.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
hStack.axis = .horizontal
|
hStack.axis = .horizontal
|
||||||
hStack.alignment = .center
|
hStack.alignment = .center
|
||||||
hStack.distribution = .fill
|
hStack.distribution = .fill
|
||||||
hStack.spacing = 4
|
hStack.spacing = 4
|
||||||
hStack.clipsToBounds = true
|
hStack.clipsToBounds = true
|
||||||
hStack.layer.borderWidth = 0.5
|
hStack.layer.borderWidth = 0.5
|
||||||
|
hStack.layer.cornerRadius = 5
|
||||||
hStack.layer.cornerCurve = .continuous
|
hStack.layer.cornerCurve = .continuous
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
||||||
updateBorderColor()
|
updateBorderColor()
|
||||||
|
|
||||||
addSubview(hStack)
|
addSubview(hStack)
|
||||||
|
hStack.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cardTapped)))
|
||||||
|
hStack.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
|
||||||
placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text"))
|
placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text"))
|
||||||
placeholderImageView.translatesAutoresizingMaskIntoConstraints = false
|
placeholderImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
placeholderImageView.contentMode = .scaleAspectFit
|
placeholderImageView.contentMode = .scaleAspectFit
|
||||||
placeholderImageView.tintColor = .gray
|
placeholderImageView.tintColor = .gray
|
||||||
placeholderImageView.isHidden = true
|
placeholderImageView.isHidden = true
|
||||||
|
|
||||||
addSubview(placeholderImageView)
|
addSubview(placeholderImageView)
|
||||||
|
|
||||||
|
authorContainerVStack = UIStackView(arrangedSubviews: [
|
||||||
|
hStack,
|
||||||
|
])
|
||||||
|
authorContainerVStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainerVStack.axis = .vertical
|
||||||
|
authorContainerVStack.alignment = .leading
|
||||||
|
authorContainerVStack.spacing = 4
|
||||||
|
addSubview(authorContainerVStack)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
imageView.heightAnchor.constraint(equalTo: heightAnchor),
|
imageView.heightAnchor.constraint(equalToConstant: Self.cardHeight),
|
||||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
|
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
|
||||||
|
|
||||||
vStack.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor, constant: -8),
|
vStack.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor, constant: -8),
|
||||||
@ -130,30 +135,39 @@ class StatusCardView: UIView {
|
|||||||
leadingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
leadingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
||||||
trailingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
trailingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
||||||
|
|
||||||
hStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
hStack.heightAnchor.constraint(equalToConstant: Self.cardHeight),
|
||||||
hStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
hStack.widthAnchor.constraint(equalTo: widthAnchor),
|
||||||
hStack.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
hStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
|
|
||||||
placeholderImageView.widthAnchor.constraint(equalToConstant: 30),
|
placeholderImageView.widthAnchor.constraint(equalToConstant: 30),
|
||||||
placeholderImageView.heightAnchor.constraint(equalToConstant: 30),
|
placeholderImageView.heightAnchor.constraint(equalToConstant: 30),
|
||||||
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
||||||
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
||||||
|
|
||||||
|
authorContainerVStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
authorContainerVStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
authorContainerVStack.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
authorContainerVStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
updateContentSize()
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
registerForTraitChanges([UITraitPreferredContentSizeCategory.self], action: #selector(updateContentSize))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unneeded on visionOS because there is no light/dark mode
|
// Unneeded on visionOS because there is no light/dark mode
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
|
||||||
updateBorderColor()
|
updateBorderColor()
|
||||||
}
|
}
|
||||||
#endif
|
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||||
|
updateContentSize()
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
hStack.layer.cornerRadius = 0.1 * bounds.height
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private func updateBorderColor() {
|
private func updateBorderColor() {
|
||||||
if traitCollection.userInterfaceStyle == .dark {
|
if traitCollection.userInterfaceStyle == .dark {
|
||||||
@ -163,8 +177,21 @@ class StatusCardView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
@objc private func updateContentSize() {
|
||||||
let newData = status.card.map { CardData(card: $0) }
|
let category = traitCollection.preferredContentSizeCategory
|
||||||
|
titleLabel.numberOfLines = if category > .extraExtraExtraLarge {
|
||||||
|
2
|
||||||
|
} else if category > .extraLarge {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
descriptionLabel.isHidden = category > .extraExtraExtraLarge || (descriptionLabel.text?.isEmpty ?? true)
|
||||||
|
domainLabel.isHidden = category > .accessibilityMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(status: StatusMO, persistentContainer: MastodonCachePersistentStore) {
|
||||||
|
let newData = status.card.map { CardData(card: $0) } ?? status.deprecatedCard.map { CardData(card: $0) }
|
||||||
guard self.card != newData else {
|
guard self.card != newData else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -176,6 +203,13 @@ class StatusCardView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUI(card: newData, sensitive: status.sensitive)
|
updateUI(card: newData, sensitive: status.sensitive)
|
||||||
|
|
||||||
|
if let authorID = newData.authorID,
|
||||||
|
let account = persistentContainer.account(for: authorID) {
|
||||||
|
createCardAuthorView(account: account)
|
||||||
|
} else {
|
||||||
|
authorHStack?.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method is internal for use by MockStatusView
|
// This method is internal for use by MockStatusView
|
||||||
@ -184,14 +218,14 @@ class StatusCardView: UIView {
|
|||||||
if sensitive {
|
if sensitive {
|
||||||
if let blurhash = card.blurhash {
|
if let blurhash = card.blurhash {
|
||||||
imageView.blurImage = false
|
imageView.blurImage = false
|
||||||
imageView.showOnlyBlurHash(blurhash, for: URL(image)!)
|
imageView.showOnlyBlurHash(blurhash, for: image)
|
||||||
} else {
|
} else {
|
||||||
// if we don't have a blurhash, load the image and show it behind a blur
|
// if we don't have a blurhash, load the image and show it behind a blur
|
||||||
imageView.blurImage = true
|
imageView.blurImage = true
|
||||||
imageView.update(for: URL(image), blurhash: nil)
|
imageView.update(for: image, blurhash: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
imageView.update(for: URL(image), blurhash: card.blurhash)
|
imageView.update(for: image, blurhash: card.blurhash)
|
||||||
}
|
}
|
||||||
imageView.isHidden = false
|
imageView.isHidden = false
|
||||||
leadingSpacer.isHidden = true
|
leadingSpacer.isHidden = true
|
||||||
@ -208,66 +242,124 @@ class StatusCardView: UIView {
|
|||||||
let converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
let converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
let description = converter.convert(html: card.description)
|
let description = converter.convert(html: card.description)
|
||||||
descriptionLabel.text = description
|
descriptionLabel.text = description
|
||||||
descriptionLabel.isHidden = description.isEmpty
|
descriptionLabel.isHidden = description.isEmpty || traitCollection.preferredContentSizeCategory > .extraExtraExtraLarge
|
||||||
|
|
||||||
if let host = card.url.host {
|
if let host = card.url.host {
|
||||||
domainLabel.text = host.serialized
|
domainLabel.text = host
|
||||||
domainLabel.isHidden = false
|
domainLabel.isHidden = false
|
||||||
} else {
|
} else {
|
||||||
domainLabel.isHidden = true
|
domainLabel.isHidden = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let titleHeight = titleLabel.isHidden ? 0 : titleLabel.sizeThatFits(CGSize(width: titleLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
private func createCardAuthorView(account: AccountMO) {
|
||||||
let descriptionHeight = descriptionLabel.isHidden ? 0 : descriptionLabel.sizeThatFits(CGSize(width: descriptionLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
if let authorHStack {
|
||||||
let domainLabel = domainLabel.isHidden ? 0 : domainLabel.sizeThatFits(CGSize(width: domainLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
authorHStack.isHidden = false
|
||||||
if titleHeight + descriptionHeight + domainLabel > vStack.bounds.height {
|
} else {
|
||||||
descriptionLabel.isHidden = true
|
let moreFromLabel = UILabel()
|
||||||
|
moreFromLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
moreFromLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
moreFromLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
moreFromLabel.text = "More from"
|
||||||
|
moreFromLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let avatarImageView = CachedImageView(cache: .avatars)
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarImageView.layer.cornerCurve = .continuous
|
||||||
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 25
|
||||||
|
authorAvatarImageView = avatarImageView
|
||||||
|
|
||||||
|
let displayNameLabel = EmojiLabel()
|
||||||
|
displayNameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
displayNameLabel.font = .preferredFont(forTextStyle: .subheadline).withTraits(.traitBold)
|
||||||
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
displayNameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
authorDisplayNameLabel = displayNameLabel
|
||||||
|
|
||||||
|
let disclosureIndicator = UIImageView()
|
||||||
|
disclosureIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
disclosureIndicator.image = UIImage(systemName: "chevron.forward")
|
||||||
|
disclosureIndicator.tintColor = .secondaryLabel
|
||||||
|
disclosureIndicator.preferredSymbolConfiguration = .init(weight: .light)
|
||||||
|
|
||||||
|
let hStack = UIStackView(arrangedSubviews: [
|
||||||
|
moreFromLabel,
|
||||||
|
avatarImageView,
|
||||||
|
displayNameLabel,
|
||||||
|
disclosureIndicator,
|
||||||
|
])
|
||||||
|
hStack.axis = .horizontal
|
||||||
|
hStack.spacing = 8
|
||||||
|
hStack.alignment = .center
|
||||||
|
authorHStack = hStack
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: 25),
|
||||||
|
avatarImageView.heightAnchor.constraint(equalToConstant: 25),
|
||||||
|
])
|
||||||
|
|
||||||
|
authorContainerVStack.addArrangedSubview(hStack)
|
||||||
|
|
||||||
|
authorHStack?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(authorTapped)))
|
||||||
|
}
|
||||||
|
|
||||||
|
authorAvatarImageView!.update(for: account.avatar)
|
||||||
|
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||||
|
authorDisplayNameLabel!.text = account.displayNameWithoutCustomEmoji
|
||||||
|
authorDisplayNameLabel!.removeEmojis()
|
||||||
|
} else {
|
||||||
|
authorDisplayNameLabel!.text = account.displayOrUserName
|
||||||
|
authorDisplayNameLabel!.setEmojis(account.emojis, identifier: account.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
@objc private func cardTapped() {
|
||||||
hStack.backgroundColor = StatusCardView.activeBackgroundColor
|
if let card, let navigationDelegate {
|
||||||
setNeedsDisplay()
|
navigationDelegate.selected(url: card.url)
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
|
||||||
setNeedsDisplay()
|
|
||||||
|
|
||||||
if let card = card, let delegate = navigationDelegate {
|
|
||||||
delegate.selected(url: URL(card.url)!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
@objc private func authorTapped() {
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
if let card,
|
||||||
setNeedsDisplay()
|
let authorID = card.authorID,
|
||||||
|
let navigationDelegate {
|
||||||
|
navigationDelegate.selected(account: authorID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CardData: Equatable {
|
struct CardData: Equatable {
|
||||||
let url: WebURL
|
let url: URL
|
||||||
let image: WebURL?
|
let image: URL?
|
||||||
let title: String
|
let title: String
|
||||||
let description: String
|
let description: String
|
||||||
let blurhash: String?
|
let blurhash: String?
|
||||||
|
let authorID: String?
|
||||||
|
|
||||||
init(card: Card) {
|
init(card: Card) {
|
||||||
self.url = card.url
|
self.url = URL(card.url)!
|
||||||
self.image = card.image
|
self.image = card.image.flatMap { URL($0) }
|
||||||
self.title = card.title
|
self.title = card.title
|
||||||
self.description = card.description
|
self.description = card.description
|
||||||
self.blurhash = card.blurhash
|
self.blurhash = card.blurhash
|
||||||
|
self.authorID = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
init(url: WebURL, image: WebURL? = nil, title: String, description: String, blurhash: String? = nil) {
|
init(card: StatusCardMO) {
|
||||||
|
self.url = card.url
|
||||||
|
self.image = card.image
|
||||||
|
self.title = card.title
|
||||||
|
self.description = card.cardDescription
|
||||||
|
self.blurhash = card.blurhash
|
||||||
|
self.authorID = card.authorAccounts.sorted(by: { $0.id < $1.id }).first?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
init(url: URL, image: URL? = nil, title: String, description: String, blurhash: String? = nil) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.image = image
|
self.image = image
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
self.blurhash = blurhash
|
self.blurhash = blurhash
|
||||||
|
self.authorID = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +369,7 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
|
|||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let card = card else { return nil }
|
guard let card = card else { return nil }
|
||||||
|
|
||||||
return self.actionProvider?.contextMenuConfigurationForURL(URL(card.url)!, source: .view(self))
|
return self.actionProvider?.contextMenuConfigurationForURL(card.url, source: .view(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
@ -301,6 +393,12 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusCardView: StatusContentView {
|
||||||
|
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||||
|
Self.cardHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class StatusCardImageView: CachedImageView {
|
private class StatusCardImageView: CachedImageView {
|
||||||
@Lazy private var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
@Lazy private var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||||
var blurImage = false {
|
var blurImage = false {
|
||||||
|
@ -102,7 +102,7 @@ extension StatusCollectionViewCell {
|
|||||||
pollView.delegate = delegate
|
pollView.delegate = delegate
|
||||||
pollView.updateUI(status: status, poll: status.poll)
|
pollView.updateUI(status: status, poll: status.poll)
|
||||||
if Preferences.shared.showLinkPreviews {
|
if Preferences.shared.showLinkPreviews {
|
||||||
cardView.updateUI(status: status)
|
cardView.updateUI(status: status, persistentContainer: mastodonController.persistentContainer)
|
||||||
cardView.isHidden = status.card == nil
|
cardView.isHidden = status.card == nil
|
||||||
cardView.navigationDelegate = delegate
|
cardView.navigationDelegate = delegate
|
||||||
cardView.actionProvider = delegate
|
cardView.actionProvider = delegate
|
||||||
|
@ -9,9 +9,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class StatusContentContainer: UIView {
|
class StatusContentContainer: UIView {
|
||||||
// TODO: this is a weird place for this
|
|
||||||
static var cardViewHeight: CGFloat { 90 }
|
|
||||||
|
|
||||||
private var arrangedSubviews: [any StatusContentView]
|
private var arrangedSubviews: [any StatusContentView]
|
||||||
|
|
||||||
private var isHiddenObservations: [NSKeyValueObservation] = []
|
private var isHiddenObservations: [NSKeyValueObservation] = []
|
||||||
@ -206,12 +203,6 @@ extension ContentTextView: StatusContentView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusCardView: StatusContentView {
|
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
|
||||||
StatusContentContainer.cardViewHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachmentsContainerView: StatusContentView {
|
extension AttachmentsContainerView: StatusContentView {
|
||||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
||||||
effectiveWidth / aspectRatio
|
effectiveWidth / aspectRatio
|
||||||
|
@ -196,9 +196,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||||||
$0.emojiFont = TimelineStatusCollectionViewCell.contentFont
|
$0.emojiFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
}
|
}
|
||||||
|
|
||||||
let cardView = StatusCardView().configure {
|
let cardView = StatusCardView()
|
||||||
$0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachmentsView = AttachmentsContainerView()
|
let attachmentsView = AttachmentsContainerView()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user