Compare commits

...

14 Commits

Author SHA1 Message Date
Shadowfacts da6ff67a51
Add notification dismissal context menu actions
Closes #49

This is a workaround for UIKit's built-in suggested context menu actions
not working as expected, and should be replaced with the system thing if
it becomes possible.
2019-11-17 18:49:48 -05:00
Shadowfacts a92d9ddc6f
Automatically collapse long (> 500 chars) statuses
Closes #12
2019-11-17 18:36:19 -05:00
Shadowfacts eb8afdaab8
Change how pin icon is displayed on statuses 2019-11-17 15:28:58 -05:00
Shadowfacts d4fa2f36e3
Remove send message from profile share menu 2019-11-17 15:19:20 -05:00
Shadowfacts 4cfe5e0fa5
Change action notification line limit to 3 2019-11-17 15:16:58 -05:00
Shadowfacts 975fb23292
Possibly fix crash when reblogger account is cached 2019-11-17 14:46:14 -05:00
Shadowfacts 85812d774d
Fix crash when trying to open context menu for attachment that hasn't yet loaded 2019-11-17 14:45:38 -05:00
Shadowfacts 150adeb581
Use custom navigation controller for preferences to override
viewWillDisappear method and send preferences changed notification

Workaround for #36
2019-11-17 14:31:07 -05:00
Shadowfacts 81a5fce602
Add preference to always blur media 2019-11-17 12:52:42 -05:00
Shadowfacts 6ce96764f3
Use visual effect views for sensitive media hide button so the button is
visible regardless of the image color
2019-11-17 12:28:21 -05:00
Shadowfacts 42a0a8890c
Tweak profile header more button appearance 2019-11-17 11:33:49 -05:00
Shadowfacts 56d4a6690f
Fix crash when displaying posts with more than 4 attachments 2019-11-17 11:32:35 -05:00
Shadowfacts c91a7baaa6
Show pinned posts on profiles
Closes #53
2019-11-17 11:14:33 -05:00
Shadowfacts af65aa88e0
Don't use suggested actions in context menus 2019-11-17 10:19:50 -05:00
19 changed files with 304 additions and 131 deletions

View File

@ -89,6 +89,7 @@
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
@ -348,6 +349,7 @@
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
@ -811,6 +813,7 @@
D641C789213DD87E004B4513 /* Preferences */ = {
isa = PBXGroup;
children = (
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
@ -1593,6 +1596,7 @@
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,

View File

@ -8,7 +8,6 @@
import Foundation
import Pachyderm
import SwiftUI
import Combine
class Preferences: Codable, ObservableObject {
@ -45,6 +44,7 @@ class Preferences: Codable, ObservableObject {
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
@ -66,6 +66,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(openLinksInApps, forKey: .openLinksInApps)
try container.encode(useInAppSafari, forKey: .useInAppSafari)
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
@ -86,6 +87,7 @@ class Preferences: Codable, ObservableObject {
@Published var defaultPostVisibility = Status.Visibility.public
@Published var automaticallySaveDrafts = true
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published var blurAllMedia = false
@Published var openLinksInApps = true
@Published var useInAppSafari = true
@Published var inAppSafariAutomaticReaderMode = false
@ -106,6 +108,7 @@ class Preferences: Codable, ObservableObject {
case defaultPostVisibility
case automaticallySaveDrafts
case contentWarningCopyMode
case blurAllMedia
case openLinksInApps
case useInAppSafari
case inAppSafariAutomaticReaderMode

View File

@ -32,8 +32,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
return true
}
override var childForHomeIndicatorAutoHidden: UIViewController? {
return
viewControllers?.first
return viewControllers?.first
}
init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) {

View File

@ -141,18 +141,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let dismissAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Dismiss", comment: "dismiss notification swipe action title")) { (action, view, completion) in
let group = DispatchGroup()
self.groups[indexPath.row].notificationIDs
.map(Pachyderm.Notification.dismiss(id:))
.forEach { (request) in
group.enter()
MastodonController.client.run(request) { (response) in
group.leave()
}
}
group.notify(queue: .main) {
self.groups.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.dismissNotificationsInGroup(at: indexPath) {
completion(true)
}
}
@ -169,6 +158,31 @@ class NotificationsTableViewController: EnhancedTableViewController {
return config
}
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
return [
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.dismissNotificationsInGroup(at: indexPath)
})
]
}
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
let group = DispatchGroup()
groups[indexPath.row].notificationIDs
.map(Pachyderm.Notification.dismiss(id:))
.forEach { (request) in
group.enter()
MastodonController.client.run(request) { (response) in
group.leave()
}
}
group.notify(queue: .main) {
self.groups.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
completion?()
}
}
@objc func refreshNotifications(_ sender: Any) {
guard let newer = newer else { return }

View File

@ -15,6 +15,7 @@ struct BehaviorPrefsView: View {
List {
section1
section2
section3
}.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Behavior"))
}
@ -44,6 +45,14 @@ struct BehaviorPrefsView: View {
var section2: some View {
Section(header: Text("READING")) {
Toggle(isOn: $preferences.blurAllMedia) {
Text("Blur All Media")
}
}
}
var section3: some View {
Section(header: Text("LINKS")) {
Toggle(isOn: $preferences.openLinksInApps) {
Text("Open Links in Apps")
}

View File

@ -0,0 +1,35 @@
//
// PreferencesHostingController.swift
// Tusker
//
// Created by Shadowfacts on 11/17/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import SwiftUI
class PreferencesNavigationController: UINavigationController {
init() {
let hostingController = UIHostingController(rootView: PreferencesView())
super.init(rootViewController: hostingController)
hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// workaround for onDisappear not being called when a modally presented UIHostingController is dismissed
NotificationCenter.default.post(name: .preferencesChanged, object: nil)
}
@objc func donePressed() {
dismiss(animated: true)
}
}

View File

@ -51,11 +51,7 @@ class MyProfileTableViewController: ProfileTableViewController {
}
@objc func preferencesPressed() {
let view = PreferencesView().environmentObject(Preferences.shared)
let hostingController = UIHostingController(rootView: view)
let navigationController = UINavigationController(rootViewController: hostingController)
hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(closePreferences))
present(navigationController, animated: true)
present(PreferencesNavigationController(), animated: true)
}
@objc func closePreferences() {

View File

@ -22,6 +22,13 @@ class ProfileTableViewController: EnhancedTableViewController {
}
}
var pinnedStatusIDs: [String] = [] {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
var timelineSegments: [TimelineSegment<Status>] = [] {
didSet {
DispatchQueue.main.async {
@ -98,6 +105,13 @@ class ProfileTableViewController: EnhancedTableViewController {
updateUIForPreferences()
getStatuses(onlyPinned: true) { (response) in
guard case let .success(statuses, _) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses)
self.pinnedStatusIDs = statuses.map { $0.id }
}
getStatuses() { response in
guard case let .success(statuses, pagination) = response else { fatalError() }
@ -114,8 +128,8 @@ class ProfileTableViewController: EnhancedTableViewController {
navigationItem.title = account.realDisplayName
}
func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) {
let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: !Preferences.shared.showRepliesInProfiles)
func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) {
let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles)
MastodonController.client.run(request, completion: completion)
}
@ -128,27 +142,38 @@ class ProfileTableViewController: EnhancedTableViewController {
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1 + timelineSegments.count
// 1 section for header, 1 section for pinned, rest for timeline
return 2 + timelineSegments.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1
} else if section == 1 {
return pinnedStatusIDs.count
} else {
return timelineSegments[section - 1].count
return timelineSegments[section - 2].count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
switch indexPath.section {
case 0:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "headerCell", for: indexPath) as? ProfileHeaderTableViewCell else { fatalError() }
cell.selectionStyle = .none
cell.delegate = self
cell.updateUI(for: accountID)
return cell
} else {
case 1:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() }
let statusID = timelineSegments[indexPath.section - 1][indexPath.row]
let statusID = pinnedStatusIDs[indexPath.row]
cell.showPinned = true
cell.updateUI(statusID: statusID)
cell.delegate = self
return cell
default:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() }
let statusID = timelineSegments[indexPath.section - 2][indexPath.row]
cell.updateUI(statusID: statusID)
cell.delegate = self
return cell
@ -156,14 +181,14 @@ class ProfileTableViewController: EnhancedTableViewController {
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if timelineSegments.count > 0 && indexPath.section == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 1].count - 1 {
if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 {
guard let older = older else { return }
getStatuses(for: older) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: newStatuses)
self.timelineSegments[indexPath.section - 1].append(objects: newStatuses)
self.timelineSegments[indexPath.section - 2].append(objects: newStatuses)
self.older = pagination?.older
}
@ -224,13 +249,11 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
if let relationship = relationship {
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
customActivities = [
SendMessageActivity(),
toggleFollowActivity,
OpenInSafariActivity()
]
} else {
customActivities = [
SendMessageActivity(),
OpenInSafariActivity()
]
}
@ -246,8 +269,8 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
extension ProfileTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 0 {
let statusID = timelineSegments[indexPath.section - 1][indexPath.row]
for indexPath in indexPaths where indexPath.section > 1 {
let statusID = timelineSegments[indexPath.section - 2][indexPath.row]
guard let status = MastodonCache.status(for: statusID) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments {
@ -257,8 +280,8 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 0 {
let statusID = timelineSegments[indexPath.section - 1][indexPath.row]
for indexPath in indexPaths where indexPath.section > 1 {
let statusID = timelineSegments[indexPath.section - 2][indexPath.row]
guard let status = MastodonCache.status(for: statusID) else { continue }
ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments {

View File

@ -46,8 +46,9 @@ extension EnhancedTableViewController {
guard let (previewProvider, actionsProvider) = cell.getPreviewProviders(for: cellLocation, sourceViewController: self) else {
return nil
}
let actionProvider: UIContextMenuActionProvider = { (elements) in
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements + actionsProvider())
let actionProvider: UIContextMenuActionProvider = { (_) in
let suggested = self.getSuggestedContextMenuActions(tableView: tableView, indexPath: indexPath, point: point)
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: suggested + actionsProvider())
}
return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: actionProvider)
} else {
@ -55,6 +56,11 @@ extension EnhancedTableViewController {
}
}
// todo: replace this with the UIKit suggested actions, if possible
@objc open func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
return []
}
override func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop

View File

@ -68,13 +68,6 @@ extension MenuPreviewProvider {
})
]
}
func actionsForNotificationGroup(_ group: NotificationGroup) -> [UIAction] {
// let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:))
return [
// todo: clear notifications option
]
}
private func createAction(identifier: String, title: String, systemImageName: String, handler: @escaping UIActionHandler) -> UIAction {
return UIAction(title: title, image: UIImage(systemName: systemImageName), identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler)

View File

@ -105,14 +105,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
}
@objc func imagePressed() {
// switch attachment.kind {
// case .image:
delegate?.showAttachmentsGallery(startingAt: index)
// case .video:
// delegate?.showVideo(attachment: attachment)
// default:
// fatalError()
// }
delegate?.showAttachmentsGallery(startingAt: index)
}
}

View File

@ -19,18 +19,14 @@ class AttachmentsContainerView: UIView {
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
var blurView: UIVisualEffectView?
var hideButton: UIButton?
var hideButtonView: UIVisualEffectView?
var contentHidden: Bool! {
didSet {
guard let blurView = blurView,
let hideButton = hideButton else { return }
let hideButtonView = hideButtonView else { return }
blurView.alpha = contentHidden ? 0 : 1
hideButton.alpha = contentHidden ? 1 : 0
UIView.animate(withDuration: 0.2) {
blurView.alpha = self.contentHidden ? 1 : 0
hideButton.alpha = self.contentHidden ? 0 : 1
}
blurView.alpha = self.contentHidden ? 1 : 0
hideButtonView.alpha = self.contentHidden ? 0 : 1
}
}
@ -38,6 +34,11 @@ class AttachmentsContainerView: UIView {
super.awakeFromNib()
self.isUserInteractionEnabled = true
createBlurView()
createHideButton()
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
func getAttachmentView(for attachment: Attachment) -> AttachmentView? {
@ -50,8 +51,8 @@ class AttachmentsContainerView: UIView {
self.statusID = status.id
attachments = status.attachments.filter { $0.kind == .image || $0.kind == .video }
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
attachmentViews.removeAllObjects()
subviews.forEach { $0.removeFromSuperview() }
if attachments.count > 0 {
self.isHidden = false
@ -62,14 +63,17 @@ class AttachmentsContainerView: UIView {
case 1:
let attachmentView = createAttachmentView(index: 0)
fillView(attachmentView)
sendSubviewToBack(attachmentView)
accessibilityElements.append(attachmentView)
case 2:
let left = createAttachmentView(index: 0)
let right = createAttachmentView(index: 1)
fillView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
let stack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
right
]))
])
fillView(stack)
sendSubviewToBack(stack)
NSLayoutConstraint.activate([
left.halfWidth()
])
@ -79,13 +83,15 @@ class AttachmentsContainerView: UIView {
let left = createAttachmentView(index: 0)
let topRight = createAttachmentView(index: 1)
let bottomRight = createAttachmentView(index: 2)
fillView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentsStack(axis: .vertical, arrangedSubviews: [
topRight,
bottomRight
])
]))
])
fillView(outerStack)
sendSubviewToBack(outerStack)
NSLayoutConstraint.activate([
left.halfWidth(),
topRight.halfHeight(),
@ -102,13 +108,15 @@ class AttachmentsContainerView: UIView {
])
let topRight = createAttachmentView(index: 1)
let bottomRight = createAttachmentView(index: 3)
fillView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentsStack(axis: .vertical, arrangedSubviews: [
topRight,
bottomRight
])
]))
])
fillView(outerStack)
sendSubviewToBack(outerStack)
NSLayoutConstraint.activate([
left.halfWidth(),
topLeft.halfHeight(),
@ -129,6 +137,7 @@ class AttachmentsContainerView: UIView {
moreLabel.textColor = .secondaryLabel
moreLabel.textAlignment = .center
moreLabel.translatesAutoresizingMaskIntoConstraints = false
moreView.addSubview(moreLabel)
moreView.accessibilityLabel = moreLabel.text
let topLeft = createAttachmentView(index: 0)
@ -138,13 +147,15 @@ class AttachmentsContainerView: UIView {
bottomLeft
])
let topRight = createAttachmentView(index: 1)
fillView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentsStack(axis: .vertical, arrangedSubviews: [
topRight,
moreView
])
]))
])
fillView(outerStack)
sendSubviewToBack(outerStack)
NSLayoutConstraint.activate([
left.halfWidth(),
topLeft.halfHeight(),
@ -165,11 +176,11 @@ class AttachmentsContainerView: UIView {
self.isHidden = true
}
if status.sensitive {
contentHidden = true
createBlurView()
createHideButton()
}
updateUIForPreferences()
}
@objc func updateUIForPreferences() {
contentHidden = Preferences.shared.blurAllMedia || (MastodonCache.status(for: statusID)?.sensitive ?? false)
}
private func createAttachmentView(index: Int) -> AttachmentView {
@ -194,7 +205,7 @@ class AttachmentsContainerView: UIView {
private func createBlurView() {
let blur = UIBlurEffect(style: .dark)
let blurView = UIVisualEffectView(effect: blur)
blurView.effect = blur
blurView.alpha = 0
blurView.translatesAutoresizingMaskIntoConstraints = false
fillView(blurView)
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blur, style: .label))
@ -210,7 +221,7 @@ class AttachmentsContainerView: UIView {
let stack = UIStackView(arrangedSubviews: [
imageView,
label
])
])
stack.axis = .vertical
stack.alignment = .center
stack.translatesAutoresizingMaskIntoConstraints = false
@ -230,21 +241,43 @@ class AttachmentsContainerView: UIView {
}
private func createHideButton() {
let hideButton = UIButton()
hideButton.translatesAutoresizingMaskIntoConstraints = false
hideButton.alpha = 0
hideButton.layer.cornerRadius = 2
hideButton.layer.masksToBounds = true
hideButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
hideButton.addTarget(self, action: #selector(hideButtonTapped), for: .touchUpInside)
let blurEffect = UIBlurEffect(style: .regular)
let hideButtonBlurView = UIVisualEffectView(effect: blurEffect)
hideButtonBlurView.translatesAutoresizingMaskIntoConstraints = false
hideButtonBlurView.alpha = 1
hideButtonBlurView.isUserInteractionEnabled = true
hideButtonBlurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideButtonTapped)))
addSubview(hideButtonBlurView)
self.hideButtonView = hideButtonBlurView
addSubview(hideButton)
let maskLayer = CALayer()
let image = UIImage(systemName: "eye.slash.fill")!
maskLayer.contents = image.cgImage!
maskLayer.frame = CGRect(origin: .zero, size: image.size)
hideButtonBlurView.layer.mask = maskLayer
let hideButtonVibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
hideButtonVibrancyView.translatesAutoresizingMaskIntoConstraints = false
hideButtonBlurView.contentView.addSubview(hideButtonVibrancyView)
let fillView = UIView()
fillView.translatesAutoresizingMaskIntoConstraints = false
fillView.backgroundColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 0.5)
hideButtonVibrancyView.contentView.addSubview(fillView)
NSLayoutConstraint.activate([
hideButton.topAnchor.constraint(equalTo: topAnchor, constant: 8),
hideButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8)
hideButtonBlurView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
hideButtonBlurView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
hideButtonBlurView.widthAnchor.constraint(equalToConstant: image.size.width),
hideButtonBlurView.heightAnchor.constraint(equalToConstant: image.size.height),
hideButtonVibrancyView.leadingAnchor.constraint(equalTo: hideButtonBlurView.contentView.leadingAnchor),
hideButtonVibrancyView.trailingAnchor.constraint(equalTo: hideButtonBlurView.contentView.trailingAnchor),
hideButtonVibrancyView.topAnchor.constraint(equalTo: hideButtonBlurView.contentView.topAnchor),
hideButtonVibrancyView.bottomAnchor.constraint(equalTo: hideButtonBlurView.contentView.bottomAnchor),
fillView.leadingAnchor.constraint(equalTo: hideButtonBlurView.contentView.leadingAnchor),
fillView.trailingAnchor.constraint(equalTo: hideButtonBlurView.contentView.trailingAnchor),
fillView.topAnchor.constraint(equalTo: hideButtonBlurView.contentView.topAnchor),
fillView.bottomAnchor.constraint(equalTo: hideButtonBlurView.contentView.bottomAnchor),
])
self.hideButton = hideButton
}
private func fillView(_ view: UIView, in parentView: UIView? = nil) {
@ -261,11 +294,15 @@ class AttachmentsContainerView: UIView {
// MARK: - Interaction
@objc func blurViewTapped() {
contentHidden = false
UIView.animate(withDuration: 0.2) {
self.contentHidden = false
}
}
@objc func hideButtonTapped() {
contentHidden = true
UIView.animate(withDuration: 0.2) {
self.contentHidden = true
}
}
@objc func showSensitiveContent() {

View File

@ -189,7 +189,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
}
return self.delegate?.statusActionAccountList(action: action, statusID: self.statusID, accountIDs: accountIDs)
}, actions: {
return self.actionsForNotificationGroup(self.group)
return []
})
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14819.2"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -45,7 +45,7 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="5" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lc7-zZ-HrZ">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lc7-zZ-HrZ">
<rect key="frame" x="0.0" y="79" width="230" height="74"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>

View File

@ -142,7 +142,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
return AccountListTableViewController(accountIDs: accountIDs)
}
}, actions: {
return self.actionsForNotificationGroup(self.group)
return []
})
}

View File

@ -27,6 +27,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
@IBOutlet weak var fieldsStackView: UIStackView!
@IBOutlet weak var fieldNamesStackView: UIStackView!
@IBOutlet weak var fieldValuesStack: UIStackView!
@IBOutlet weak var moreButtonVisualEffectView: UIVisualEffectView!
var accountID: String!
@ -40,6 +41,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
avatarImageView.isUserInteractionEnabled = true
headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed)))
headerImageView.isUserInteractionEnabled = true
moreButtonVisualEffectView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(morePressed)))
let maskLayer = CAShapeLayer()
maskLayer.frame = moreButtonVisualEffectView.bounds
maskLayer.path = CGPath(ellipseIn: moreButtonVisualEffectView.bounds, transform: nil)
moreButtonVisualEffectView.layer.mask = maskLayer
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
@ -129,7 +136,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
}
}
@IBAction func morePressed(_ sender: Any) {
@objc func morePressed() {
delegate?.showMoreOptions()
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -85,18 +85,49 @@
</stackView>
</subviews>
</stackView>
<button opaque="NO" alpha="0.59999999999999998" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qiv-gB-kiX">
<rect key="frame" x="323.5" y="120" width="35.5" height="22"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<state key="normal" image="ellipsis" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="32" scale="default"/>
</state>
<connections>
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="0go-4p-qDa"/>
</connections>
</button>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mQY-XN-PfZ">
<rect key="frame" x="335" y="110" width="32" height="32"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="0Ol-1d-la6">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t0d-eE-mbc">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="TgJ-FF-QyB">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ellipsis" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="cLs-dC-SWU">
<rect key="frame" x="2" y="12.5" width="28" height="7"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="24"/>
</imageView>
</subviews>
<gestureRecognizers/>
<constraints>
<constraint firstItem="cLs-dC-SWU" firstAttribute="leading" secondItem="TgJ-FF-QyB" secondAttribute="leading" constant="2" id="7nV-7d-GAY"/>
<constraint firstAttribute="bottom" secondItem="cLs-dC-SWU" secondAttribute="bottom" constant="2" id="8sP-mZ-ZSQ"/>
<constraint firstAttribute="trailing" secondItem="cLs-dC-SWU" secondAttribute="trailing" constant="2" id="iBQ-oA-yOm"/>
<constraint firstItem="cLs-dC-SWU" firstAttribute="top" secondItem="TgJ-FF-QyB" secondAttribute="top" constant="2" id="jSB-2f-sZF"/>
</constraints>
</view>
<vibrancyEffect style="label">
<blurEffect style="prominent"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
<constraints>
<constraint firstItem="t0d-eE-mbc" firstAttribute="leading" secondItem="0Ol-1d-la6" secondAttribute="leading" id="6Py-U4-Jlo"/>
<constraint firstAttribute="bottom" secondItem="t0d-eE-mbc" secondAttribute="bottom" id="OT5-Yh-eiG"/>
<constraint firstAttribute="trailing" secondItem="t0d-eE-mbc" secondAttribute="trailing" id="a8T-dS-dc8"/>
<constraint firstItem="t0d-eE-mbc" firstAttribute="top" secondItem="0Ol-1d-la6" secondAttribute="top" id="xKq-qM-vmk"/>
</constraints>
</view>
<constraints>
<constraint firstAttribute="height" constant="32" id="Zye-sh-FIH"/>
<constraint firstAttribute="width" constant="32" id="hpF-s0-dbt"/>
</constraints>
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
@ -110,6 +141,7 @@
<constraint firstItem="Fw7-OL-iy5" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="LqH-lE-AIe"/>
<constraint firstItem="KyB-ey-l11" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="NN7-5B-k1Q"/>
<constraint firstItem="MIj-OR-NOR" firstAttribute="bottom" secondItem="tH8-sR-DHC" secondAttribute="bottom" id="PhQ-El-olR"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="mQY-XN-PfZ" secondAttribute="trailing" constant="8" id="TZn-8m-0Wq"/>
<constraint firstItem="sHU-GU-klv" firstAttribute="top" secondItem="DfO-uD-UNI" secondAttribute="bottom" constant="8" id="Vza-1s-qbG"/>
<constraint firstAttribute="trailingMargin" secondItem="sHU-GU-klv" secondAttribute="trailing" id="XJa-zP-Ma2"/>
<constraint firstItem="sHU-GU-klv" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leadingMargin" id="cSX-WD-2aJ"/>
@ -118,9 +150,8 @@
<constraint firstItem="MIj-OR-NOR" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="iG7-yZ-9u3"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="sHU-GU-klv" secondAttribute="bottom" constant="8" id="iRf-l0-ZZX"/>
<constraint firstItem="MIj-OR-NOR" firstAttribute="top" relation="greaterThanOrEqual" secondItem="LjK-72-Bez" secondAttribute="bottom" id="nMM-6t-bjX"/>
<constraint firstAttribute="trailing" secondItem="qiv-gB-kiX" secondAttribute="trailing" constant="16" id="nYG-p6-Ezm"/>
<constraint firstItem="qiv-gB-kiX" firstAttribute="bottom" secondItem="Fw7-OL-iy5" secondAttribute="bottom" constant="-8" id="pg7-L7-u2f"/>
<constraint firstItem="DfO-uD-UNI" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="pqd-E3-Aw4"/>
<constraint firstItem="LjK-72-Bez" firstAttribute="top" secondItem="mQY-XN-PfZ" secondAttribute="bottom" constant="16" id="rTO-fy-u0V"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
@ -133,6 +164,7 @@
<outlet property="fieldsStackView" destination="sHU-GU-klv" id="Gli-Gf-Ubh"/>
<outlet property="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
<outlet property="moreButtonVisualEffectView" destination="mQY-XN-PfZ" id="t7l-wg-nj0"/>
<outlet property="noteLabel" destination="I0n-aP-dJP" id="7yW-mE-jxY"/>
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
</connections>

View File

@ -42,6 +42,7 @@ class StatusTableViewCell: UITableViewCell {
@IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var reblogButton: UIButton!
@IBOutlet weak var moreButton: UIButton!
@IBOutlet weak var pinImageView: UIImageView!
var statusID: String!
var accountID: String!
@ -58,6 +59,7 @@ class StatusTableViewCell: UITableViewCell {
reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
}
}
var showPinned: Bool = false
var collapsible = false {
didSet {
@ -110,10 +112,7 @@ class StatusTableViewCell: UITableViewCell {
rebloggerAccountUpdater = MastodonCache.accountSubject
.filter { $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { (_) in
// this method is responsible for setting the reblog label text
self.updateUIForPreferences()
})
.sink(receiveValue: updateRebloggerLabel(reblogger:))
}
func updateUI(statusID: String) {
@ -153,6 +152,17 @@ class StatusTableViewCell: UITableViewCell {
setCollapsed(collapsible, animated: false)
contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty
if !collapsed,
let text = contentLabel.text,
text.count > 500 {
collapsible = true
setCollapsed(true, animated: false)
}
let pinned = status.pinned ?? false
pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden
}
private func updateStatusState(status: Status) {
@ -189,11 +199,15 @@ class StatusTableViewCell: UITableViewCell {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
if let rebloggerID = rebloggerID,
let reblogger = MastodonCache.account(for: rebloggerID) {
reblogLabel.text = "Reblogged by \(reblogger.realDisplayName)"
updateRebloggerLabel(reblogger: reblogger)
}
displayNameLabel.text = account.realDisplayName
}
func updateRebloggerLabel(reblogger: Account) {
reblogLabel.text = "Reblogged by \(reblogger.realDisplayName)"
}
func updateTimestamp() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
@ -225,7 +239,8 @@ class StatusTableViewCell: UITableViewCell {
}
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
showPinned = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
@ -439,8 +454,8 @@ extension StatusTableViewCell: MenuPreviewProvider {
return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) })
} else if attachmentsView.frame.contains(location) {
let attachmentsViewLocation = attachmentsView.convert(location, from: self)
if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView {
let image = attachmentView.image!
if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView,
let image = attachmentView.image {
let description = attachmentView.attachment.description
return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] })
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -23,7 +23,7 @@
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="ve3-Y1-NQH">
<rect key="frame" x="0.0" y="28.5" width="343" height="165.5"/>
<rect key="frame" x="0.0" y="28.5" width="343" height="103.5"/>
<subviews>
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -37,7 +37,7 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
<rect key="frame" x="58" y="0.0" width="277" height="165.5"/>
<rect key="frame" x="58" y="0.0" width="277" height="103.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
@ -62,6 +62,11 @@
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView hidden="YES" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pin.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
<rect key="frame" x="248.5" y="-0.5" width="0.0" height="22"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" label="Pinned Status"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="752" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="35d-EA-ReR">
<rect key="frame" x="252.5" y="0.0" width="24.5" height="20.5"/>
<accessibility key="accessibilityConfiguration">
@ -101,7 +106,7 @@
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HrJ-t9-KcD" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
<rect key="frame" x="0.0" y="83" width="277" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -120,17 +125,17 @@
</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="198" width="343" height="0.0"/>
<rect key="frame" x="0.0" y="136" width="343" height="0.0"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
<rect key="frame" x="0.0" y="202" width="343" height="22"/>
<rect key="frame" x="0.0" y="140" width="343" height="84"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
<rect key="frame" x="0.0" y="0.0" width="21" height="22"/>
<rect key="frame" x="0.0" y="62" width="21" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
<connections>
@ -138,7 +143,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
<rect key="frame" x="107" y="0.0" width="22" height="22"/>
<rect key="frame" x="107" y="62" width="22" height="22"/>
<accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/>
<connections>
@ -146,7 +151,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
<rect key="frame" x="215.5" y="0.0" width="22.5" height="22"/>
<rect key="frame" x="215.5" y="62" width="22.5" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/>
<connections>
@ -154,7 +159,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
<rect key="frame" x="324" y="0.0" width="19" height="22"/>
<rect key="frame" x="324" y="62" width="19" height="22"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/>
<connections>
@ -189,6 +194,7 @@
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="63y-He-xy1"/>
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="Ohz-bs-Ebr"/>
<outlet property="moreButton" destination="982-J4-NGl" id="Xga-I4-CzK"/>
<outlet property="pinImageView" destination="LRh-Cc-1br" id="9jn-0V-PdJ"/>
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="i9h-QA-ZPd"/>
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
<outlet property="replyButton" destination="rKF-yF-KIa" id="rul-lk-bIR"/>
@ -202,6 +208,7 @@
<image name="arrowshape.turn.up.left.fill" catalog="system" width="64" height="52"/>
<image name="chevron.down" catalog="system" width="64" height="36"/>
<image name="ellipsis" catalog="system" width="64" height="18"/>
<image name="pin.fill" catalog="system" width="58" height="64"/>
<image name="repeat" catalog="system" width="64" height="48"/>
<image name="star.fill" catalog="system" width="64" height="58"/>
</resources>