Compare commits

...

13 Commits

29 changed files with 164 additions and 283 deletions

View File

@ -173,8 +173,6 @@
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 */; };
@ -502,8 +500,6 @@
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>"; };
@ -1145,15 +1141,6 @@
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 = (
@ -1273,7 +1260,6 @@
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */,
D626494023C122C800612E6E /* Asset Picker */,
D61959D0241E842400A37B8E /* Draft Cell */,
D641C78A213DD926004B4513 /* Status */,
@ -1645,7 +1631,6 @@
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 */,
@ -1818,7 +1803,6 @@
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,6 +89,13 @@
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "DISABLE_IMAGE_CACHE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -15,6 +15,7 @@ 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

@ -15,6 +15,12 @@ class ImageCache {
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
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>
@ -38,8 +44,8 @@ class ImageCache {
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
let key = url.absoluteString
if (try? cache.existsObject(forKey: key)) ?? false,
let data = try? cache.object(forKey: key) {
if !ImageCache.disableCaching,
let data = try? cache.object(forKey: key) {
completion?(data)
return nil
} else {

View File

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

View File

@ -158,15 +158,11 @@ 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,7 +91,9 @@ struct ComposeAttachmentsList: View {
}
private var canAddAttachment: Bool {
switch mastodonController.instance.instanceType {
switch mastodonController.instance?.instanceType {
case nil:
return false
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,7 +19,9 @@ struct ComposeAvatarImageView: View {
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
.onAppear(perform: self.loadImage)
.conditionally(url != nil) {
$0.onAppear(perform: self.loadImage)
}
.onDisappear(perform: self.cancelRequest)
}
@ -27,24 +29,33 @@ struct ComposeAvatarImageView: View {
if let avatarImage = avatarImage {
return Image(uiImage: avatarImage)
} else {
let imageName: String
switch preferences.avatarStyle {
case .circle:
imageName = "person.crop.circle"
case .roundRect:
imageName = "person.crop.square"
}
return Image(systemName: imageName)
return placeholderImage
}
}
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
DispatchQueue.main.async {
self.request = nil
if let data = data, let image = UIImage(data: data) {
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.request = nil
self.avatarImage = image
}
} else {
DispatchQueue.main.async {
self.request = nil
}
}
}
}

View File

@ -12,24 +12,29 @@ 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.displayName) avatar"))
ComposeAvatarImageView(url: account?.avatar)
.accessibility(label: Text(account != nil ? "\(account!.displayName) avatar" : "Avatar"))
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)
if let id = account?.id,
let account = mastodonController.persistentContainer.account(for: id) {
VStack(alignment: .leading) {
AccountDisplayNameLabel(account: account, fontSize: 20)
.lineLimit(1)
Text(verbatim: "@\(account.acct)")
.font(.system(size: 17, weight: .light))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
Spacer()
}
}
}

View File

@ -58,15 +58,11 @@ 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,13 +27,7 @@ struct ComposeTextView: View {
var body: some View {
ZStack(alignment: .topLeading) {
WrappedTextView(
text: $text,
textDidChange: self.textDidChange,
backgroundColor: backgroundColor,
font: .systemFont(ofSize: fontSize)
)
.frame(height: height ?? minHeight)
Color(backgroundColor)
if text.isEmpty, let placeholder = placeholder {
placeholder
@ -41,6 +35,13 @@ struct ComposeTextView: View {
.foregroundColor(.secondary)
.offset(x: 4, y: 8)
}
WrappedTextView(
text: $text,
textDidChange: self.textDidChange,
font: .systemFont(ofSize: fontSize)
)
.frame(height: height ?? minHeight)
}
}
@ -73,14 +74,13 @@ 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 = backgroundColor
textView.backgroundColor = .clear
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,6 +20,15 @@ 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,
@ -28,13 +37,6 @@ 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
@ -59,7 +61,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isEditable = true
textView.backgroundColor = .secondarySystemBackground
textView.backgroundColor = .clear
textView.font = .systemFont(ofSize: 20)
textView.textContainer.lineBreakMode = .byWordWrapping
context.coordinator.textView = textView

View File

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

View File

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

View File

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

View File

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

View File

@ -36,11 +36,9 @@ class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
if #available(iOS 13.4, *) {
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
}
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,8 +54,6 @@ 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
@ -71,7 +69,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.minus", handler: { (_) in
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
let request = (following ? Account.unfollow : Account.follow)(accountID)
mastodonController.run(request) { (_) in
}
@ -82,7 +80,6 @@ extension MenuPreviewProvider {
}
}))
}
#endif
let shareSection = [
openInSafariAction(url: account.url),

View File

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

View File

@ -57,12 +57,12 @@ class AttachmentsContainerView: UIView {
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
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,12 +215,15 @@ 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

@ -1,56 +0,0 @@
//
// 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

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

View File

@ -87,11 +87,9 @@ 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)
}
@ -122,19 +120,25 @@ class BaseStatusTableViewCell: UITableViewCell {
}
}
func updateUI(statusID: String, state: StatusState) {
final 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)
updateUIForPreferences(account: account, status: status)
attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0
@ -194,12 +198,10 @@ 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) {
@ -213,18 +215,19 @@ class BaseStatusTableViewCell: UITableViewCell {
}
}
@objc func preferencesChanged() {
@objc private 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)
updateStatusIconsForPreferences(status)
updateUIForPreferences(account: account, status: status)
}
func updateUIForPreferences(account: AccountMO) {
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
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,10 +38,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
contentTextView.defaultFont = .systemFont(ofSize: 18)
}
override func updateUI(statusID: String, state: StatusState) {
super.updateUI(statusID: statusID, state: state)
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() }
override func doUpdateUI(status: StatusMO, state: StatusState) {
super.doUpdateUI(status: status, state: state)
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.applicationName {
timestampAndClientText += "\(application)"
@ -63,8 +62,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
}
override func updateUIForPreferences(account: AccountMO) {
super.updateUIForPreferences(account: account)
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
super.updateUIForPreferences(account: account, status: status)
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
}

View File

@ -68,10 +68,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
}
}
override func updateUI(statusID: String, state: StatusState) {
guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String
override func doUpdateUI(status: StatusMO, state: StatusState) {
var status = status
if let rebloggedStatus = status.reblog {
reblogStatusID = statusID
rebloggerID = status.account.id
@ -79,25 +78,24 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateRebloggerLabel(reblogger: status.account)
status = rebloggedStatus
realStatusID = rebloggedStatus.id
statusID = rebloggedStatus.id
} else {
reblogStatusID = nil
rebloggerID = nil
reblogLabel.isHidden = true
realStatusID = statusID
}
super.updateUI(statusID: realStatusID, state: state)
updateTimestamp()
super.doUpdateUI(status: status, state: state)
doUpdateTimestamp(status: status)
let pinned = showPinned && (status.pinned ?? false)
timestampLabel.isHidden = pinned
pinImageView.isHidden = !pinned
}
@objc override func preferencesChanged() {
super.preferencesChanged()
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
super.updateUIForPreferences(account: account, status: status)
if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
@ -121,12 +119,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
}
func updateTimestamp() {
private 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,9 +52,7 @@ 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)))
}
@ -63,12 +61,10 @@ 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,12 +16,16 @@ fileprivate class WeakWrapper<T: AnyObject> {
}
}
struct WeakArray<Element: AnyObject>: Collection {
struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
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) }
}
@ -30,11 +34,20 @@ struct WeakArray<Element: AnyObject>: Collection {
array = elements.map { WeakWrapper($0) }
}
subscript(_ index: Int) -> Element? {
return array[index].value
subscript(position: Int) -> Element? {
get {
array[position].value
}
set(newValue) {
array[position] = WeakWrapper(newValue)
}
}
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) })
}
}