Compare commits

..

No commits in common. "5ee140cdab142294dc3e851474874676ca92a1fa" and "13d649bacec6258c44416d04bf2d03d234b56f02" have entirely different histories.

101 changed files with 314 additions and 960 deletions

View File

@ -1,33 +1,5 @@
# Changelog
## 2022.1 (52)
Features/Improvements:
- Save and restore position for all timelines and all accounts
- As a side effect of this change, the first time you launch with this update, the timeline position will be lost
- Add preference to never blur attachments
- New segmented control for timeline switcher that scrolls when there's not enough space
- Copy status expand all setting when viewing More Replies in a converstaion
- Include followed hashtags in the Explore screen and iPad sidebar
- Allow saving or following hashtag from Add Hashtag screen
- Scroll long attachment descriptions in the gallery
- VoiceOver: Improve Profile Directory
- VoiceOver: Include attachment descriptions in timeline items when not marked as sensitive
Bugfixes:
- Fix swipe actions preference not persisting
- Fix rich text list bullets/numbers appearing black in dark mode
- Fix crash when previewing non-HTTP(S) link
- Fix images from Safari pasting as URLs rather than attachments
- Fix Trending Posts appearing to reload forever
- Fix controls reappearing when swiping between pages in the attachments gallery
- Fix reblog confirmation alert actions missing hover effect
- Fix status reply/visibility/local icons flashing blue when expanding a status
- iPad: Fix My Profile item sidebar item not updating when avatar style changes
- iPad: Fix tapping status bar not scrolling to top in single-column navigation mode
- VoiceOver: Fix crash when scrolling through local/federated timeline
- VoiceOver: Fix escape gesture not working in attachment gallery
- VoiceOver: Fix custom emoji not being stripped from display names
## 2022.1 (51)
Features/Improvements:
- Clarify text for conversation main status favorite/reblog count preference

View File

@ -304,8 +304,6 @@
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
@ -561,7 +559,7 @@
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPicker.swift; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAvatarImageView.swift; sourceTree = "<group>"; };
@ -698,8 +696,6 @@
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TuskerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
@ -935,7 +931,6 @@
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
D6D706A62948D4D0000827ED /* TimlineState.swift */,
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
);
@ -1369,7 +1364,6 @@
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */,
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */,
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
D620483723D38190008A63EF /* StatusContentTextView.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
@ -1415,6 +1409,7 @@
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */,
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
@ -1488,7 +1483,6 @@
D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6370B9924421FE00092A7FF /* CoreData */,
D667E5F62135C2ED0057A976 /* Extensions */,
D6D706A12947D954000827ED /* Gestures */,
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
D61959D2241E846D00A37B8E /* Models */,
@ -1529,14 +1523,6 @@
path = TuskerUITests;
sourceTree = "<group>";
};
D6D706A12947D954000827ED /* Gestures */ = {
isa = PBXGroup;
children = (
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
);
path = Gestures;
sourceTree = "<group>";
};
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
isa = PBXGroup;
children = (
@ -2049,7 +2035,6 @@
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
@ -2074,7 +2059,6 @@
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
@ -2307,7 +2291,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2375,7 +2359,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2525,7 +2509,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2554,7 +2538,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2664,7 +2648,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2691,7 +2675,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 51;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -257,8 +257,8 @@ private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) {
]
if let nodeInfo {
crumb.data!["nodeInfo"] = [
"software": nodeInfo.software.name,
"version": nodeInfo.software.version,
"version": nodeInfo.version,
"software": nodeInfo.software,
]
}
SentrySDK.addBreadcrumb(crumb: crumb)

View File

@ -355,15 +355,6 @@ class MastodonCachePersistentStore: NSPersistentContainer {
}
}
func getTimelineState(timeline: Timeline) -> TimelineState? {
do {
let req = TimelineState.fetchRequest(timeline: timeline)
return try viewContext.fetch(req).first
} catch {
return nil
}
}
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
let changes = hasChangedSavedHashtagsOrInstances(notification)
if changes.hashtags {

View File

@ -18,12 +18,6 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
return NSFetchRequest<StatusMO>(entityName: "Status")
}
@nonobjc public class func fetchRequest(id: String) -> NSFetchRequest<StatusMO> {
let req = Self.fetchRequest()
req.predicate = NSPredicate(format: "id = %@", id)
return req
}
@NSManaged public var applicationName: String?
@NSManaged private var attachmentsData: Data?
@NSManaged private var bookmarkedInternal: Bool

View File

@ -1,89 +0,0 @@
//
// TimlineState.swift
// Tusker
//
// Created by Shadowfacts on 12/13/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import CoreData
import Pachyderm
@objc(TimelineState)
public final class TimelineState: NSManagedObject {
@nonobjc public class func fetchRequest(timeline: Timeline) -> NSFetchRequest<TimelineState> {
let req = NSFetchRequest<TimelineState>(entityName: "TimelineState")
req.predicate = NSPredicate(format: "timelineKind = %@", toTimelineKind(timeline))
return req
}
@NSManaged private var timelineKind: String
@NSManaged public var centerStatusID: String?
@NSManaged private var statuses: NSOrderedSet
var timeline: Timeline {
get { fromTimelineKind(timelineKind) }
set { timelineKind = toTimelineKind(newValue) }
}
var statusMOs: [StatusMO] {
statuses.array as! [StatusMO]
}
convenience init(timeline: Timeline, context: NSManagedObjectContext) {
self.init(context: context)
self.timeline = timeline
}
func setStatuses(_ statusIDs: [String]) {
let context = managedObjectContext!
// todo: this feels really inefficient, but I'm not sure if it's better or worse than doing a single "id IN %@" fetch and sorting after
let mos = statusIDs.compactMap { try? context.fetch(StatusMO.fetchRequest(id: $0)).first }
self.statuses = NSOrderedSet(array: mos)
}
}
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate
private func toTimelineKind(_ timeline: Timeline) -> String {
switch timeline {
case .home:
return "home"
case .public(local: true):
return "local"
case .public(local: false):
return "federated"
case .direct:
return "direct"
case .tag(hashtag: let name):
return "hashtag:\(name)"
case .list(id: let id):
return "list:\(id)"
}
}
private func fromTimelineKind(_ kind: String) -> Timeline {
if kind == "home" {
return .home
} else if kind == "local" {
return .public(local: true)
} else if kind == "federated" {
return .public(local: false)
} else if kind == "direct" {
return .direct
} else if kind.starts(with: "hashtag:") {
return .tag(hashtag: String(trimmingPrefix("hashtag:", of: kind)))
} else if kind.starts(with: "list:") {
return .list(id: String(trimmingPrefix("list:", of: kind)))
} else {
fatalError("invalid timeline kind \(kind)")
}
}
// replace with Collection.trimmingPrefix
@available(iOS, obsoleted: 16.0)
private func trimmingPrefix(_ prefix: String, of str: String) -> Substring {
return str[str.index(str.startIndex, offsetBy: prefix.count)...]
}

View File

@ -105,16 +105,10 @@
<attribute name="visibilityString" attributeType="String"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineState" inverseName="statuses" inverseEntity="TimelineState"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="TimelineState" representedClassName="TimelineState" syncable="YES">
<attribute name="centerStatusID" optional="YES" attributeType="String"/>
<attribute name="timelineKind" attributeType="String" valueTransformerName="pachydermTimeline" customClassName="Tusker.TimelineContainer"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Status" inverseName="timelines" inverseEntity="Status"/>
</entity>
</model>

View File

@ -20,30 +20,18 @@ extension AccountMO {
}
var displayNameWithoutCustomEmoji: String {
let stripped = stripCustomEmoji(from: displayName)
if stripped.isEmpty {
if displayName.isEmpty {
return username
} else {
return stripped
return stripCustomEmoji(from: displayName)
}
}
}
extension Account {
var displayNameWithoutCustomEmoji: String {
let stripped = stripCustomEmoji(from: displayName)
if stripped.isEmpty {
return username
} else {
return stripped
}
}
}
private let customEmojiRegex = try! NSRegularExpression(pattern: ":[a-zA-Z0-9_]+:", options: [])
private static let customEmojiRegex = try! NSRegularExpression(pattern: ":[a-zA-Z0-9_]+:", options: [])
private func stripCustomEmoji(from string: String) -> String {
let range = NSRange(location: 0, length: string.utf16.count)
return customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
return AccountMO.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
}
}

View File

@ -106,9 +106,9 @@ struct HTMLConverter {
let index = (try? node.elementSiblingIndex()) ?? 0
// we use the monospace digit font so that the periods of all the list items line up
// TODO: this probably breaks with dynamic type
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: font.pointSize, weight: .regular), .foregroundColor: color])
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: font.pointSize, weight: .regular)])
} else if parentTag == "ul" {
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: font, .foregroundColor: color])
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: font])
} else {
bullet = NSAttributedString()
}

View File

@ -43,8 +43,6 @@ class Preferences: Codable, ObservableObject {
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
@ -54,11 +52,7 @@ class Preferences: Codable, ObservableObject {
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
self.useTwitterKeyboard = try container.decodeIfPresent(Bool.self, forKey: .useTwitterKeyboard) ?? false
if let blurAllMedia = try? container.decodeIfPresent(Bool.self, forKey: .blurAllMedia) {
self.attachmentBlurMode = blurAllMedia ? .always : .useStatusSetting
} else {
self.attachmentBlurMode = try container.decode(AttachmentBlurMode.self, forKey: .attachmentBlurMode)
}
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
@ -92,8 +86,6 @@ class Preferences: Codable, ObservableObject {
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
@ -103,7 +95,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(mentionReblogger, forKey: .mentionReblogger)
try container.encode(useTwitterKeyboard, forKey: .useTwitterKeyboard)
try container.encode(attachmentBlurMode, forKey: .attachmentBlurMode)
try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning)
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
@ -148,12 +140,10 @@ class Preferences: Codable, ObservableObject {
@Published var useTwitterKeyboard = false
// MARK: Media
@Published var attachmentBlurMode = AttachmentBlurMode.useStatusSetting {
@Published var blurAllMedia = false {
didSet {
if attachmentBlurMode == .always {
if blurAllMedia {
blurMediaBehindContentWarning = true
} else if attachmentBlurMode == .never {
blurMediaBehindContentWarning = false
}
}
}
@ -192,8 +182,6 @@ class Preferences: Codable, ObservableObject {
case showIsStatusReplyIcon
case alwaysShowStatusVisibilityIcon
case hideActionsInTimeline
case leadingStatusSwipeActions
case trailingStatusSwipeActions
case defaultPostVisibility
case defaultReplyVisibility
@ -203,8 +191,7 @@ class Preferences: Codable, ObservableObject {
case mentionReblogger
case useTwitterKeyboard
case blurAllMedia // only used for migration
case attachmentBlurMode
case blurAllMedia
case blurMediaBehindContentWarning
case automaticallyPlayGifs
@ -267,23 +254,4 @@ extension Preferences {
}
}
extension Preferences {
enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
case useStatusSetting
case always
case never
var displayName: String {
switch self {
case .useStatusSetting:
return "Default"
case .always:
return "Always"
case .never:
return "Never"
}
}
}
}
extension UIUserInterfaceStyle: Codable {}

View File

@ -131,7 +131,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
minDate.addTimeInterval(-7 * 24 * 60 * 60)
let statusReq: NSFetchRequest<NSFetchRequestResult> = StatusMO.fetchRequest()
statusReq.predicate = NSPredicate(format: "((lastFetchedAt = nil) OR (lastFetchedAt < %@)) AND (timelines.@count = 0)", minDate as NSDate)
statusReq.predicate = NSPredicate(format: "(lastFetchedAt = nil) OR (lastFetchedAt < %@)", minDate as NSDate)
let deleteStatusReq = NSBatchDeleteRequest(fetchRequest: statusReq)
deleteStatusReq.resultType = .resultTypeCount
if let res = try? context.execute(deleteStatusReq) as? NSBatchDeleteResult {

View File

@ -137,11 +137,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
}
}
override func accessibilityPerformEscape() -> Bool {
dismiss(animated: true)
return true
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {

View File

@ -16,7 +16,6 @@ protocol ComposeUIStateDelegate: AnyObject {
func presentAssetPickerSheet()
func presentComposeDrawing()
func selectDraft(_ draft: Draft)
func paste(itemProviders: [NSItemProvider])
}
class ComposeUIState: ObservableObject {

View File

@ -72,24 +72,8 @@ struct ComposeView: View {
return attachmentIds.contains { uiState.attachmentsMissingDescriptions.contains($0) }
}
private var validAttachmentCombination: Bool {
if !mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
return true
} else if draft.attachments.contains(where: { $0.data.type == .video }) && draft.attachments.count > 1 {
return false
} else if draft.attachments.count > 4 {
return false
}
return true
}
private var postButtonEnabled: Bool {
draft.hasContent
&& charactersRemaining >= 0
&& !isPosting
&& !requiresAttachmentDescriptions
&& validAttachmentCombination
&& (draft.poll == nil || draft.poll!.options.allSatisfy { !$0.text.isEmpty })
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions && (draft.poll == nil || draft.poll!.options.allSatisfy { !$0.text.isEmpty })
}
var body: some View {

View File

@ -83,7 +83,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
@Environment(\.isEnabled) var isEnabled: Bool
func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView(uiState: uiState)
let textView = WrappedTextView()
textView.delegate = context.coordinator
textView.isEditable = true
textView.backgroundColor = .clear
@ -128,16 +128,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
class WrappedTextView: UITextView {
private let formattingActions = [#selector(toggleBoldface(_:)), #selector(toggleItalics(_:))]
unowned var uiState: ComposeUIState
init(uiState: ComposeUIState) {
self.uiState = uiState
super.init(frame: .zero, textContainer: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if formattingActions.contains(action) {
@ -164,14 +154,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
}
}
override func paste(_ sender: Any?) {
if UIPasteboard.general.contains(pasteboardTypes: CompositionAttachment.readableTypeIdentifiersForItemProvider) {
uiState.delegate?.paste(itemProviders: UIPasteboard.general.itemProviders)
} else {
super.paste(sender)
}
}
}
class Coordinator: NSObject, UITextViewDelegate, ComposeInput, ComposeTextViewCaretScrolling {

View File

@ -354,7 +354,6 @@ class ConversationTableViewController: EnhancedTableViewController {
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
let conv = ConversationTableViewController(for: id, state: state, mastodonController: mastodonController)
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
conv.showStatusesAutomatically = showStatusesAutomatically
show(conv)
} else {
super.tableView(tableView, didSelectRowAt: indexPath)

View File

@ -84,7 +84,7 @@ class AddSavedHashtagViewController: UIViewController {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
}
override func viewWillAppear(_ animated: Bool) {
@ -107,12 +107,15 @@ class AddSavedHashtagViewController: UIViewController {
}
private func selectHashtag(_ hashtag: Hashtag) {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
let context = mastodonController.persistentContainer.viewContext
_ = SavedHashtag(hashtag: hashtag, context: context)
try! context.save()
presentingViewController!.dismiss(animated: true)
}
// MARK: - Interaction
@objc func doneButtonPressed() {
@objc func cancelButtonPressed() {
dismiss(animated: true)
}
@ -125,6 +128,11 @@ extension AddSavedHashtagViewController {
enum Item: Hashable {
case tag(Hashtag)
}
// class DataSource: UITableViewDiffableDataSource<Section, Item> {
// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// return
// }
// }
}
extension AddSavedHashtagViewController: UICollectionViewDelegate {

View File

@ -24,7 +24,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
var searchControllerStatusOnAppearance: Bool? = nil
private var cancellables = Set<AnyCancellable>()
private var listsCancellable: AnyCancellable?
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
@ -70,26 +70,12 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
mastodonController.$lists
listsCancellable = mastodonController.$lists
.sink { [unowned self] in self.reloadLists($0) }
.store(in: &cancellables)
mastodonController.$followedHashtags
.merge(with:
NotificationCenter.default.publisher(for: .savedHashtagsChanged)
.map { [unowned self] _ in self.mastodonController.followedHashtags }
)
.sink { [unowned self] in self.updateHashtagsSection(followed: $0) }
.store(in: &cancellables)
let a = PassthroughSubject<Int, Never>()
let b = PassthroughSubject<Int, Never>()
a.merge(with: b)
.sink(receiveValue: { print($0) })
.store(in: &cancellables)
}
override func viewWillAppear(_ animated: Bool) {
@ -163,7 +149,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
addDiscoverSection(to: &snapshot)
}
snapshot.appendItems([.addList], toSection: .lists)
let hashtags = fetchHashtagItems(followed: mastodonController.followedHashtags)
let hashtags = fetchSavedHashtags().map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
}
snapshot.appendItems(hashtags, toSection: .savedHashtags)
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
let instances = fetchSavedInstances().map {
@ -205,16 +193,14 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
}
@MainActor
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? []
var items = saved.map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
private func fetchSavedHashtags() -> [SavedHashtag] {
let req = SavedHashtag.fetchRequest()
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.localizedCompare(_:)))]
do {
return try mastodonController.persistentContainer.viewContext.fetch(req)
} catch {
return []
}
for followed in followed where !saved.contains(where: { $0.name == followed.name }) {
items.append(.savedHashtag(Hashtag(name: followed.name, url: followed.url)))
}
items.sort(using: SemiCaseSensitiveComparator.keyPath(\.label))
return items
}
@MainActor
@ -228,10 +214,12 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
}
}
private func updateHashtagsSection(followed: [FollowedHashtag]) {
@objc private func savedHashtagsChanged() {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
let hashtags = fetchHashtagItems(followed: followed)
let hashtags = fetchSavedHashtags().map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
}
snapshot.appendItems(hashtags, toSection: .savedHashtags)
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
dataSource.apply(snapshot)
@ -398,7 +386,7 @@ extension ExploreViewController {
case .lists:
return NSLocalizedString("Lists", comment: "explore lists section title")
case .savedHashtags:
return NSLocalizedString("Hashtags", comment: "explore saved hashtags section title")
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
case .savedInstances:
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
}
@ -437,7 +425,7 @@ extension ExploreViewController {
case let .savedHashtag(hashtag):
return hashtag.name
case .addSavedHashtag:
return NSLocalizedString("Add Hashtag...", comment: "save hashtag nav item title")
return NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
case let .savedInstance(url):
return url.host!
case .findInstance:

View File

@ -123,23 +123,4 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
}
}
// MARK: Accessibility
override var isAccessibilityElement: Bool {
get { true }
set {}
}
override var accessibilityAttributedLabel: NSAttributedString? {
get {
guard let account else {
return nil
}
let s = NSMutableAttributedString(string: "\(account.displayNameWithoutCustomEmoji), ")
s.append(noteTextView.attributedText)
return s
}
set {}
}
}

View File

@ -34,9 +34,8 @@ class ProfileDirectoryViewController: UIViewController {
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
let filterItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: nil)
filterItem.accessibilityLabel = "Filter"
navigationItem.rightBarButtonItem = filterItem
// todo: it would be nice if there were a better "filter" icon
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scope"), menu: nil)
updateFilterMenu()
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in

View File

@ -85,14 +85,14 @@ class TrendingStatusesViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !loaded {
loaded = true
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses])
snapshot.appendItems([.loadingIndicator])
dataSource.apply(snapshot, animatingDifferences: false)
Task {
if !loaded {
loaded = true
await loadTrendingStatuses()
}
}

View File

@ -17,10 +17,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var topControlsView: UIView!
@IBOutlet weak var descriptionTextView: UITextView!
@IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel!
private var shareContainer: UIView!
private var closeContainer: UIView!
private var shareImage: UIImageView!
private var shareButtonTopConstraint: NSLayoutConstraint!
private var shareButtonLeadingConstraint: NSLayoutConstraint!
@ -46,8 +46,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
}
var shrinkGestureEnabled = true
private var isInitialAppearance = true
private var skipUpdatingControlsWhileZooming = false
private var prevZoomScale: CGFloat?
private var isGrayscale = false
private var contentViewSizeObservation: NSKeyValueObservation?
@ -100,14 +98,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
if let imageDescription = imageDescription,
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
descriptionTextView.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
descriptionTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
// i'm not sure why .automatic doesn't work for this
descriptionTextView.contentInsetAdjustmentBehavior = .always
let height = min(150, descriptionTextView.contentSize.height)
descriptionTextView.topAnchor.constraint(equalTo: descriptionTextView.safeAreaLayoutGuide.bottomAnchor, constant: -(height + 16)).isActive = true
descriptionLabel.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
} else {
descriptionTextView.isHidden = true
bottomControlsView.isHidden = true
}
if shrinkGestureEnabled {
@ -123,12 +116,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
view.addGestureRecognizer(doubleTap)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
accessibilityElements = [
topControlsView!,
contentView,
descriptionTextView!,
]
}
private func setupContentView() {
@ -148,9 +135,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
private func setupControls() {
shareContainer = UIView()
shareContainer.isAccessibilityElement = true
shareContainer.accessibilityTraits = .button
shareContainer.accessibilityLabel = "Share"
shareContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(sharePressed)))
shareContainer.translatesAutoresizingMaskIntoConstraints = false
topControlsView.addSubview(shareContainer)
@ -177,10 +161,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
shareImage.heightAnchor.constraint(equalToConstant: 24),
])
closeContainer = UIView()
closeContainer.isAccessibilityElement = true
closeContainer.accessibilityTraits = .button
closeContainer.accessibilityLabel = "Close"
let closeContainer = UIView()
closeContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed)))
closeContainer.translatesAutoresizingMaskIntoConstraints = false
topControlsView.addSubview(closeContainer)
@ -217,11 +198,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
let heightScale = maxHeight / contentView.intrinsicContentSize.height
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
let minScale = min(widthScale, heightScale)
skipUpdatingControlsWhileZooming = true
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
skipUpdatingControlsWhileZooming = false
centerImage()
@ -251,26 +230,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
}
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// the controls view transforms take the safe area insets into account, so they need to be updated
updateControlsView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// on the first appearance, the text view flashes its own scroll indicators automatically
// so we only need to do it on subsequent appearances
if isInitialAppearance {
isInitialAppearance = false
} else {
if animated && controlsVisible && !descriptionTextView.isHidden {
descriptionTextView.flashScrollIndicators()
}
}
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
@ -285,20 +244,17 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
self.contentView.setControlsVisible(controlsVisible)
self.updateControlsView()
}
if controlsVisible && !descriptionTextView.isHidden {
descriptionTextView.flashScrollIndicators()
}
} else {
updateControlsView()
}
}
func updateControlsView() {
let topOffset = self.controlsVisible ? 0 : -(self.topControlsView.bounds.height + self.view.safeAreaInsets.top)
let topOffset = self.controlsVisible ? 0 : -self.topControlsView.bounds.height
self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
if self.imageDescription != nil {
let bottomOffset = self.controlsVisible ? 0 : self.descriptionTextView.bounds.height + self.view.safeAreaInsets.bottom
self.descriptionTextView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
let bottomOffset = self.controlsVisible ? 0 : self.bottomControlsView.bounds.height + self.view.safeAreaInsets.bottom
self.bottomControlsView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
}
}
@ -308,13 +264,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
if !skipUpdatingControlsWhileZooming {
if scrollView.zoomScale <= scrollView.minimumZoomScale {
setControlsVisible(true, animated: true)
} else if scrollView.zoomScale > prevZoomScale {
setControlsVisible(false, animated: true)
}
}
self.prevZoomScale = scrollView.zoomScale
}

View File

@ -10,7 +10,8 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target">
<connections>
<outlet property="descriptionTextView" destination="JZk-BO-2Vh" id="cby-Hc-ezg"/>
<outlet property="bottomControlsView" destination="rPa-Zu-T6g" id="Rgz-AQ-9nt"/>
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
<outlet property="topControlsView" destination="kHo-B9-R7a" id="8sJ-xQ-7ix"/>
<outlet property="view" destination="BJw-5C-9nT" id="1C2-VA-mNf"/>
@ -28,34 +29,41 @@
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
</view>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JZk-BO-2Vh">
<rect key="frame" x="0.0" y="517" width="375" height="150"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstAttribute="height" constant="150" placeholder="YES" id="YfV-kQ-0RT"/>
</constraints>
<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" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rPa-Zu-T6g">
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eo5-fc-RV8">
<rect key="frame" x="16" y="8" width="343" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="eo5-fc-RV8" firstAttribute="top" secondItem="rPa-Zu-T6g" secondAttribute="top" constant="8" id="6n3-E0-2G6"/>
<constraint firstAttribute="trailing" secondItem="eo5-fc-RV8" secondAttribute="trailing" constant="16" id="6uL-vY-tqk"/>
<constraint firstItem="eo5-fc-RV8" firstAttribute="leading" secondItem="rPa-Zu-T6g" secondAttribute="leading" constant="16" id="KIF-vw-K7n"/>
<constraint firstAttribute="bottom" secondItem="eo5-fc-RV8" secondAttribute="bottom" constant="16" id="v43-mS-tyR"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerY" secondItem="BJw-5C-9nT" secondAttribute="centerY" id="0Xb-ib-2hg"/>
<constraint firstAttribute="bottom" secondItem="JZk-BO-2Vh" secondAttribute="bottom" id="7Z2-gW-sPj"/>
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="rPa-Zu-T6g" secondAttribute="trailing" id="2GG-7P-Qv1"/>
<constraint firstItem="w1g-VC-Ll9" firstAttribute="bottom" secondItem="rPa-Zu-T6g" secondAttribute="bottom" id="3qf-5e-vl0"/>
<constraint firstItem="kHo-B9-R7a" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="IvH-gU-Kie"/>
<constraint firstAttribute="trailing" secondItem="JZk-BO-2Vh" secondAttribute="trailing" id="JgV-jy-qjS"/>
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerX" secondItem="BJw-5C-9nT" secondAttribute="centerX" id="KMe-Zc-NZq"/>
<constraint firstItem="Skj-xq-AgQ" firstAttribute="width" secondItem="BJw-5C-9nT" secondAttribute="width" id="Onj-l9-fBu"/>
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="kHo-B9-R7a" secondAttribute="trailing" id="Uh0-ub-R9V"/>
<constraint firstItem="rPa-Zu-T6g" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="asz-Xj-FUC"/>
<constraint firstItem="Skj-xq-AgQ" firstAttribute="height" secondItem="BJw-5C-9nT" secondAttribute="height" id="jvz-QW-n9c"/>
<constraint firstItem="JZk-BO-2Vh" firstAttribute="leading" secondItem="BJw-5C-9nT" secondAttribute="leading" id="kkj-O9-1rE"/>
<constraint firstItem="kHo-B9-R7a" firstAttribute="top" secondItem="BJw-5C-9nT" secondAttribute="top" id="n1O-C3-yQR"/>
</constraints>
<point key="canvasLocation" x="-164" y="475.41229385307349"/>
<point key="canvasLocation" x="-164" y="476"/>
</view>
</objects>
</document>

View File

@ -80,7 +80,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
return
}
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
self.contentConfiguration = config
self.contentConfiguration = contentConfiguration
}
}

View File

@ -30,7 +30,7 @@ class MainSidebarViewController: UIViewController {
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var cancellables = Set<AnyCancellable>()
private var listsCancellable: AnyCancellable?
var allItems: [Item] {
[
@ -101,19 +101,12 @@ class MainSidebarViewController: UIViewController {
select(item: .tab(.timelines), animated: false)
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
mastodonController.$lists
listsCancellable = mastodonController.$lists
.sink { [unowned self] in self.reloadLists($0) }
.store(in: &cancellables)
mastodonController.$followedHashtags
.merge(with:
NotificationCenter.default.publisher(for: .savedHashtagsChanged)
.map { [unowned self] _ in self.mastodonController.followedHashtags }
)
.sink { [unowned self] in self.updateHashtagsSection(followed: $0) }
.store(in: &cancellables)
onViewDidLoad?()
}
@ -183,7 +176,7 @@ class MainSidebarViewController: UIViewController {
applyDiscoverSectionSnapshot()
reloadLists(mastodonController.lists)
updateHashtagsSection(followed: mastodonController.followedHashtags)
reloadSavedHashtags()
reloadSavedInstances()
}
@ -231,16 +224,14 @@ class MainSidebarViewController: UIViewController {
}
@MainActor
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? []
var items = saved.map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
private func fetchSavedHashtags() -> [SavedHashtag] {
let req = SavedHashtag.fetchRequest()
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.localizedCompare(_:)))]
do {
return try mastodonController.persistentContainer.viewContext.fetch(req)
} catch {
return []
}
for followed in followed where !saved.contains(where: { $0.name == followed.name }) {
items.append(.savedHashtag(Hashtag(name: followed.name, url: followed.url)))
}
items.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
return items
}
@MainActor
@ -254,8 +245,10 @@ class MainSidebarViewController: UIViewController {
}
}
private func updateHashtagsSection(followed: [FollowedHashtag]) {
let hashtags = fetchHashtagItems(followed: followed)
@objc private func reloadSavedHashtags() {
let hashtags = fetchSavedHashtags().map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
}
if let selectedItem,
case .savedHashtag(_) = selectedItem,
!hashtags.contains(selectedItem) {
@ -410,13 +403,13 @@ extension MainSidebarViewController {
case .addList:
return "New List..."
case .savedHashtagsHeader:
return "Hashtags"
return "Saved Hashtags"
case let .savedHashtag(hashtag):
return hashtag.name
case .addSavedHashtag:
return "Add Hashtag..."
return "Save Hashtag..."
case .savedInstancesHeader:
return "Instance Timelines"
return "Saved Instances"
case let .savedInstance(url):
return url.host!
case .addSavedInstance:

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
class NotificationsPageViewController: SegmentedPageViewController<NotificationsPageViewController.Page> {
class NotificationsPageViewController: SegmentedPageViewController {
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
@ -30,9 +30,12 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
mentions.title = mentionsTitle
mentions.userActivity = UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
super.init(pages: [
(.all, notificationsTitle, notifications),
(.mentions, mentionsTitle, mentions),
super.init(titles: [
notificationsTitle,
mentionsTitle
], pageControllers: [
notifications,
mentions
])
title = notificationsTitle
@ -50,20 +53,15 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
}
func selectMode(_ mode: NotificationsMode) {
let page: Page
let index: Int
switch mode {
case .allNotifications:
page = .all
index = 0
case .mentionsOnly:
page = .mentions
index = 1
}
segmentedControl.setSelectedOption(page, animated: false)
selectPage(page, animated: false)
}
enum Page {
case all
case mentions
segmentedControl.selectedSegmentIndex = index
selectPage(at: index, animated: false)
}
}

View File

@ -21,18 +21,14 @@ struct MediaPrefsView: View {
var viewingSection: some View {
Section(header: Text("Viewing")) {
Picker(selection: $preferences.attachmentBlurMode) {
ForEach(Preferences.AttachmentBlurMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
} label: {
Text("Blur Media")
Toggle(isOn: $preferences.blurAllMedia) {
Text("Blur All Media")
}
Toggle(isOn: $preferences.blurMediaBehindContentWarning) {
Text("Blur Media Behind Content Warning")
}
.disabled(preferences.attachmentBlurMode != .useStatusSetting)
.disabled(preferences.blurAllMedia)
Toggle(isOn: $preferences.automaticallyPlayGifs) {
Text("Automatically Play GIFs")

View File

@ -142,7 +142,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
let view = ProfileHeaderView.create()
view.delegate = self.profileHeaderDelegate
view.updateUI(for: id)
view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false)
view.pagesSegmentedControl.selectedSegmentIndex = self.owner?.currentIndex ?? 0
cell.addHeader(view)
case .useExistingView(let view):
cell.addHeader(view)

View File

@ -29,11 +29,7 @@ class ProfileViewController: UIViewController {
}
private(set) var currentIndex: Int!
private let pages = [Page.posts, .postsAndReplies, .media]
private var pageControllers: [ProfileStatusesViewController]!
var currentPage: Page {
pages[currentIndex]
}
var currentViewController: ProfileStatusesViewController {
pageControllers[currentIndex]
}
@ -287,14 +283,6 @@ class ProfileViewController: UIViewController {
}
}
extension ProfileViewController {
enum Page: Hashable {
case posts
case postsAndReplies
case media
}
}
extension ProfileViewController {
enum State {
case idle
@ -310,25 +298,24 @@ extension ProfileViewController: ToastableViewController {
}
extension ProfileViewController: ProfileHeaderViewDelegate {
func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: Page) {
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) {
guard case .idle = state else {
headerView.pagesSegmentedControl.setSelectedOption(currentPage, animated: false)
return
}
selectPage(at: pages.firstIndex(of: newPage)!, animated: true)
selectPage(at: newIndex, animated: true)
}
}
extension ProfileViewController: TabbedPageViewController {
func selectNextPage() {
guard currentIndex < pageControllers.count - 1 else { return }
currentViewController.headerCell?.view?.pagesSegmentedControl.setSelectedOption(pages[currentIndex + 1], animated: true)
currentViewController.headerCell?.view?.pagesSegmentedControl.selectedSegmentIndex = currentIndex + 1
selectPage(at: currentIndex + 1, animated: true)
}
func selectPrevPage() {
guard currentIndex > 0 else { return }
currentViewController.headerCell?.view?.pagesSegmentedControl.setSelectedOption(pages[currentIndex - 1], animated: true)
currentViewController.headerCell?.view?.pagesSegmentedControl.selectedSegmentIndex = currentIndex - 1
selectPage(at: currentIndex - 1, animated: true)
}
}

View File

@ -15,8 +15,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
weak var mastodonController: MastodonController!
let filterer: Filterer
var persistsState = false
private(set) var controller: TimelineLikeController<TimelineItem>!
let confirmLoadMore = PassthroughSubject<Void, Never>()
// stored separately because i don't want to query the snapshot every time the user scrolls
@ -198,7 +196,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
if case .notLoadedInitial = controller.state {
if restoreState() {
if doRestore() {
Task {
await checkPresent(jumpImmediately: false)
}
@ -229,23 +227,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
super.viewDidDisappear(animated)
disappearedAt = Date()
saveState()
}
private func saveState() {
guard isViewLoaded,
persistsState else {
return
func stateRestorationActivity() -> NSUserActivity? {
guard isViewLoaded else {
return nil
}
let visible = collectionView.indexPathsForVisibleItems.sorted()
let snapshot = dataSource.snapshot()
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
guard !visible.isEmpty,
guard let currentAccountID = mastodonController.accountInfo?.id,
!visible.isEmpty,
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
return
return nil
}
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
@ -285,31 +282,35 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} else {
fatalError()
}
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(centerVisibleID)")
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) ?? TimelineState(timeline: timeline, context: mastodonController.persistentContainer.viewContext)
state.setStatuses(ids)
state.centerStatusID = centerVisibleID
mastodonController.persistentContainer.save(context: mastodonController.persistentContainer.viewContext)
}
func stateRestorationActivity() -> NSUserActivity? {
guard isViewLoaded,
let currentAccountID = mastodonController.accountInfo?.id else {
return nil
}
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
activity.addUserInfoEntries(from: [
"statusIDs": ids,
"centerID": centerVisibleID,
])
activity.isEligibleForPrediction = false
return activity
}
private func restoreState() -> Bool {
guard persistsState,
Preferences.shared.timelineStateRestoration,
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) else {
func restoreActivity(_ activity: NSUserActivity) {
self.activityToRestore = activity
}
private func doRestore() -> Bool {
guard let activity = activityToRestore,
Preferences.shared.timelineStateRestoration else {
return false
}
guard let statusIDs = activity.userInfo?["statusIDs"] as? [String] else {
stateRestorationLogger.fault("TimelineViewController: activity missing statusIDs")
return false
}
activityToRestore = nil
let existingStatuses = statusIDs.filter { mastodonController.persistentContainer.status(for: $0) != nil }
guard !existingStatuses.isEmpty else {
return false
}
let statusIDs = state.statusMOs.map(\.id)
loadViewIfNeeded()
controller.restoreInitial {
var snapshot = dataSource.snapshot()
@ -317,7 +318,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
let items = statusIDs.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) }
snapshot.appendItems(items, toSection: .statuses)
dataSource.apply(snapshot, animatingDifferences: false) {
if let centerID = state.centerStatusID,
if let centerID = activity.userInfo?["centerID"] as? String ?? activity.userInfo?["topID"] as? String,
let index = statusIDs.firstIndex(of: centerID),
let indexPath = self.dataSource.indexPath(for: items[index]) {
// it sometimes takes multiple attempts to convert on the right scroll position
@ -344,7 +345,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
private func removeTimelineDescriptionCell() {
var snapshot = dataSource.snapshot()
snapshot.deleteItems([.publicTimelineDescription])
snapshot.deleteSections([.header])
dataSource.apply(snapshot, animatingDifferences: true)
isShowingTimelineDescription = false
}
@ -399,7 +400,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return
}
disappearedAt = Date()
saveState()
}
@objc func refresh() {

View File

@ -9,7 +9,7 @@
import UIKit
import SwiftUI
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
class TimelinesPageViewController: SegmentedPageViewController {
private let homeTitle = NSLocalizedString("Home", comment: "home timeline tab title")
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
@ -22,20 +22,21 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
home.title = homeTitle
home.persistsState = true
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
federated.title = federatedTitle
federated.persistsState = true
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
local.title = localTitle
local.persistsState = true
super.init(pages: [
(.home, "Home", home),
(.local, "Local", local),
(.federated, "Federated", federated),
super.init(titles: [
homeTitle,
federatedTitle,
localTitle
], pageControllers: [
home,
federated,
local
])
title = homeTitle
@ -74,28 +75,24 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
return
}
let page: Page
let index: Int
switch timeline {
case .home:
page = .home
index = 0
case .public(local: false):
page = .federated
index = 1
case .public(local: true):
page = .local
index = 2
default:
return
}
selectPage(page, animated: false)
selectPage(at: index, animated: false)
let timelineVC = pageControllers[index] as! TimelineViewController
timelineVC.restoreActivity(activity)
}
@objc private func filtersPressed() {
present(UIHostingController(rootView: FiltersView(mastodonController: mastodonController)), animated: true)
}
enum Page: Hashable {
case home
case local
case federated
}
}

View File

@ -394,8 +394,6 @@ class CustomAlertActionButton: UIControl {
self.isContextMenuInteractionEnabled = true
self.showsMenuAsPrimaryAction = action.handler == nil
}
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
}
required init?(coder: NSCoder) {
@ -431,17 +429,6 @@ class CustomAlertActionButton: UIControl {
super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
}
@objc func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
backgroundColor = .secondarySystemFill
case .ended:
backgroundColor = nil
default:
break
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)

View File

@ -8,45 +8,33 @@
import UIKit
class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageViewControllerDelegate, TabbedPageViewController {
class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDelegate {
let pages: [Page]
let titles: [String]
let pageControllers: [UIViewController]
private var initialPage: Page
private var currentPage: Page
var currentIndex: Int {
pages.firstIndex(of: currentPage)!
}
private var initialIndex = 0
private(set) var currentIndex = 0
let segmentedControl = ScrollingSegmentedControl<Page>()
var segmentedControl: UISegmentedControl!
init(pages: [(Page, String, UIViewController)]) {
precondition(!pages.isEmpty)
init(titles: [String], pageControllers: [UIViewController]) {
precondition(!pageControllers.isEmpty)
self.pages = pages.map(\.0)
self.pageControllers = pages.map(\.2)
initialPage = self.pages.first!
currentPage = self.pages.first!
self.titles = titles
self.pageControllers = pageControllers
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.delegate = self
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
// before the view has necessarily loaded
segmentedControl.options = pages.map {
.init(value: $0.0, name: $0.1)
}
segmentedControl.didSelectOption = { [unowned self] option in
if let option {
self.selectPage(option, animated: true)
}
}
// TODO: the custom segmented control isn't treated as a group and I have no idea how to change that
segmentedControl = UISegmentedControl(items: titles)
segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged)
// the segemented control itself is only focusable when VoiceOver is in Group navigation mode,
// so make it clear that to switch tabs the user needs to enter the group
segmentedControl.accessibilityHint = "Enter group to select timeline"
segmentedControl.setSelectedOption(segmentedControl.options.first!.value, animated: false)
navigationItem.titleView = segmentedControl
}
@ -59,7 +47,7 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
view.backgroundColor = .systemBackground
selectPage(initialPage, animated: false)
selectPage(at: initialIndex, animated: false)
addKeyCommand(MenuController.prevSubTabCommand)
addKeyCommand(MenuController.nextSubTabCommand)
@ -72,36 +60,28 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
}
}
func selectPage(_ page: Page, animated: Bool) {
guard pages.contains(page) else {
fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages")
}
func selectPage(at index: Int, animated: Bool) {
guard isViewLoaded else {
initialPage = page
initialIndex = index
return
}
let prevIndex = currentIndex
currentPage = page
let index = pages.firstIndex(of: page)!
let newController = pageControllers[index]
let direction: UIPageViewController.NavigationDirection = index - prevIndex > 0 ? .forward : .reverse
setViewControllers([newController], direction: direction, animated: animated)
navigationItem.title = newController.title
segmentedControl.setSelectedOption(page, animated: animated)
let direction: UIPageViewController.NavigationDirection = index - currentIndex > 0 ? .forward : .reverse
setViewControllers([pageControllers[index]], direction: direction, animated: animated)
navigationItem.title = pageControllers[index].title
currentIndex = index
segmentedControl.selectedSegmentIndex = index
}
// MARK: TabbedPageViewController
func selectNextPage() {
guard currentIndex < pageControllers.count - 1 else { return }
selectPage(pages[currentIndex + 1], animated: true)
@objc func segmentedControlChanged() {
selectPage(at: segmentedControl.selectedSegmentIndex, animated: true)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
func selectPrevPage() {
guard currentIndex > 0 else { return }
selectPage(pages[currentIndex - 1], animated: true)
// MARK: - Page View Controller Delegate
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
currentIndex = pageControllers.firstIndex(of: viewControllers!.first!)!
segmentedControl.selectedSegmentIndex = currentIndex
navigationItem.title = viewControllers!.first!.title
}
}
@ -114,6 +94,18 @@ extension SegmentedPageViewController: TabBarScrollableViewController {
}
}
extension SegmentedPageViewController: TabbedPageViewController {
func selectNextPage() {
guard currentIndex < pageControllers.count - 1 else { return }
selectPage(at: currentIndex + 1, animated: true)
}
func selectPrevPage() {
guard currentIndex > 0 else { return }
selectPage(at: currentIndex - 1, animated: true)
}
}
extension SegmentedPageViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
if let current = pageControllers[currentIndex] as? BackgroundableViewController {

View File

@ -245,7 +245,7 @@ extension SplitNavigationController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
let vcs = viewControllers
if !canShowSecondaryNav || vcs.count < 2 {
return (vcs.last! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
return (vcs.first! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
} else {
let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
let positionInSecondary = secondaryNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)

View File

@ -217,19 +217,20 @@ class UserActivityManager {
switch timeline {
case .home, .public(true), .public(false):
navigationController.popToRootViewController(animated: false)
let rootController = navigationController.viewControllers.first! as! TimelinesPageViewController
let page: TimelinesPageViewController.Page
let rootController = navigationController.viewControllers.first! as! SegmentedPageViewController
let index: Int
switch timeline {
case .home:
page = .home
case .public(local: false):
page = .federated
case .public(local: true):
page = .local
index = 0
case .public(false):
index = 1
case .public(true):
index = 2
default:
fatalError()
}
rootController.selectPage(page, animated: false)
rootController.segmentedControl.selectedSegmentIndex = index
rootController.selectPage(at: index, animated: false)
default:
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
navigationController.pushViewController(timeline, animated: false)

View File

@ -94,7 +94,7 @@ class AccountCollectionViewCell: UICollectionViewListCell {
let account = mastodonController.persistentContainer.account(for: accountID) else {
return nil
}
var str = AttributedString(account.displayNameWithoutCustomEmoji)
var str = AttributedString(account.displayOrUserName)
str += ", @"
str += AttributedString(account.acct)
return NSAttributedString(str)

View File

@ -173,17 +173,15 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
// MARK: - Navigation
func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController? {
func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController {
let text = (self.text as NSString).substring(with: range)
if let mention = getMention(for: url, text: text) {
return ProfileViewController(accountID: mention.id, mastodonController: mastodonController!)
} else if let tag = getHashtag(for: url, text: text) {
return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!)
} else if url.scheme == "https" || url.scheme == "http" {
return SFSafariViewController(url: url)
} else {
return nil
return SFSafariViewController(url: url)
}
}

View File

@ -11,7 +11,7 @@ import Pachyderm
import Combine
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page)
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
}
class ProfileHeaderView: UIView {
@ -35,11 +35,10 @@ class ProfileHeaderView: UIView {
@IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var lockImageView: UIImageView!
@IBOutlet weak var vStack: UIStackView!
@IBOutlet weak var relationshipLabel: UILabel!
@IBOutlet weak var noteTextView: StatusContentTextView!
@IBOutlet weak var fieldsView: ProfileFieldsView!
private(set) var pagesSegmentedControl: ScrollingSegmentedControl<ProfileViewController.Page>!
@IBOutlet weak var pagesSegmentedControl: UISegmentedControl!
var accountID: String!
@ -84,22 +83,6 @@ class ProfileHeaderView: UIView {
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.adjustsFontForContentSizeCategory = true
pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero)
pagesSegmentedControl.options = [
.init(value: .posts, name: "Posts"),
.init(value: .postsAndReplies, name: "Posts and Replies"),
.init(value: .media, name: "Media"),
]
pagesSegmentedControl.setSelectedOption(.posts, animated: false)
pagesSegmentedControl.didSelectOption = { [unowned self] newPage in
if let newPage {
self.delegate?.profileHeader(self, selectedPageChangedTo: newPage)
}
}
vStack.addArrangedSubview(pagesSegmentedControl)
// equal inset on both sides, the leading inset is applied to the vStack
pagesSegmentedControl.widthAnchor.constraint(equalTo: vStack.widthAnchor, constant: -16).isActive = true
// the segemented control itself is only focusable when VoiceOver is in Group navigation mode,
// so make it clear that to switch tabs the user needs to enter the group
pagesSegmentedControl.accessibilityHint = "Enter group to select scope"
@ -281,6 +264,11 @@ class ProfileHeaderView: UIView {
delegate?.showLoadingLargeImage(url: header, cache: .headers, description: nil, animatingFrom: headerImageView)
}
@IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) {
delegate?.profileHeader(self, selectedPostsIndexChangedTo: sender.selectedSegmentIndex)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
}
extension ProfileHeaderView: UIPointerInteractionDelegate {

View File

@ -69,22 +69,42 @@
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1O8-2P-Gbf" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="382" height="460"/>
<rect key="frame" x="0.0" y="0.0" width="382" height="259.5"/>
<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"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="468" width="398" height="128"/>
<rect key="frame" x="0.0" y="267.5" width="398" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
</constraints>
</view>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="n1M-vM-Cj0">
<rect key="frame" x="0.0" y="403.5" width="382" height="185"/>
<segments>
<segment title="Posts"/>
<segment title="Posts and Replies"/>
<segment title="Media"/>
</segments>
<connections>
<action selector="postsSegmentedControlChanged:" destination="iN0-l3-epB" eventType="valueChanged" id="D6y-ZM-DwU"/>
</connections>
</segmentedControl>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ja-fK-Fqz">
<rect key="frame" x="0.0" y="595.5" width="398" height="0.5"/>
<color key="backgroundColor" systemColor="separatorColor"/>
<constraints>
<constraint firstAttribute="height" constant="0.5" id="VwS-gV-q8M"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="vKC-m1-Sbs" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="0dI-ax-7eI"/>
<constraint firstItem="n1M-vM-Cj0" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="9Ds-zl-acc"/>
<constraint firstItem="5ja-fK-Fqz" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="azv-le-93y"/>
<constraint firstItem="1O8-2P-Gbf" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="hnA-3G-B9B"/>
</constraints>
</stackView>
@ -104,13 +124,6 @@
</imageView>
</subviews>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ja-fK-Fqz">
<rect key="frame" x="16" y="861.5" width="398" height="0.5"/>
<color key="backgroundColor" systemColor="separatorColor"/>
<constraints>
<constraint firstAttribute="height" constant="0.5" id="VwS-gV-q8M"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
@ -120,11 +133,9 @@
<constraint firstItem="wT9-2J-uSY" firstAttribute="centerY" secondItem="dgG-dR-lSv" secondAttribute="bottom" id="7gb-T3-Xe7"/>
<constraint firstItem="vcl-Gl-kXl" firstAttribute="top" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="8" symbolic="YES" id="7ss-Mf-YYH"/>
<constraint firstItem="vcl-Gl-kXl" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="8ho-WU-MxW"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="5ja-fK-Fqz" secondAttribute="bottom" id="9ZS-Ey-eKd"/>
<constraint firstItem="jwU-EH-hmC" firstAttribute="top" secondItem="vcl-Gl-kXl" secondAttribute="bottom" id="9lx-Fn-M0U"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="u4P-3i-gEq" secondAttribute="bottom" id="9zc-N2-mfI"/>
<constraint firstItem="bRJ-Xf-kc9" firstAttribute="bottom" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="-8" id="AXS-bG-20Q"/>
<constraint firstAttribute="trailing" secondItem="5ja-fK-Fqz" secondAttribute="trailing" id="EMk-dp-yJV"/>
<constraint firstItem="jwU-EH-hmC" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="TkY-oK-if4" secondAttribute="bottom" id="HBR-rg-RxO"/>
<constraint firstItem="dgG-dR-lSv" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="VD1-yc-KSa"/>
<constraint firstItem="wT9-2J-uSY" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="WNS-AR-ff2"/>
@ -132,7 +143,6 @@
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="vcl-Gl-kXl" secondAttribute="trailing" constant="16" id="e38-Od-kPg"/>
<constraint firstItem="u4P-3i-gEq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="hgl-UR-o3W"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dgG-dR-lSv" secondAttribute="trailing" id="j0d-hY-815"/>
<constraint firstItem="5ja-fK-Fqz" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="jPG-WM-9km"/>
<constraint firstItem="jwU-EH-hmC" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="oGR-6M-gdd"/>
<constraint firstAttribute="trailing" secondItem="u4P-3i-gEq" secondAttribute="trailing" id="ph6-NT-A02"/>
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="wT9-2J-uSY" secondAttribute="bottom" priority="999" constant="8" id="tKQ-6d-Z55"/>
@ -147,9 +157,9 @@
<outlet property="lockImageView" destination="KNY-GD-beC" id="9EJ-iM-Eos"/>
<outlet property="moreButton" destination="bRJ-Xf-kc9" id="zIN-pz-L7y"/>
<outlet property="noteTextView" destination="1O8-2P-Gbf" id="yss-zZ-uQ5"/>
<outlet property="pagesSegmentedControl" destination="n1M-vM-Cj0" id="TCU-ku-YZN"/>
<outlet property="relationshipLabel" destination="UF8-nI-KVj" id="dTe-DQ-eJV"/>
<outlet property="usernameLabel" destination="1C3-Pd-QiL" id="57b-LQ-3pM"/>
<outlet property="vStack" destination="u4P-3i-gEq" id="EUC-d2-cQC"/>
</connections>
<point key="canvasLocation" x="-590" y="117"/>
</view>

View File

@ -1,228 +0,0 @@
//
// ScrollingSegmentedControl.swift
// Tusker
//
// Created by Shadowfacts on 12/11/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecognizerDelegate, UIPointerInteractionDelegate {
private(set) var selectedOption: Value?
var options: [Option] = [] {
didSet {
createOptionViews()
}
}
var didSelectOption: ((Value?) -> Void)?
private let optionsStack = UIStackView()
private let selectedIndicatorView = UIView()
private var selectedIndicatorViewAlignmentConstraints: [NSLayoutConstraint] = []
private var changeSelectionPanRecognizer: UIGestureRecognizer!
private var selectedOptionAtStartOfPan: Value?
private lazy var selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
override var intrinsicContentSize: CGSize {
let buttonWidths = optionsStack.arrangedSubviews.map(\.intrinsicContentSize.width).reduce(0, +)
let spacing = (CGFloat(optionsStack.arrangedSubviews.count) - 1) * 8
// add 16 to account for the spacing around optionsStack
return CGSize(width: buttonWidths + spacing + 16, height: 44)
}
override init(frame: CGRect) {
super.init(frame: frame)
showsHorizontalScrollIndicator = false
optionsStack.axis = .horizontal
optionsStack.spacing = 8
optionsStack.distribution = .fillProportionally
optionsStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(optionsStack)
NSLayoutConstraint.activate([
optionsStack.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 8),
optionsStack.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -8),
optionsStack.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
optionsStack.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
optionsStack.heightAnchor.constraint(equalTo: heightAnchor),
// add 16 to account for the spacing around optionsStack
widthAnchor.constraint(lessThanOrEqualTo: optionsStack.widthAnchor, constant: 16),
])
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
self.changeSelectionPanRecognizer = panRecognizer
panRecognizer.delegate = self
optionsStack.addGestureRecognizer(panRecognizer)
optionsStack.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(optionTapped)))
optionsStack.addInteraction(UIPointerInteraction(delegate: self))
self.panGestureRecognizer.delegate = self
selectedIndicatorView.isHidden = true
selectedIndicatorView.backgroundColor = .tintColor
selectedIndicatorView.translatesAutoresizingMaskIntoConstraints = false
addSubview(selectedIndicatorView)
NSLayoutConstraint.activate([
selectedIndicatorView.heightAnchor.constraint(equalToConstant: 4),
selectedIndicatorView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func createOptionViews() {
optionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
for (index, option) in options.enumerated() {
let label = UILabel()
label.text = option.name
label.font = .preferredFont(forTextStyle: .headline)
label.adjustsFontForContentSizeCategory = true
label.textColor = .secondaryLabel
label.textAlignment = .center
label.accessibilityTraits = .button
label.accessibilityLabel = "\(option.name), \(index + 1) of \(options.count)"
optionsStack.addArrangedSubview(label)
}
}
func setSelectedOption(_ value: Value, animated: Bool) {
guard selectedOption != value,
options.contains(where: { $0.value == value }) else {
return
}
if selectedOption != nil {
selectionChangedFeedbackGenerator.selectionChanged()
}
selectedOption = value
didSelectOption?(value)
updateSelectedIndicatorView()
if animated && !selectedIndicatorView.isHidden {
let animator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.8) {
self.layoutIfNeeded()
}
animator.startAnimation()
}
}
private func updateSelectedIndicatorView() {
guard let selectedOption,
let selectedIndex = options.firstIndex(where: { $0.value == selectedOption }) else {
selectedIndicatorView.isHidden = true
return
}
let selectedOptionView = optionsStack.arrangedSubviews[selectedIndex]
selectedIndicatorView.isHidden = false
NSLayoutConstraint.deactivate(selectedIndicatorViewAlignmentConstraints)
selectedIndicatorViewAlignmentConstraints = [
selectedIndicatorView.leadingAnchor.constraint(equalTo: selectedOptionView.leadingAnchor),
selectedIndicatorView.trailingAnchor.constraint(equalTo: selectedOptionView.trailingAnchor),
]
NSLayoutConstraint.activate(selectedIndicatorViewAlignmentConstraints)
for (index, optionView) in optionsStack.arrangedSubviews.enumerated() {
let label = optionView as! UILabel
label.textColor = index == selectedIndex ? .label : .secondaryLabel
label.accessibilityTraits = index == selectedIndex ? [.button, .selected] : .button
}
}
// MARK: Interaction
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let beganOnSelectedOption: Bool
if let selectedIndex = options.firstIndex(where: { $0.value == selectedOption }),
optionsStack.arrangedSubviews[selectedIndex].frame.contains(self.panGestureRecognizer.location(in: optionsStack)) {
beganOnSelectedOption = true
} else {
beganOnSelectedOption = false
}
// only begin changing selection if the gesutre started on the currently selected item
// otherwise, let the scroll view handle things
if gestureRecognizer == self.changeSelectionPanRecognizer {
return beganOnSelectedOption
} else {
return !beganOnSelectedOption
}
}
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
let horizontalLocationInStack = CGPoint(x: recognizer.location(in: optionsStack).x, y: 0)
switch recognizer.state {
case .began:
selectedOptionAtStartOfPan = selectedOption
selectionChangedFeedbackGenerator.prepare()
case .changed:
if updateSelectionFor(location: horizontalLocationInStack) {
selectionChangedFeedbackGenerator.prepare()
}
case .ended:
if let selectedOptionAtStartOfPan {
self.selectedOptionAtStartOfPan = nil
if let selectedOption,
selectedOptionAtStartOfPan != selectedOption {
didSelectOption?(selectedOption)
}
}
default:
break
}
}
@objc private func optionTapped(_ recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: optionsStack)
if updateSelectionFor(location: location) {
didSelectOption?(selectedOption!)
}
}
private func updateSelectionFor(location: CGPoint) -> Bool {
for (index, optionView) in optionsStack.arrangedSubviews.enumerated() where optionView.frame.contains(location) {
if selectedOption != options[index].value {
selectedOption = options[index].value
let animator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.8) {
self.updateSelectedIndicatorView()
self.scrollRectToVisible(optionView.frame.insetBy(dx: -16, dy: 0), animated: false)
self.layoutIfNeeded()
}
animator.startAnimation()
selectionChangedFeedbackGenerator.selectionChanged()
return true
} else {
return false
}
}
return false
}
func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
func distanceToAnyEdge(_ view: UIView) -> CGFloat {
min(abs(view.frame.minX - request.location.x), abs(view.frame.maxX - request.location.x))
}
let (view, index, _) = optionsStack.arrangedSubviews.enumerated().map { ($0.1, $0.0, distanceToAnyEdge($0.1)) }.min(by: { $0.2 < $1.2 })!
return UIPointerRegion(rect: view.frame.insetBy(dx: -8, dy: 0), identifier: index)
}
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
let index = region.identifier as! Int
let optionView = optionsStack.arrangedSubviews[index]
return UIPointerStyle(effect: .hover(UITargetedPreview(view: optionView)))
}
struct Option: Hashable {
let value: Value
let name: String
}
}

View File

@ -231,18 +231,17 @@ class BaseStatusTableViewCell: UITableViewCell {
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
switch Preferences.shared.attachmentBlurMode {
case .never:
attachmentsView.contentHidden = false
case .always:
if Preferences.shared.blurAllMedia {
attachmentsView.contentHidden = true
default:
if status.sensitive {
attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
} else if status.sensitive {
if !Preferences.shared.blurMediaBehindContentWarning && !status.spoilerText.isEmpty {
attachmentsView.contentHidden = false
} else {
attachmentsView.contentHidden = true
}
} else {
attachmentsView.contentHidden = false
}
}
updateStatusIconsForPreferences(status)

View File

@ -148,18 +148,17 @@ extension StatusCollectionViewCell {
func baseUpdateUIForPreferences(status: StatusMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
switch Preferences.shared.attachmentBlurMode {
case .never:
contentContainer.attachmentsView.contentHidden = false
case .always:
if Preferences.shared.blurAllMedia {
contentContainer.attachmentsView.contentHidden = true
default:
if status.sensitive {
contentContainer.attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
} else if status.sensitive {
if !Preferences.shared.blurMediaBehindContentWarning && !status.spoilerText.isEmpty {
contentContainer.attachmentsView.contentHidden = false
} else {
contentContainer.attachmentsView.contentHidden = true
}
} else {
contentContainer.attachmentsView.contentHidden = false
}
}
let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogEnabled(status: status) {

View File

@ -19,7 +19,6 @@ class StatusMetaIndicatorsView: UIView {
var secondaryAxisAlignment: Alignment = .leading
private var images: [UIImageView] = []
private var isUsingSingleAxis = false
private var statusID: String?
private var needsSingleAxis: Bool {
traitCollection.preferredContentSizeCategory > .extraLarge
@ -62,11 +61,6 @@ class StatusMetaIndicatorsView: UIView {
}
func updateUI(status: StatusMO) {
guard statusID != status.id else {
return
}
statusID = status.id
var images: [UIImage] = []
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {

View File

@ -369,13 +369,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
return nil
}
var str: AttributedString = ""
if let rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
str += AttributedString("Reblogged by \(reblogger.displayNameWithoutCustomEmoji): ")
}
str += AttributedString(status.account.displayNameWithoutCustomEmoji)
str += ", "
var str = AttributedString("\(status.account.displayOrUserName), ")
if statusState.collapsed ?? false {
if !status.spoilerText.isEmpty {
str += AttributedString(status.spoilerText)
@ -384,37 +378,15 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
str += "collapsed"
} else {
str += AttributedString(contentTextView.attributedText)
}
if status.attachments.count > 0 {
let includeDescriptions: Bool
switch Preferences.shared.attachmentBlurMode {
case .useStatusSetting:
includeDescriptions = !Preferences.shared.blurMediaBehindContentWarning || status.spoilerText.isEmpty
case .always:
includeDescriptions = true
case .never:
includeDescriptions = false
}
if includeDescriptions {
if status.attachments.count == 1 {
let attachment = status.attachments[0]
let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
str += AttributedString(", attachment: \(desc)")
} else {
for (index, attachment) in status.attachments.enumerated() {
let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
str += AttributedString(", attachment \(index + 1): \(desc)")
}
}
} else {
str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count == 1 ? "" : "s")")
}
// TODO: localize me
str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count > 1 ? "s" : "")")
}
if status.poll != nil {
str += ", poll"
}
}
str += AttributedString(", \(status.createdAt.formatted(.relative(presentation: .numeric)))")
if status.visibility < .unlisted {
str += AttributedString(", \(status.visibility.displayName)")
@ -422,6 +394,10 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
if status.localOnly {
str += ", local"
}
if let rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
str += AttributedString(", reblogged by \(reblogger.displayOrUserName)")
}
return NSAttributedString(str)
}
set {}

View File

@ -254,13 +254,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
return nil
}
var str: AttributedString = ""
if let rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
str += AttributedString("Reblogged by \(reblogger.displayNameWithoutCustomEmoji): ")
}
str += AttributedString(status.account.displayNameWithoutCustomEmoji)
str += ", "
var str = AttributedString("\(status.account.displayOrUserName), ")
if statusState.collapsed ?? false {
if !status.spoilerText.isEmpty {
str += AttributedString(status.spoilerText)
@ -269,37 +263,15 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
str += "collapsed"
} else {
str += AttributedString(contentTextView.attributedText)
}
if status.attachments.count > 0 {
let includeDescriptions: Bool
switch Preferences.shared.attachmentBlurMode {
case .useStatusSetting:
includeDescriptions = !Preferences.shared.blurMediaBehindContentWarning || status.spoilerText.isEmpty
case .always:
includeDescriptions = true
case .never:
includeDescriptions = false
}
if includeDescriptions {
if status.attachments.count == 1 {
let attachment = status.attachments[0]
let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
str += AttributedString(", attachment: \(desc)")
} else {
for (index, attachment) in status.attachments.enumerated() {
let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
str += AttributedString(", attachment \(index + 1): \(desc)")
}
}
} else {
str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count == 1 ? "" : "s")")
}
// TODO: localize me
str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count > 1 ? "s" : "")")
}
if status.poll != nil {
str += ", poll"
}
}
str += AttributedString(", \(status.createdAt.formatted(.relative(presentation: .numeric)))")
if status.visibility < .unlisted {
str += AttributedString(", \(status.visibility.displayName)")
@ -307,6 +279,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
if status.localOnly {
str += ", local"
}
if let rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
str += AttributedString(", reblogged by \(reblogger.displayOrUserName)")
}
return NSAttributedString(str)
}
set {}

Some files were not shown because too many files have changed in this diff Show More