Compare commits
3 Commits
5d9f4b8ea8
...
93828830a9
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 93828830a9 | |
Shadowfacts | 39b244384b | |
Shadowfacts | 80b3585b71 |
|
@ -22,8 +22,8 @@ public final class Account: AccountProtocol, Decodable {
|
|||
public let url: URL
|
||||
public let avatar: URL
|
||||
public let avatarStatic: URL
|
||||
public let header: URL
|
||||
public let headerStatic: URL
|
||||
public let header: URL?
|
||||
public let headerStatic: URL?
|
||||
public private(set) var emojis: [Emoji]
|
||||
public let moved: Bool?
|
||||
public let movedTo: Account?
|
||||
|
@ -46,8 +46,8 @@ public final class Account: AccountProtocol, Decodable {
|
|||
self.url = try container.decode(URL.self, forKey: .url)
|
||||
self.avatar = try container.decode(URL.self, forKey: .avatar)
|
||||
self.avatarStatic = try container.decode(URL.self, forKey: .avatarStatic)
|
||||
self.header = try container.decode(URL.self, forKey: .header)
|
||||
self.headerStatic = try container.decode(URL.self, forKey: .headerStatic)
|
||||
self.header = try? container.decode(URL.self, forKey: .header)
|
||||
self.headerStatic = try? container.decode(URL.self, forKey: .headerStatic)
|
||||
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
|
||||
self.bot = try? container.decode(Bool.self, forKey: .bot)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Card: Decodable {
|
||||
public class Card: Codable {
|
||||
public let url: URL
|
||||
public let title: String
|
||||
public let description: String
|
||||
|
@ -21,6 +21,7 @@ public class Card: Decodable {
|
|||
public let html: String?
|
||||
public let width: Int?
|
||||
public let height: Int?
|
||||
public let blurhash: String?
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
@ -29,14 +30,26 @@ public class Card: Decodable {
|
|||
self.title = try container.decode(String.self, forKey: .title)
|
||||
self.description = try container.decode(String.self, forKey: .description)
|
||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||
self.image = try? container.decode(URL.self, forKey: .image)
|
||||
self.authorName = try? container.decode(String.self, forKey: .authorName)
|
||||
self.authorURL = try? container.decode(URL.self, forKey: .authorURL)
|
||||
self.providerName = try? container.decode(String.self, forKey: .providerName)
|
||||
self.providerURL = try? container.decode(URL.self, forKey: .providerURL)
|
||||
self.html = try? container.decode(String.self, forKey: .html)
|
||||
self.width = try? container.decode(Int.self, forKey: .width)
|
||||
self.height = try? container.decode(Int.self, forKey: .height)
|
||||
self.image = try? container.decodeIfPresent(URL.self, forKey: .image)
|
||||
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
|
||||
self.authorURL = try? container.decodeIfPresent(URL.self, forKey: .authorURL)
|
||||
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
|
||||
self.providerURL = try? container.decodeIfPresent(URL.self, forKey: .providerURL)
|
||||
self.html = try? container.decodeIfPresent(String.self, forKey: .html)
|
||||
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
|
||||
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
||||
self.blurhash = try? container.decodeIfPresent(String.self, forKey: .blurhash)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(url, forKey: .url)
|
||||
try container.encode(title, forKey: .title)
|
||||
try container.encode(description, forKey: .description)
|
||||
try container.encode(kind, forKey: .kind)
|
||||
try container.encode(image, forKey: .image)
|
||||
try container.encode(blurhash, forKey: .blurhash)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
|
@ -52,14 +65,14 @@ public class Card: Decodable {
|
|||
case html
|
||||
case width
|
||||
case height
|
||||
case blurhash
|
||||
}
|
||||
}
|
||||
|
||||
extension Card {
|
||||
public enum Kind: String, Decodable {
|
||||
public enum Kind: String, Codable {
|
||||
case link
|
||||
case photo
|
||||
case video
|
||||
case rich
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ import Foundation
|
|||
|
||||
public class LoginSettings: Decodable {
|
||||
public let accessToken: String
|
||||
private let scope: String
|
||||
private let scope: String?
|
||||
|
||||
public var scopes: [Scope] {
|
||||
guard let scope = scope else { return [] }
|
||||
return scope.components(separatedBy: .whitespaces).compactMap(Scope.init)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ public protocol AccountProtocol {
|
|||
var note: String { get }
|
||||
var url: URL { get }
|
||||
var avatar: URL { get }
|
||||
var header: URL { get }
|
||||
var header: URL? { get }
|
||||
var moved: Bool? { get }
|
||||
var bot: Bool? { get }
|
||||
|
||||
|
|
|
@ -13,6 +13,25 @@ public class RegisteredApplication: Decodable {
|
|||
public let clientID: String
|
||||
public let clientSecret: String
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Pixelfed API returns id/client_id as numbers instead of strings
|
||||
func decodeStringOrInt(key: CodingKeys) throws -> String {
|
||||
if let str = try? container.decode(String.self, forKey: key) {
|
||||
return str
|
||||
} else if let int = try? container.decode(Int.self, forKey: key) {
|
||||
return int.description
|
||||
} else {
|
||||
throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: container.codingPath + [CodingKeys.id], debugDescription: ""))
|
||||
}
|
||||
}
|
||||
|
||||
self.id = try decodeStringOrInt(key: .id)
|
||||
self.clientID = try decodeStringOrInt(key: .clientID)
|
||||
self.clientSecret = try container.decode(String.self, forKey: .clientSecret)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case clientID = "client_id"
|
||||
|
|
|
@ -244,6 +244,7 @@
|
|||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
|
||||
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; };
|
||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */; };
|
||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||
|
@ -579,6 +580,7 @@
|
|||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.swift; sourceTree = "<group>"; };
|
||||
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1061,6 +1063,7 @@
|
|||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */,
|
||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
|
||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
|
||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
|
||||
);
|
||||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1945,6 +1948,7 @@
|
|||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||
|
|
|
@ -27,7 +27,7 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
|
|||
@NSManaged private var fieldsData: Data?
|
||||
@NSManaged public var followersCount: Int
|
||||
@NSManaged public var followingCount: Int
|
||||
@NSManaged public var header: URL
|
||||
@NSManaged public var header: URL?
|
||||
@NSManaged public var id: String
|
||||
@NSManaged public var locked: Bool
|
||||
@NSManaged public var movedCD: Bool
|
||||
|
|
|
@ -21,6 +21,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@NSManaged public var applicationName: String?
|
||||
@NSManaged private var attachmentsData: Data?
|
||||
@NSManaged private var bookmarkedInternal: Bool
|
||||
@NSManaged public var cardData: Data?
|
||||
@NSManaged public var content: String
|
||||
@NSManaged public var createdAt: Date
|
||||
@NSManaged private var emojisData: Data?
|
||||
|
@ -56,6 +57,9 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
|
||||
public var mentions: [Mention]
|
||||
|
||||
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
||||
public var card: Card?
|
||||
|
||||
public var pinned: Bool? { pinnedInternal }
|
||||
public var bookmarked: Bool? { bookmarkedInternal }
|
||||
|
||||
|
@ -105,6 +109,7 @@ extension StatusMO {
|
|||
self.applicationName = status.application?.name
|
||||
self.attachments = status.attachments
|
||||
self.bookmarkedInternal = status.bookmarked ?? false
|
||||
self.card = status.card
|
||||
self.content = status.content
|
||||
self.createdAt = status.createdAt
|
||||
self.emojis = status.emojis
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17507" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17510.1" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="URI"/>
|
||||
|
@ -10,7 +10,7 @@
|
|||
<attribute name="fieldsData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" attributeType="URI"/>
|
||||
<attribute name="header" optional="YES" attributeType="URI"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
|
@ -44,6 +44,7 @@
|
|||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="attachmentsData" attributeType="Binary"/>
|
||||
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="cardData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="emojisData" attributeType="Binary" customClassName="[Data]"/>
|
||||
|
@ -74,7 +75,7 @@
|
|||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="433"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -38,6 +38,8 @@ class ProfileViewController: UIPageViewController {
|
|||
|
||||
private var headerView: ProfileHeaderView!
|
||||
|
||||
private var hasAppeared = false
|
||||
|
||||
init(accountID: String?, mastodonController: MastodonController) {
|
||||
self.accountID = accountID
|
||||
self.mastodonController = mastodonController
|
||||
|
@ -95,6 +97,12 @@ class ProfileViewController: UIPageViewController {
|
|||
loadAccount()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
hasAppeared = true
|
||||
}
|
||||
|
||||
private func loadAccount() {
|
||||
guard let accountID = accountID else { return }
|
||||
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||
|
@ -122,8 +130,14 @@ class ProfileViewController: UIPageViewController {
|
|||
// Optionally invoke updateUI on headerView because viewDidLoad may not have been called yet
|
||||
headerView?.updateUI(for: accountID)
|
||||
navigationItem.title = account.displayNameWithoutCustomEmoji
|
||||
pageControllers.forEach {
|
||||
$0.updateUI(account: account)
|
||||
|
||||
// Only call updateUI on the individual page controllers if the account is loaded after the profile VC has appeared on screen.
|
||||
// Otherwise, fi the page view controllers do something with the table view before they appear, the table view doesn't load
|
||||
// its cells until the user begins to scroll.
|
||||
if hasAppeared {
|
||||
pageControllers.forEach {
|
||||
$0.updateUI(account: account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -113,11 +113,13 @@ class ProfileHeaderView: UIView {
|
|||
self.avatarImageView.image = UIImage(data: data)
|
||||
}
|
||||
}
|
||||
headerRequest = ImageCache.headers.get(account.header) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
self.headerRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.headerImageView.image = UIImage(data: data)
|
||||
if let header = account.header {
|
||||
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
self.headerRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.headerImageView.image = UIImage(data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,16 +207,17 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
@objc func avatarPressed() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID!)")
|
||||
return
|
||||
}
|
||||
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
||||
}
|
||||
|
||||
@objc func headerPressed() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID!)")
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID),
|
||||
let header = account.header else {
|
||||
return
|
||||
}
|
||||
delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
||||
delegate?.showLoadingLargeImage(url: header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
||||
}
|
||||
|
||||
@IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) {
|
||||
|
|
|
@ -32,6 +32,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var contentWarningLabel: EmojiLabel!
|
||||
@IBOutlet weak var collapseButton: UIButton!
|
||||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||
@IBOutlet weak var cardView: StatusCardView!
|
||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||
@IBOutlet weak var replyButton: UIButton!
|
||||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
|
@ -143,6 +144,10 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
updateUI(account: account)
|
||||
updateUIForPreferences(account: account, status: status)
|
||||
|
||||
cardView.card = status.card
|
||||
cardView.isHidden = status.card == nil
|
||||
cardView.navigationDelegate = navigationDelegate
|
||||
|
||||
attachmentsView.updateUI(status: status)
|
||||
attachmentsView.isAccessibilityElement = status.attachments.count > 0
|
||||
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
|
||||
|
@ -265,6 +270,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
self.collapsed = collapsed
|
||||
|
||||
contentTextView.isHidden = collapsed
|
||||
cardView.isHidden = cardView.card == nil || collapsed
|
||||
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
||||
|
||||
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
||||
|
|
|
@ -96,6 +96,13 @@
|
|||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
|
@ -205,6 +212,7 @@
|
|||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="QqC-GR-TLC" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2WL-jD-I09"/>
|
||||
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
|
||||
<constraint firstItem="z0g-HN-gS0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="4TF-2Z-mdf"/>
|
||||
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
|
||||
|
@ -227,6 +235,7 @@
|
|||
<connections>
|
||||
<outlet property="attachmentsView" destination="IF9-9U-Gk0" id="Oxw-sJ-MJE"/>
|
||||
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="0R0-rt-Osh"/>
|
||||
<outlet property="cardView" destination="QqC-GR-TLC" id="CWR-fH-IfE"/>
|
||||
<outlet property="collapseButton" destination="8r8-O8-Agh" id="0es-Hi-bpt"/>
|
||||
<outlet property="contentTextView" destination="z0g-HN-gS0" id="atk-1f-83e"/>
|
||||
<outlet property="contentWarningLabel" destination="cwQ-mR-L1b" id="5sm-PC-FIN"/>
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
//
|
||||
// StatusCardView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/25/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SafariServices
|
||||
|
||||
class StatusCardView: UIView {
|
||||
|
||||
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||
|
||||
var card: Card? {
|
||||
didSet {
|
||||
if let card = card {
|
||||
self.updateUI(card: card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let activeBackgroundColor = UIColor.secondarySystemFill
|
||||
private let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
||||
|
||||
private var imageRequest: ImageCache.Request?
|
||||
|
||||
private var titleLabel: UILabel!
|
||||
private var descriptionLabel: UILabel!
|
||||
private var imageView: UIImageView!
|
||||
private var placeholderImageView: UIImageView!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 6.5
|
||||
self.layer.borderWidth = 1
|
||||
self.layer.borderColor = UIColor.lightGray.cgColor
|
||||
self.backgroundColor = inactiveBackgroundColor
|
||||
|
||||
self.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0)
|
||||
titleLabel.numberOfLines = 2
|
||||
|
||||
descriptionLabel = UILabel()
|
||||
descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0)
|
||||
descriptionLabel.numberOfLines = 2
|
||||
descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
|
||||
let vStack = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
descriptionLabel
|
||||
])
|
||||
vStack.axis = .vertical
|
||||
vStack.alignment = .leading
|
||||
vStack.distribution = .fill
|
||||
vStack.spacing = 0
|
||||
|
||||
imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
|
||||
let hStack = UIStackView(arrangedSubviews: [
|
||||
imageView,
|
||||
vStack
|
||||
])
|
||||
hStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
hStack.axis = .horizontal
|
||||
hStack.alignment = .center
|
||||
hStack.distribution = .fill
|
||||
hStack.spacing = 4
|
||||
|
||||
addSubview(hStack)
|
||||
|
||||
placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text"))
|
||||
placeholderImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderImageView.contentMode = .scaleAspectFit
|
||||
placeholderImageView.tintColor = .gray
|
||||
|
||||
addSubview(placeholderImageView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.heightAnchor.constraint(equalTo: heightAnchor),
|
||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
|
||||
|
||||
vStack.heightAnchor.constraint(equalTo: heightAnchor, constant: -8),
|
||||
|
||||
hStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
|
||||
hStack.topAnchor.constraint(equalTo: topAnchor),
|
||||
hStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
placeholderImageView.widthAnchor.constraint(equalToConstant: 30),
|
||||
placeholderImageView.heightAnchor.constraint(equalToConstant: 30),
|
||||
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
||||
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func updateUI(card: Card) {
|
||||
self.imageView.image = nil
|
||||
|
||||
if let image = card.image {
|
||||
placeholderImageView.isHidden = true
|
||||
|
||||
imageRequest = ImageCache.attachments.get(image, completion: { (data) in
|
||||
guard let data = data, let image = UIImage(data: data) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.imageView.image = image
|
||||
}
|
||||
})
|
||||
if imageRequest != nil {
|
||||
loadBlurHash()
|
||||
}
|
||||
} else {
|
||||
placeholderImageView.isHidden = false
|
||||
}
|
||||
|
||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
titleLabel.text = title
|
||||
titleLabel.isHidden = title.isEmpty
|
||||
|
||||
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.isHidden = description.isEmpty
|
||||
}
|
||||
|
||||
private func loadBlurHash() {
|
||||
guard let card = card, let hash = card.blurhash else { return }
|
||||
|
||||
let imageViewSize = self.imageView.bounds.size
|
||||
|
||||
// todo: merge this code with AttachmentView, use a single DispatchQueue
|
||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let size: CGSize
|
||||
if let width = card.width, let height = card.height {
|
||||
size = CGSize(width: width, height: height)
|
||||
} else {
|
||||
size = imageViewSize
|
||||
}
|
||||
|
||||
guard let preview = UIImage(blurHash: hash, size: size) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self,
|
||||
self.card?.url == card.url,
|
||||
self.imageView.image == nil else { return }
|
||||
|
||||
self.imageView.image = preview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
backgroundColor = activeBackgroundColor
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
backgroundColor = inactiveBackgroundColor
|
||||
setNeedsDisplay()
|
||||
|
||||
if let card = card, let delegate = navigationDelegate {
|
||||
delegate.selected(url: card.url)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
backgroundColor = inactiveBackgroundColor
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusCardView: MenuPreviewProvider {
|
||||
}
|
||||
|
||||
extension StatusCardView: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let card = card else { return nil }
|
||||
|
||||
return UIContextMenuConfiguration(identifier: nil) {
|
||||
return SFSafariViewController(url: card.url)
|
||||
} actionProvider: { (_) in
|
||||
let actions = self.actionsForURL(card.url, sourceView: self)
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||
}
|
||||
}
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
if let viewController = animator.previewViewController,
|
||||
let delegate = navigationDelegate {
|
||||
animator.preferredCommitStyle = .pop
|
||||
animator.addCompletion {
|
||||
if let customPresenting = viewController as? CustomPreviewPresenting {
|
||||
customPresenting.presentFromPreview(presenter: delegate)
|
||||
} else {
|
||||
delegate.show(viewController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,6 +114,13 @@
|
|||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
|
@ -238,6 +245,7 @@
|
|||
<connections>
|
||||
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
|
||||
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
||||
<outlet property="cardView" destination="LKo-VB-XWl" id="6X5-8P-Ata"/>
|
||||
<outlet property="collapseButton" destination="O0E-Vf-XYR" id="nWd-gg-st8"/>
|
||||
<outlet property="contentTextView" destination="waJ-f5-LKv" id="hrR-Zg-gLY"/>
|
||||
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
|
||||
|
|
|
@ -270,7 +270,7 @@ struct XCBActions {
|
|||
"following": account.followingCount.description,
|
||||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar.absoluteString,
|
||||
"headerURL": account.header.absoluteString
|
||||
"headerURL": account.header?.absoluteString
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -285,7 +285,7 @@ struct XCBActions {
|
|||
"following": account.followingCount.description,
|
||||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar.absoluteString,
|
||||
"headerURL": account.header.absoluteString
|
||||
"headerURL": account.header?.absoluteString
|
||||
])
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue