Compare commits

...

3 Commits

16 changed files with 340 additions and 34 deletions

View File

@ -22,8 +22,8 @@ public final class Account: AccountProtocol, Decodable {
public let url: URL public let url: URL
public let avatar: URL public let avatar: URL
public let avatarStatic: URL public let avatarStatic: URL
public let header: URL public let header: URL?
public let headerStatic: URL public let headerStatic: URL?
public private(set) var emojis: [Emoji] public private(set) var emojis: [Emoji]
public let moved: Bool? public let moved: Bool?
public let movedTo: Account? public let movedTo: Account?
@ -46,8 +46,8 @@ public final class Account: AccountProtocol, Decodable {
self.url = try container.decode(URL.self, forKey: .url) self.url = try container.decode(URL.self, forKey: .url)
self.avatar = try container.decode(URL.self, forKey: .avatar) self.avatar = try container.decode(URL.self, forKey: .avatar)
self.avatarStatic = try container.decode(URL.self, forKey: .avatarStatic) self.avatarStatic = try container.decode(URL.self, forKey: .avatarStatic)
self.header = try container.decode(URL.self, forKey: .header) self.header = try? container.decode(URL.self, forKey: .header)
self.headerStatic = try container.decode(URL.self, forKey: .headerStatic) self.headerStatic = try? container.decode(URL.self, forKey: .headerStatic)
self.emojis = try container.decode([Emoji].self, forKey: .emojis) self.emojis = try container.decode([Emoji].self, forKey: .emojis)
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? [] self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
self.bot = try? container.decode(Bool.self, forKey: .bot) self.bot = try? container.decode(Bool.self, forKey: .bot)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Card: Decodable { public class Card: Codable {
public let url: URL public let url: URL
public let title: String public let title: String
public let description: String public let description: String
@ -21,6 +21,7 @@ public class Card: Decodable {
public let html: String? public let html: String?
public let width: Int? public let width: Int?
public let height: Int? public let height: Int?
public let blurhash: String?
public required init(from decoder: Decoder) throws { public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) 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.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description) self.description = try container.decode(String.self, forKey: .description)
self.kind = try container.decode(Kind.self, forKey: .kind) self.kind = try container.decode(Kind.self, forKey: .kind)
self.image = try? container.decode(URL.self, forKey: .image) self.image = try? container.decodeIfPresent(URL.self, forKey: .image)
self.authorName = try? container.decode(String.self, forKey: .authorName) self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
self.authorURL = try? container.decode(URL.self, forKey: .authorURL) self.authorURL = try? container.decodeIfPresent(URL.self, forKey: .authorURL)
self.providerName = try? container.decode(String.self, forKey: .providerName) self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
self.providerURL = try? container.decode(URL.self, forKey: .providerURL) self.providerURL = try? container.decodeIfPresent(URL.self, forKey: .providerURL)
self.html = try? container.decode(String.self, forKey: .html) self.html = try? container.decodeIfPresent(String.self, forKey: .html)
self.width = try? container.decode(Int.self, forKey: .width) self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
self.height = try? container.decode(Int.self, forKey: .height) 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 { private enum CodingKeys: String, CodingKey {
@ -52,14 +65,14 @@ public class Card: Decodable {
case html case html
case width case width
case height case height
case blurhash
} }
} }
extension Card { extension Card {
public enum Kind: String, Decodable { public enum Kind: String, Codable {
case link case link
case photo case photo
case video case video
case rich
} }
} }

View File

@ -10,9 +10,10 @@ import Foundation
public class LoginSettings: Decodable { public class LoginSettings: Decodable {
public let accessToken: String public let accessToken: String
private let scope: String private let scope: String?
public var scopes: [Scope] { public var scopes: [Scope] {
guard let scope = scope else { return [] }
return scope.components(separatedBy: .whitespaces).compactMap(Scope.init) return scope.components(separatedBy: .whitespaces).compactMap(Scope.init)
} }

View File

@ -23,7 +23,7 @@ public protocol AccountProtocol {
var note: String { get } var note: String { get }
var url: URL { get } var url: URL { get }
var avatar: URL { get } var avatar: URL { get }
var header: URL { get } var header: URL? { get }
var moved: Bool? { get } var moved: Bool? { get }
var bot: Bool? { get } var bot: Bool? { get }

View File

@ -13,6 +13,25 @@ public class RegisteredApplication: Decodable {
public let clientID: String public let clientID: String
public let clientSecret: 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 { private enum CodingKeys: String, CodingKey {
case id case id
case clientID = "client_id" case clientID = "client_id"

View File

@ -244,6 +244,7 @@
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; }; D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; }; D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; };
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.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 */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
@ -1061,6 +1063,7 @@
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */, D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */,
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */, D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */, D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
); );
path = Status; path = Status;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1945,6 +1948,7 @@
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */, D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */, D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */, D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */, D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,

View File

@ -27,7 +27,7 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
@NSManaged private var fieldsData: Data? @NSManaged private var fieldsData: Data?
@NSManaged public var followersCount: Int @NSManaged public var followersCount: Int
@NSManaged public var followingCount: Int @NSManaged public var followingCount: Int
@NSManaged public var header: URL @NSManaged public var header: URL?
@NSManaged public var id: String @NSManaged public var id: String
@NSManaged public var locked: Bool @NSManaged public var locked: Bool
@NSManaged public var movedCD: Bool @NSManaged public var movedCD: Bool

View File

@ -21,6 +21,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
@NSManaged public var applicationName: String? @NSManaged public var applicationName: String?
@NSManaged private var attachmentsData: Data? @NSManaged private var attachmentsData: Data?
@NSManaged private var bookmarkedInternal: Bool @NSManaged private var bookmarkedInternal: Bool
@NSManaged public var cardData: Data?
@NSManaged public var content: String @NSManaged public var content: String
@NSManaged public var createdAt: Date @NSManaged public var createdAt: Date
@NSManaged private var emojisData: Data? @NSManaged private var emojisData: Data?
@ -56,6 +57,9 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData) @LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
public var mentions: [Mention] public var mentions: [Mention]
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
public var card: Card?
public var pinned: Bool? { pinnedInternal } public var pinned: Bool? { pinnedInternal }
public var bookmarked: Bool? { bookmarkedInternal } public var bookmarked: Bool? { bookmarkedInternal }
@ -105,6 +109,7 @@ 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.emojis = status.emojis self.emojis = status.emojis

View File

@ -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="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"> <entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/> <attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="URI"/> <attribute name="avatar" attributeType="URI"/>
@ -10,7 +10,7 @@
<attribute name="fieldsData" optional="YES" attributeType="Binary"/> <attribute name="fieldsData" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" 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="id" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="movedCD" optional="YES" 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="applicationName" optional="YES" attributeType="String"/>
<attribute name="attachmentsData" attributeType="Binary"/> <attribute name="attachmentsData" attributeType="Binary"/>
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="cardData" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/> <attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="emojisData" attributeType="Binary" customClassName="[Data]"/> <attribute name="emojisData" attributeType="Binary" customClassName="[Data]"/>
@ -74,7 +75,7 @@
</entity> </entity>
<elements> <elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/> <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="Relationship" positionX="63" positionY="135" width="128" height="208"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="433"/>
</elements> </elements>
</model> </model>

View File

@ -38,6 +38,8 @@ class ProfileViewController: UIPageViewController {
private var headerView: ProfileHeaderView! private var headerView: ProfileHeaderView!
private var hasAppeared = false
init(accountID: String?, mastodonController: MastodonController) { init(accountID: String?, mastodonController: MastodonController) {
self.accountID = accountID self.accountID = accountID
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -95,6 +97,12 @@ class ProfileViewController: UIPageViewController {
loadAccount() loadAccount()
} }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
hasAppeared = true
}
private func loadAccount() { private func loadAccount() {
guard let accountID = accountID else { return } guard let accountID = accountID else { return }
if mastodonController.persistentContainer.account(for: accountID) != nil { 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 // Optionally invoke updateUI on headerView because viewDidLoad may not have been called yet
headerView?.updateUI(for: accountID) headerView?.updateUI(for: accountID)
navigationItem.title = account.displayNameWithoutCustomEmoji 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)
}
} }
} }

View File

@ -113,11 +113,13 @@ class ProfileHeaderView: UIView {
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = UIImage(data: data)
} }
} }
headerRequest = ImageCache.headers.get(account.header) { [weak self] (data) in if let header = account.header {
guard let self = self, let data = data, self.accountID == accountID else { return } headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
self.headerRequest = nil guard let self = self, let data = data, self.accountID == accountID else { return }
DispatchQueue.main.async { self.headerRequest = nil
self.headerImageView.image = UIImage(data: data) DispatchQueue.main.async {
self.headerImageView.image = UIImage(data: data)
}
} }
} }
@ -205,16 +207,17 @@ class ProfileHeaderView: UIView {
@objc func avatarPressed() { @objc func avatarPressed() {
guard let account = mastodonController.persistentContainer.account(for: accountID) else { 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) delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
} }
@objc func headerPressed() { @objc func headerPressed() {
guard let account = mastodonController.persistentContainer.account(for: accountID) else { guard let account = mastodonController.persistentContainer.account(for: accountID),
fatalError("Missing cached account \(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) { @IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) {

View File

@ -32,6 +32,7 @@ class BaseStatusTableViewCell: UITableViewCell {
@IBOutlet weak var contentWarningLabel: EmojiLabel! @IBOutlet weak var contentWarningLabel: EmojiLabel!
@IBOutlet weak var collapseButton: UIButton! @IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentTextView: StatusContentTextView! @IBOutlet weak var contentTextView: StatusContentTextView!
@IBOutlet weak var cardView: StatusCardView!
@IBOutlet weak var attachmentsView: AttachmentsContainerView! @IBOutlet weak var attachmentsView: AttachmentsContainerView!
@IBOutlet weak var replyButton: UIButton! @IBOutlet weak var replyButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var favoriteButton: UIButton!
@ -143,6 +144,10 @@ class BaseStatusTableViewCell: UITableViewCell {
updateUI(account: account) updateUI(account: account)
updateUIForPreferences(account: account, status: status) updateUIForPreferences(account: account, status: status)
cardView.card = status.card
cardView.isHidden = status.card == nil
cardView.navigationDelegate = navigationDelegate
attachmentsView.updateUI(status: status) attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0 attachmentsView.isAccessibilityElement = status.attachments.count > 0
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count) 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 self.collapsed = collapsed
contentTextView.isHidden = collapsed contentTextView.isHidden = collapsed
cardView.isHidden = cardView.card == nil || collapsed
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")! let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!

View File

@ -96,6 +96,13 @@
<fontDescription key="fontDescription" type="system" pointSize="20"/> <fontDescription key="fontDescription" type="system" pointSize="20"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </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"> <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"/> <rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/> <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
@ -205,6 +212,7 @@
</stackView> </stackView>
</subviews> </subviews>
<constraints> <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="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="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"/> <constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
@ -227,6 +235,7 @@
<connections> <connections>
<outlet property="attachmentsView" destination="IF9-9U-Gk0" id="Oxw-sJ-MJE"/> <outlet property="attachmentsView" destination="IF9-9U-Gk0" id="Oxw-sJ-MJE"/>
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="0R0-rt-Osh"/> <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="collapseButton" destination="8r8-O8-Agh" id="0es-Hi-bpt"/>
<outlet property="contentTextView" destination="z0g-HN-gS0" id="atk-1f-83e"/> <outlet property="contentTextView" destination="z0g-HN-gS0" id="atk-1f-83e"/>
<outlet property="contentWarningLabel" destination="cwQ-mR-L1b" id="5sm-PC-FIN"/> <outlet property="contentWarningLabel" destination="cwQ-mR-L1b" id="5sm-PC-FIN"/>

View File

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

View File

@ -114,6 +114,13 @@
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </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"> <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"/> <rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/> <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
@ -238,6 +245,7 @@
<connections> <connections>
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/> <outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/> <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="collapseButton" destination="O0E-Vf-XYR" id="nWd-gg-st8"/>
<outlet property="contentTextView" destination="waJ-f5-LKv" id="hrR-Zg-gLY"/> <outlet property="contentTextView" destination="waJ-f5-LKv" id="hrR-Zg-gLY"/>
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/> <outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>

View File

@ -270,7 +270,7 @@ struct XCBActions {
"following": account.followingCount.description, "following": account.followingCount.description,
"url": account.url.absoluteString, "url": account.url.absoluteString,
"avatarURL": account.avatar.absoluteString, "avatarURL": account.avatar.absoluteString,
"headerURL": account.header.absoluteString "headerURL": account.header?.absoluteString
]) ])
} }
} }
@ -285,7 +285,7 @@ struct XCBActions {
"following": account.followingCount.description, "following": account.followingCount.description,
"url": account.url.absoluteString, "url": account.url.absoluteString,
"avatarURL": account.avatar.absoluteString, "avatarURL": account.avatar.absoluteString,
"headerURL": account.header.absoluteString "headerURL": account.header?.absoluteString
]) ])
} }