Compare commits

..

No commits in common. "576e4aa90d292426effd295cf3bec1e1f10ca439" and "ee90b20f7fa405b0e5b8d946053cb6e7d2da79b3" have entirely different histories.

29 changed files with 283 additions and 164 deletions

View File

@ -173,6 +173,8 @@
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
@ -500,6 +502,8 @@
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
@ -1141,6 +1145,15 @@
path = "Account Detail";
sourceTree = "<group>";
};
D67C57B021E28F9400C3118B /* Compose Status Reply */ = {
isa = PBXGroup;
children = (
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */,
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */,
);
path = "Compose Status Reply";
sourceTree = "<group>";
};
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -1260,6 +1273,7 @@
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */,
D626494023C122C800612E6E /* Asset Picker */,
D61959D0241E842400A37B8E /* Draft Cell */,
D641C78A213DD926004B4513 /* Status */,
@ -1631,6 +1645,7 @@
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
@ -1803,6 +1818,7 @@
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,

View File

@ -89,13 +89,6 @@
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "DISABLE_IMAGE_CACHE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -15,7 +15,6 @@ enum Cache<T> {
case disk(DiskStorage<T>)
case hybrid(HybridStorage<T>)
@available(*, deprecated, message: "disk-based caches synchronously interact with the file system. Avoid using if possible.")
func existsObject(forKey key: String) throws -> Bool {
switch self {
case let .memory(memory):

View File

@ -16,12 +16,6 @@ class ImageCache {
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
#if DEBUG
private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
#else
private static let disableCaching = false
#endif
private let cache: Cache<Data>
private var groups = [URL: RequestGroup]()
@ -44,8 +38,8 @@ class ImageCache {
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
let key = url.absoluteString
if !ImageCache.disableCaching,
let data = try? cache.object(forKey: key) {
if (try? cache.existsObject(forKey: key)) ?? false,
let data = try? cache.object(forKey: key) {
completion?(data)
return nil
} else {

View File

@ -9,7 +9,7 @@
import Foundation
import Pachyderm
class MastodonController: ObservableObject {
class MastodonController {
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
@ -42,8 +42,8 @@ class MastodonController: ObservableObject {
let client: Client!
@Published private(set) var account: Account!
@Published private(set) var instance: Instance!
var account: Account!
var instance: Instance!
var loggedIn: Bool {
accountInfo != nil
@ -95,9 +95,7 @@ class MastodonController: ObservableObject {
completion?(.failure(error))
case let .success(account, _):
DispatchQueue.main.async {
self.account = account
}
self.account = account
self.persistentContainer.backgroundContext.perform {
if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
@ -120,12 +118,13 @@ class MastodonController: ObservableObject {
let request = Client.getInstance()
run(request) { (response) in
guard case let .success(instance, _) = response else { fatalError() }
DispatchQueue.main.async {
self.instance = instance
completion?(instance)
}
self.instance = instance
completion?(instance)
}
}
}
}
// ObservableObject so that SwiftUI views can receive it through @EnvironmentObject
extension MastodonController: ObservableObject {}

View File

@ -158,11 +158,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
mastodonController.getOwnInstance()
let rootController: UIViewController
#if SDK_IOS_14
if #available(iOS 14.0, *) {
rootController = MainSplitViewController(mastodonController: mastodonController)
} else {
rootController = MainTabBarViewController(mastodonController: mastodonController)
}
#else
rootController = MainTabBarViewController(mastodonController: mastodonController)
#endif
window!.rootViewController = rootController
}

View File

@ -91,9 +91,7 @@ struct ComposeAttachmentsList: View {
}
private var canAddAttachment: Bool {
switch mastodonController.instance?.instanceType {
case nil:
return false
switch mastodonController.instance.instanceType {
case .pleroma:
return true
case .mastodon:

View File

@ -9,7 +9,7 @@
import SwiftUI
struct ComposeAvatarImageView: View {
let url: URL?
let url: URL
@State var request: ImageCache.Request? = nil
@State var avatarImage: UIImage? = nil
@ObservedObject var preferences = Preferences.shared
@ -19,9 +19,7 @@ struct ComposeAvatarImageView: View {
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
.conditionally(url != nil) {
$0.onAppear(perform: self.loadImage)
}
.onAppear(perform: self.loadImage)
.onDisappear(perform: self.cancelRequest)
}
@ -29,33 +27,24 @@ struct ComposeAvatarImageView: View {
if let avatarImage = avatarImage {
return Image(uiImage: avatarImage)
} else {
return placeholderImage
let imageName: String
switch preferences.avatarStyle {
case .circle:
imageName = "person.crop.circle"
case .roundRect:
imageName = "person.crop.square"
}
return Image(systemName: imageName)
}
}
private var placeholderImage: Image {
let imageName: String
switch preferences.avatarStyle {
case .circle:
imageName = "person.crop.circle"
case .roundRect:
imageName = "person.crop.square"
}
return Image(systemName: imageName)
}
private func loadImage() {
guard let url = url else { return }
request = ImageCache.avatars.get(url) { (data) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.request = nil
DispatchQueue.main.async {
self.request = nil
if let data = data, let image = UIImage(data: data) {
self.avatarImage = image
}
} else {
DispatchQueue.main.async {
self.request = nil
}
}
}
}

View File

@ -12,29 +12,24 @@ import Pachyderm
struct ComposeCurrentAccount: View {
@EnvironmentObject var mastodonController: MastodonController
var account: Account? {
mastodonController.account
var account: Account {
mastodonController.account!
}
var body: some View {
HStack(alignment: .top) {
ComposeAvatarImageView(url: account?.avatar)
.accessibility(label: Text(account != nil ? "\(account!.displayName) avatar" : "Avatar"))
ComposeAvatarImageView(url: account.avatar)
.accessibility(label: Text("\(account.displayName) avatar"))
if let id = account?.id,
let account = mastodonController.persistentContainer.account(for: id) {
VStack(alignment: .leading) {
AccountDisplayNameLabel(account: account, fontSize: 20)
.lineLimit(1)
VStack(alignment: .leading) {
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
.lineLimit(1)
Text(verbatim: "@\(account.acct)")
.font(.system(size: 17, weight: .light))
.foregroundColor(.secondary)
.lineLimit(1)
}
Text(verbatim: "@\(account.acct)")
.font(.system(size: 17, weight: .light))
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
}
}
}

View File

@ -58,11 +58,15 @@ class ComposeDrawingViewController: UIViewController {
canvasView.drawing = initialDrawing
}
canvasView.delegate = self
#if SDK_IOS_14
if #available(iOS 14.0, *) {
canvasView.drawingPolicy = .anyInput
} else {
canvasView.allowsFingerDrawing = true
}
#else
canvasView.allowsFingerDrawing = true
#endif
canvasView.minimumZoomScale = 0.5
canvasView.maximumZoomScale = 2
canvasView.backgroundColor = .systemBackground

View File

@ -27,7 +27,13 @@ struct ComposeTextView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color(backgroundColor)
WrappedTextView(
text: $text,
textDidChange: self.textDidChange,
backgroundColor: backgroundColor,
font: .systemFont(ofSize: fontSize)
)
.frame(height: height ?? minHeight)
if text.isEmpty, let placeholder = placeholder {
placeholder
@ -35,13 +41,6 @@ struct ComposeTextView: View {
.foregroundColor(.secondary)
.offset(x: 4, y: 8)
}
WrappedTextView(
text: $text,
textDidChange: self.textDidChange,
font: .systemFont(ofSize: fontSize)
)
.frame(height: height ?? minHeight)
}
}
@ -74,13 +73,14 @@ struct WrappedTextView: UIViewRepresentable {
@Binding var text: String
var textDidChange: ((UITextView) -> Void)?
var backgroundColor = UIColor.secondarySystemBackground
var font = UIFont.systemFont(ofSize: 20)
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isEditable = true
textView.backgroundColor = .clear
textView.backgroundColor = backgroundColor
textView.font = font
textView.textContainer.lineBreakMode = .byWordWrapping
return textView

View File

@ -28,7 +28,7 @@ struct ComposeView: View {
}
var charactersRemaining: Int {
let limit = mastodonController.instance?.maxStatusCharacters ?? 500
let limit = mastodonController.instance.maxStatusCharacters ?? 500
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
return limit - (cwCount + CharacterCounter.count(text: draft.text))
}

View File

@ -20,15 +20,6 @@ struct MainComposeTextView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color(UIColor.secondarySystemBackground)
if draft.text.isEmpty {
placeholder
.font(.system(size: 20))
.foregroundColor(.secondary)
.offset(x: 4, y: 8)
}
MainComposeWrappedTextView(
text: $draft.text,
visibility: draft.visibility,
@ -37,6 +28,13 @@ struct MainComposeTextView: View {
self.height = max(textView.contentSize.height, minHeight)
}
.frame(height: height ?? minHeight)
if draft.text.isEmpty {
placeholder
.font(.system(size: 20))
.foregroundColor(.secondary)
.offset(x: 4, y: 8)
}
}.onAppear {
if !hasFirstAppeared {
hasFirstAppeared = true
@ -61,7 +59,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isEditable = true
textView.backgroundColor = .clear
textView.backgroundColor = .secondarySystemBackground
textView.font = .systemFont(ofSize: 20)
textView.textContainer.lineBreakMode = .byWordWrapping
context.coordinator.textView = textView

View File

@ -52,8 +52,6 @@ class ConversationTableViewController: EnhancedTableViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("Conversation", comment: "conversation screen title")
tableView.delegate = self
tableView.dataSource = self

View File

@ -20,7 +20,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
super.init()
self.viewController = viewController
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
panRecognizer.allowedScrollTypesMask = .all
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .all
}
viewController.view.addGestureRecognizer(panRecognizer)
}

View File

@ -9,6 +9,7 @@
import UIKit
import Pachyderm
#if SDK_IOS_14
@available(iOS 14.0, *)
protocol MainSidebarViewControllerDelegate: class {
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
@ -379,3 +380,4 @@ extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
dismiss(animated: true)
}
}
#endif

View File

@ -8,6 +8,7 @@
import UIKit
#if SDK_IOS_14
@available(iOS 14.0, *)
class MainSplitViewController: UISplitViewController {
@ -315,15 +316,11 @@ extension MainSplitViewController: TuskerRootViewController {
}
func select(tab: MainTabBarViewController.Tab) {
if traitCollection.horizontalSizeClass == .compact {
tabBarViewController?.select(tab: tab)
if tab == .compose {
presentCompose()
} else {
if tab == .compose {
presentCompose()
} else {
select(item: .tab(tab))
sidebar.select(item: .tab(tab), animated: false)
}
select(item: .tab(tab))
}
}
}
#endif

View File

@ -36,9 +36,11 @@ class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
if #available(iOS 13.4, *) {
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
}
}
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {

View File

@ -54,6 +54,8 @@ extension MenuPreviewProvider {
}),
]
// todo: handle pre-iOS 14
#if SDK_IOS_14
if accountID != mastodonController.account.id,
#available(iOS 14.0, *) {
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
@ -69,7 +71,7 @@ extension MenuPreviewProvider {
let following = relationship.following
DispatchQueue.main.async {
elementHandler([
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.minus", handler: { (_) in
let request = (following ? Account.unfollow : Account.follow)(accountID)
mastodonController.run(request) { (_) in
}
@ -80,6 +82,7 @@ extension MenuPreviewProvider {
}
}))
}
#endif
let shareSection = [
openInSafariAction(url: account.url),

View File

@ -47,9 +47,8 @@ enum AppShortcutItem: String, CaseIterable {
}
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
if let controller = window.rootViewController as? TuskerRootViewController {
controller.select(tab: tab)
}
let controller = window.rootViewController as! MainTabBarViewController
controller.select(tab: tab)
}
}

View File

@ -58,11 +58,11 @@ class AttachmentsContainerView: UIView {
attachmentViews.removeAllObjects()
moreView?.removeFromSuperview()
var accessibilityElements = [Any]()
if attachments.count > 0 {
self.isHidden = false
var accessibilityElements = [Any]()
switch attachments.count {
case 1:
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
@ -215,16 +215,13 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(topRight)
accessibilityElements.append(bottomLeft)
accessibilityElements.append(moreView)
}
self.accessibilityElements = accessibilityElements
} else {
self.isHidden = true
}
// Make sure accessibilityElements is set every time the UI is updated, otherwise it holds
// on to strong references to the old set of attachment views
self.accessibilityElements = accessibilityElements
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
}

View File

@ -0,0 +1,56 @@
//
// ComposeStatusReplyView.swift
// Tusker
//
// Created by Shadowfacts on 1/6/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class ComposeStatusReplyView: UIView {
weak var mastodonController: MastodonController?
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var statusContentTextView: StatusContentTextView!
var avatarRequest: ImageCache.Request?
static func create() -> ComposeStatusReplyView {
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
}
deinit {
avatarRequest?.cancel()
}
override func awakeFromNib() {
super.awakeFromNib()
updateUIForPreferences()
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
@objc func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
}
func updateUI(for status: StatusMO) {
displayNameLabel.updateForAccountDisplayName(account: status.account)
usernameLabel.text = "@\(status.account.acct)"
statusContentTextView.overrideMastodonController = mastodonController
statusContentTextView.setTextFrom(status: status)
avatarRequest = ImageCache.avatars.get(status.account.avatar) { [weak self] (data) in
guard let self = self, let data = data else { return }
DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data)
}
}
}
}

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" 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="16082.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ComposeStatusReplyView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ypn-Ed-MTq">
<rect key="frame" x="8" y="8" width="50" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="8qi-gl-5ci"/>
<constraint firstAttribute="width" constant="50" id="Dy2-jh-AJj"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2cE-sS-Uut">
<rect key="frame" x="66" y="8" width="301" height="651"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sdv-dB-Plm" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0yZ-71-eTj">
<rect key="frame" x="115" y="0.0" width="178" height="21"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
<bool key="isElement" value="NO"/>
</accessibility>
<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"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="atN-ay-ceL" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="25" width="301" height="626"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="atN-ay-ceL" secondAttribute="bottom" id="3ub-qq-laF"/>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="6v5-7p-9gm"/>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="YmP-yU-sfe"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="bdX-ge-bMT"/>
<constraint firstAttribute="trailing" secondItem="0yZ-71-eTj" secondAttribute="trailing" constant="8" id="hU7-aZ-ibI"/>
<constraint firstItem="atN-ay-ceL" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="k5c-jg-Dy8"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="leading" secondItem="Sdv-dB-Plm" secondAttribute="trailing" constant="8" id="m0X-YU-m3V"/>
<constraint firstItem="atN-ay-ceL" firstAttribute="top" secondItem="0yZ-71-eTj" secondAttribute="bottom" constant="4" id="pXc-4g-PAe"/>
<constraint firstAttribute="trailing" secondItem="atN-ay-ceL" secondAttribute="trailing" id="qcg-bA-8ba"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="2cE-sS-Uut" firstAttribute="height" relation="greaterThanOrEqual" secondItem="Ypn-Ed-MTq" secondAttribute="height" id="Fn3-o4-RGx"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="2cE-sS-Uut" secondAttribute="bottom" constant="8" id="G2d-Kz-c4e"/>
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="MbW-9d-3gC"/>
<constraint firstItem="2cE-sS-Uut" firstAttribute="leading" secondItem="Ypn-Ed-MTq" secondAttribute="trailing" constant="8" id="TS2-Sr-PB3"/>
<constraint firstItem="2cE-sS-Uut" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="cat-Cr-PSV"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="2cE-sS-Uut" secondAttribute="trailing" constant="8" id="eH4-lG-5UR"/>
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" placeholder="YES" id="xCn-8G-jUZ"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="avatarImageView" destination="Ypn-Ed-MTq" id="eea-bc-klc"/>
<outlet property="displayNameLabel" destination="Sdv-dB-Plm" id="RxW-Ra-Ups"/>
<outlet property="statusContentTextView" destination="atN-ay-ceL" id="i6A-Rd-rJp"/>
<outlet property="usernameLabel" destination="0yZ-71-eTj" id="VQm-Dq-3zP"/>
</connections>
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>
</view>
</objects>
</document>

View File

@ -67,11 +67,15 @@ class ProfileHeaderView: UIView {
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
moreButton.addInteraction(UIPointerInteraction(delegate: self))
if #available(iOS 13.4, *) {
moreButton.addInteraction(UIPointerInteraction(delegate: self))
}
#if SDK_IOS_14
if #available(iOS 14.0, *) {
moreButton.showsMenuAsPrimaryAction = true
moreButton.isContextMenuInteractionEnabled = true
}
#endif
}
func updateUI(for accountID: String) {

View File

@ -87,9 +87,11 @@ class BaseStatusTableViewCell: UITableViewCell {
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
attachmentsView.isAccessibilityElement = true
#if SDK_IOS_14
if #available(iOS 14.0, *) {
moreButton.showsMenuAsPrimaryAction = true
}
#endif
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
@ -120,25 +122,19 @@ class BaseStatusTableViewCell: UITableViewCell {
}
}
final func updateUI(statusID: String, state: StatusState) {
func updateUI(statusID: String, state: StatusState) {
createObserversIfNecessary()
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status")
}
self.statusID = statusID
doUpdateUI(status: status, state: state)
}
func doUpdateUI(status: StatusMO, state: StatusState) {
self.statusState = state
let account = status.account
self.accountID = account.id
updateUI(account: account)
updateUIForPreferences(account: account, status: status)
updateUIForPreferences(account: account)
attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0
@ -198,10 +194,12 @@ class BaseStatusTableViewCell: UITableViewCell {
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
}
#if SDK_IOS_14
if #available(iOS 14.0, *) {
// keep menu in sync with changed states e.g. bookmarked, muted
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(statusID: statusID, sourceView: moreButton))
}
#endif
}
func updateUI(account: AccountMO) {
@ -215,19 +213,18 @@ class BaseStatusTableViewCell: UITableViewCell {
}
}
@objc private func preferencesChanged() {
@objc func preferencesChanged() {
guard let mastodonController = mastodonController,
let account = mastodonController.persistentContainer.account(for: accountID),
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
updateUIForPreferences(account: account, status: status)
updateUIForPreferences(account: account)
updateStatusIconsForPreferences(status)
}
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
func updateUIForPreferences(account: AccountMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.updateForAccountDisplayName(account: account)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
updateStatusIconsForPreferences(status)
}
func updateStatusIconsForPreferences(_ status: StatusMO) {

View File

@ -38,8 +38,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
contentTextView.defaultFont = .systemFont(ofSize: 18)
}
override func doUpdateUI(status: StatusMO, state: StatusState) {
super.doUpdateUI(status: status, state: state)
override func updateUI(statusID: String, state: StatusState) {
super.updateUI(statusID: statusID, state: state)
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() }
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.applicationName {
@ -62,8 +63,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
}
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
super.updateUIForPreferences(account: account, status: status)
override func updateUIForPreferences(account: AccountMO) {
super.updateUIForPreferences(account: account)
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
}

View File

@ -68,9 +68,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
}
}
override func doUpdateUI(status: StatusMO, state: StatusState) {
var status = status
override func updateUI(statusID: String, state: StatusState) {
guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String
if let rebloggedStatus = status.reblog {
reblogStatusID = statusID
rebloggerID = status.account.id
@ -78,24 +79,25 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateRebloggerLabel(reblogger: status.account)
status = rebloggedStatus
statusID = rebloggedStatus.id
realStatusID = rebloggedStatus.id
} else {
reblogStatusID = nil
rebloggerID = nil
reblogLabel.isHidden = true
realStatusID = statusID
}
super.doUpdateUI(status: status, state: state)
super.updateUI(statusID: realStatusID, state: state)
doUpdateTimestamp(status: status)
updateTimestamp()
let pinned = showPinned && (status.pinned ?? false)
timestampLabel.isHidden = pinned
pinImageView.isHidden = !pinned
}
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
super.updateUIForPreferences(account: account, status: status)
@objc override func preferencesChanged() {
super.preferencesChanged()
if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
@ -119,16 +121,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
}
private func updateTimestamp() {
func updateTimestamp() {
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
// so we bail out immediately, since there's nothing to update
guard let mastodonController = mastodonController else { return }
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
doUpdateTimestamp(status: status)
}
private func doUpdateTimestamp(status: StatusMO) {
timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())

View File

@ -52,7 +52,9 @@ class VisualEffectImageButton: UIControl {
imageView.bottomAnchor.constraint(equalTo: vibrancyView.bottomAnchor, constant: -2),
])
#if SDK_IOS_14
addInteraction(UIContextMenuInteraction(delegate: self))
#endif
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
@ -61,10 +63,12 @@ class VisualEffectImageButton: UIControl {
sendActions(for: .touchUpInside)
}
#if SDK_IOS_14
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
guard let menu = menu else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
return menu
}
}
#endif
}

View File

@ -16,16 +16,12 @@ fileprivate class WeakWrapper<T: AnyObject> {
}
}
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
struct WeakArray<Element: AnyObject>: Collection {
private var array: [WeakWrapper<Element>]
var startIndex: Int { array.startIndex }
var endIndex: Int { array.endIndex }
init() {
array = []
}
init(_ elements: [Element]) {
array = elements.map { WeakWrapper($0) }
}
@ -34,20 +30,11 @@ struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollect
array = elements.map { WeakWrapper($0) }
}
subscript(position: Int) -> Element? {
get {
array[position].value
}
set(newValue) {
array[position] = WeakWrapper(newValue)
}
subscript(_ index: Int) -> Element? {
return array[index].value
}
func index(after i: Int) -> Int {
return array.index(after: i)
}
mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
array.replaceSubrange(subrange, with: newElements.map { WeakWrapper($0) })
}
}