Compare commits
17 Commits
cdc64f1b2c
...
2e2279ba8c
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 2e2279ba8c | |
Shadowfacts | 60dadf599c | |
Shadowfacts | 90537f9d12 | |
Shadowfacts | 8b0c2f80b6 | |
Shadowfacts | 42423f36db | |
Shadowfacts | 176eb7c011 | |
Shadowfacts | da9ca78a8b | |
Shadowfacts | b470ee6401 | |
Shadowfacts | fccd4e427c | |
Shadowfacts | f25031afd4 | |
Shadowfacts | ca65f84137 | |
Shadowfacts | d4057adf4d | |
Shadowfacts | 007937d2d7 | |
Shadowfacts | 5f040ed390 | |
Shadowfacts | 870d0c8404 | |
Shadowfacts | 47b9ac890a | |
Shadowfacts | 50b84350d9 |
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.3 (129)
|
||||||
|
Bugfixes:
|
||||||
|
- Fix excessive network traffic on profile pages
|
||||||
|
- Fix attachment gallery controls visibility not being synced between pages
|
||||||
|
- Fix video attachments not restarting when play pressed while at ends
|
||||||
|
- Fix profile field text being misaligned
|
||||||
|
- Fix at sign in timeline statuses usernames sometimes clipping
|
||||||
|
- Fix add hashtag/instance to Pinned Timelines sheets dismissing immediately when opened
|
||||||
|
- Fix for display name being replaced with incorrect user in certain circumstances
|
||||||
|
- Fix profile moved overlay view appearing behind avatar/header
|
||||||
|
- Fix profile moved view accessibility with VoiceOver
|
||||||
|
- Fix mention/status push notifications not showing content warning
|
||||||
|
- Fix sensitive attachment thumbnails being shown in push notifications
|
||||||
|
- Fix Dynamic Type not applying to status content
|
||||||
|
- Fix expand all option in Conversation not transferring when opening ancestors
|
||||||
|
- Fix not being able to resolve remote Mastodon status links in Conversation screen
|
||||||
|
- Fix status indicator icons overlapping thread links when Dynamic Type is enabled
|
||||||
|
|
||||||
## 2024.3 (128)
|
## 2024.3 (128)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix selecting poll option playing too much haptic feedback
|
- Fix selecting poll option playing too much haptic feedback
|
||||||
|
|
|
@ -122,7 +122,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
let notificationContent: String?
|
let notificationContent: String?
|
||||||
if let status = notification.status {
|
if let status = notification.status {
|
||||||
notificationContent = NotificationService.textConverter.convert(html: status.content)
|
if notification.kind == .mention || notification.kind == .status,
|
||||||
|
!status.spoilerText.isEmpty {
|
||||||
|
notificationContent = "⚠️ \(status.spoilerText)"
|
||||||
|
} else {
|
||||||
|
notificationContent = NotificationService.textConverter.convert(html: status.content)
|
||||||
|
}
|
||||||
} else if notification.kind == .follow || notification.kind == .followRequest {
|
} else if notification.kind == .follow || notification.kind == .followRequest {
|
||||||
notificationContent = nil
|
notificationContent = nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -135,7 +140,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
// We deliberately don't include attachments for other types of notifications that have statuses (favs, etc.)
|
// We deliberately don't include attachments for other types of notifications that have statuses (favs, etc.)
|
||||||
// because we risk just fetching the same thing a bunch of times for many senders.
|
// because we risk just fetching the same thing a bunch of times for many senders.
|
||||||
if notification.kind == .mention || notification.kind == .status || notification.kind == .update,
|
if notification.kind == .mention || notification.kind == .status || notification.kind == .update,
|
||||||
let attachment = notification.status?.attachments.first {
|
let status = notification.status,
|
||||||
|
!status.sensitive,
|
||||||
|
let attachment = status.attachments.first {
|
||||||
let url = attachment.previewURL ?? attachment.url
|
let url = attachment.previewURL ?? attachment.url
|
||||||
attachmentDataTask = Task {
|
attachmentDataTask = Task {
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -44,6 +44,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
private(set) var scrollAndZoomEnabled = true
|
private(set) var scrollAndZoomEnabled = true
|
||||||
|
|
||||||
private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
|
private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
|
||||||
|
|
||||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||||
return !controlsVisible
|
return !controlsVisible
|
||||||
}
|
}
|
||||||
|
@ -227,6 +228,8 @@ class GalleryItemViewController: UIViewController {
|
||||||
updateZoomScale(resetZoom: true)
|
updateZoomScale(resetZoom: true)
|
||||||
}
|
}
|
||||||
centerContent()
|
centerContent()
|
||||||
|
// Ensure the transform is correct if the controls are hidden and their size changed.
|
||||||
|
setControlsVisible(controlsVisible, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -289,10 +292,12 @@ class GalleryItemViewController: UIViewController {
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
controlsVisible = visible
|
controlsVisible = visible
|
||||||
|
|
||||||
guard let topControlsView,
|
guard let topControlsView,
|
||||||
let bottomControlsView else {
|
let bottomControlsView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateControlsViews() {
|
func updateControlsViews() {
|
||||||
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
||||||
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
||||||
|
|
|
@ -125,6 +125,8 @@ extension GalleryViewController: UIPageViewControllerDataSource {
|
||||||
extension GalleryViewController: UIPageViewControllerDelegate {
|
extension GalleryViewController: UIPageViewControllerDelegate {
|
||||||
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||||
currentItemViewController.content.galleryContentWillDisappear()
|
currentItemViewController.content.galleryContentWillDisappear()
|
||||||
|
let new = pendingViewControllers[0] as! GalleryItemViewController
|
||||||
|
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||||
|
|
|
@ -235,6 +235,7 @@
|
||||||
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */; };
|
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */; };
|
||||||
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */; };
|
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */; };
|
||||||
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */; };
|
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */; };
|
||||||
|
D69F26342C4CDFD300FAF761 /* AccountDisplayAndUserNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F26332C4CDFD300FAF761 /* AccountDisplayAndUserNameLabel.swift */; };
|
||||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||||
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
|
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
|
||||||
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
|
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
|
||||||
|
@ -668,6 +669,7 @@
|
||||||
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsCollection.swift; sourceTree = "<group>"; };
|
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsCollection.swift; sourceTree = "<group>"; };
|
||||||
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReactionView.swift; sourceTree = "<group>"; };
|
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReactionView.swift; sourceTree = "<group>"; };
|
||||||
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncementContentTextView.swift; sourceTree = "<group>"; };
|
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncementContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
D69F26332C4CDFD300FAF761 /* AccountDisplayAndUserNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayAndUserNameLabel.swift; sourceTree = "<group>"; };
|
||||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||||
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
|
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
|
||||||
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
|
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1480,7 +1482,9 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */,
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */,
|
||||||
|
D69F26332C4CDFD300FAF761 /* AccountDisplayAndUserNameLabel.swift */,
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
|
@ -1514,7 +1518,6 @@
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
D64AAE8F26C80DB600FC57FB /* Toast */,
|
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||||
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2357,6 +2360,7 @@
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
||||||
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */,
|
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */,
|
||||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||||
|
D69F26342C4CDFD300FAF761 /* AccountDisplayAndUserNameLabel.swift in Sources */,
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
|
@ -3256,7 +3260,7 @@
|
||||||
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
|
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 0.2.5;
|
version = 0.3.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||||
|
|
|
@ -25,8 +25,14 @@ class HTMLConverter {
|
||||||
|
|
||||||
private let converter: AttributedStringConverter<Callbacks>
|
private let converter: AttributedStringConverter<Callbacks>
|
||||||
|
|
||||||
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
|
init(font: UIFont, monospaceFont: UIFont, fontMetrics: UIFontMetrics, color: UIColor, paragraphStyle: NSParagraphStyle) {
|
||||||
let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle)
|
let config = AttributedStringConverterConfiguration(
|
||||||
|
font: font,
|
||||||
|
monospaceFont: monospaceFont,
|
||||||
|
fontMetrics: fontMetrics,
|
||||||
|
color: color,
|
||||||
|
paragraphStyle: paragraphStyle
|
||||||
|
)
|
||||||
self.converter = AttributedStringConverter(configuration: config)
|
self.converter = AttributedStringConverter(configuration: config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -364,7 +364,9 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||||
show(conv)
|
show(conv)
|
||||||
} else {
|
} else {
|
||||||
selected(status: id, state: state.copy())
|
let conv = ConversationViewController(for: id, state: state.copy(), mastodonController: mastodonController)
|
||||||
|
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||||
|
show(conv)
|
||||||
}
|
}
|
||||||
case .expandThread(childThreads: let childThreads, inline: _):
|
case .expandThread(childThreads: let childThreads, inline: _):
|
||||||
let indexPathBeforeExpandThread = IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
let indexPathBeforeExpandThread = IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
||||||
|
|
|
@ -221,10 +221,16 @@ class ConversationViewController: UIViewController {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isLikelyMastodonRemoteStatus(url: url),
|
if isLikelyMastodonRemoteStatus(url: url) {
|
||||||
let (_, response) = try? await URLSession.appDefault.data(from: url, delegate: RedirectBlocker()),
|
var request = URLRequest(url: url)
|
||||||
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
// Mastodon uses an intermediate redirect page for browsers which requires user input that we don't want.
|
||||||
effectiveURL = location
|
request.addValue("application/activity+json", forHTTPHeaderField: "accept")
|
||||||
|
if let (_, response) = try? await URLSession.appDefault.data(for: request, delegate: RedirectBlocker()),
|
||||||
|
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
||||||
|
effectiveURL = location
|
||||||
|
} else {
|
||||||
|
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
||||||
}
|
}
|
||||||
|
@ -232,9 +238,14 @@ class ConversationViewController: UIViewController {
|
||||||
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
||||||
do {
|
do {
|
||||||
let (results, _) = try await mastodonController.run(request)
|
let (results, _) = try await mastodonController.run(request)
|
||||||
guard let status = results.statuses.compactMap(\.value).first(where: { $0.url?.serialized() == effectiveURL }) else {
|
let statuses = results.statuses.compactMap(\.value)
|
||||||
|
// Don't try to exactly match effective URL because the URL form Mastodon
|
||||||
|
// uses for the ActivityPub redirect doesn't match what's returned by the API.
|
||||||
|
// Instead we just assume that, if only one status was returned, it worked.
|
||||||
|
guard statuses.count == 1 else {
|
||||||
throw UnableToResolveError()
|
throw UnableToResolveError()
|
||||||
}
|
}
|
||||||
|
let status = statuses[0]
|
||||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||||
mode = .localID(status.id)
|
mode = .localID(status.id)
|
||||||
return status.id
|
return status.id
|
||||||
|
|
|
@ -13,7 +13,7 @@ struct CustomizeTimelinesView: View {
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CustomizeTimelinesList()
|
CustomizeTimelinesList(pinnedTimelines: mastodonController.accountPreferences.pinnedTimelines)
|
||||||
.environmentObject(mastodonController)
|
.environmentObject(mastodonController)
|
||||||
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,15 @@ struct CustomizeTimelinesList: View {
|
||||||
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var deletionError: (any Error)?
|
@State private var deletionError: (any Error)?
|
||||||
|
// store this separately from AccountPreferences in the view, b/c the @LazilyDecoding wrapper breaks animations
|
||||||
|
@State private var pinnedTimelines: [PinnedTimeline]
|
||||||
|
@State private var isShowingAddHashtagSheet = false
|
||||||
|
@State private var isShowingAddInstanceSheet = false
|
||||||
|
|
||||||
|
init(pinnedTimelines: [PinnedTimeline]) {
|
||||||
|
self.pinnedTimelines = pinnedTimelines
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
@ -50,8 +58,12 @@ struct CustomizeTimelinesList: View {
|
||||||
|
|
||||||
private var navigationBody: some View {
|
private var navigationBody: some View {
|
||||||
List {
|
List {
|
||||||
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
|
PinnedTimelinesView(
|
||||||
.appGroupedListRowBackground()
|
pinnedTimelines: $pinnedTimelines,
|
||||||
|
isShowingAddHashtagSheet: $isShowingAddHashtagSheet,
|
||||||
|
isShowingAddInstanceSheet: $isShowingAddInstanceSheet
|
||||||
|
)
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
||||||
|
@ -99,6 +111,12 @@ struct CustomizeTimelinesList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modifier(PinnedTimelinesModifier(
|
||||||
|
accountPreferences: mastodonController.accountPreferences,
|
||||||
|
pinnedTimelines: $pinnedTimelines,
|
||||||
|
isShowingAddHashtagSheet: $isShowingAddHashtagSheet,
|
||||||
|
isShowingAddInstanceSheet: $isShowingAddInstanceSheet
|
||||||
|
))
|
||||||
.alertWithData("Error Deleting Filter", data: $deletionError, actions: { _ in
|
.alertWithData("Error Deleting Filter", data: $deletionError, actions: { _ in
|
||||||
Button("OK") {
|
Button("OK") {
|
||||||
self.deletionError = nil
|
self.deletionError = nil
|
||||||
|
|
|
@ -11,17 +11,10 @@ import Pachyderm
|
||||||
|
|
||||||
struct PinnedTimelinesView: View {
|
struct PinnedTimelinesView: View {
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@ObservedObject private var accountPreferences: AccountPreferences
|
|
||||||
|
|
||||||
@State private var isShowingAddHashtagSheet = false
|
@Binding var pinnedTimelines: [PinnedTimeline]
|
||||||
@State private var isShowingAddInstanceSheet = false
|
@Binding var isShowingAddHashtagSheet: Bool
|
||||||
// store this separately from AccountPreferences in the view, b/c the @LazilyDecoding wrapper breaks animations
|
@Binding var isShowingAddInstanceSheet: Bool
|
||||||
@State private var pinnedTimelines: [PinnedTimeline]
|
|
||||||
|
|
||||||
init(accountPreferences: AccountPreferences) {
|
|
||||||
self.accountPreferences = accountPreferences
|
|
||||||
self.pinnedTimelines = accountPreferences.pinnedTimelines
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -110,42 +103,53 @@ struct PinnedTimelinesView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Pinned Timelines")
|
Text("Pinned Timelines")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
}
|
||||||
#if os(visionOS)
|
}
|
||||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
struct PinnedTimelinesModifier: ViewModifier {
|
||||||
#else
|
let accountPreferences: AccountPreferences
|
||||||
if #available(iOS 16.0, *) {
|
@Binding var pinnedTimelines: [PinnedTimeline]
|
||||||
|
@Binding var isShowingAddHashtagSheet: Bool
|
||||||
|
@Binding var isShowingAddInstanceSheet: Bool
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
||||||
|
#if os(visionOS)
|
||||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
} else {
|
#else
|
||||||
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
|
if #available(iOS 16.0, *) {
|
||||||
|
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
} else {
|
||||||
|
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
})
|
||||||
|
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
||||||
|
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
})
|
||||||
|
.onReceive(accountPreferences.publisher(for: \.pinnedTimelinesData)) { _ in
|
||||||
|
if pinnedTimelines != accountPreferences.pinnedTimelines {
|
||||||
|
pinnedTimelines = accountPreferences.pinnedTimelines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(visionOS)
|
||||||
|
.onChange(of: pinnedTimelines) {
|
||||||
|
if accountPreferences.pinnedTimelines != pinnedTimelines {
|
||||||
|
accountPreferences.pinnedTimelines = pinnedTimelines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.onChange(of: pinnedTimelines) { newValue in
|
||||||
|
if accountPreferences.pinnedTimelines != newValue {
|
||||||
|
accountPreferences.pinnedTimelines = newValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
})
|
|
||||||
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
|
||||||
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
|
||||||
})
|
|
||||||
.onReceive(accountPreferences.publisher(for: \.pinnedTimelinesData)) { _ in
|
|
||||||
if pinnedTimelines != accountPreferences.pinnedTimelines {
|
|
||||||
pinnedTimelines = accountPreferences.pinnedTimelines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if os(visionOS)
|
|
||||||
.onChange(of: pinnedTimelines) {
|
|
||||||
if accountPreferences.pinnedTimelines != pinnedTimelines {
|
|
||||||
accountPreferences.pinnedTimelines = pinnedTimelines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
.onChange(of: pinnedTimelines) { newValue in
|
|
||||||
if accountPreferences.pinnedTimelines != newValue {
|
|
||||||
accountPreferences.pinnedTimelines = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,9 @@ class VideoOverlayViewController: UIViewController {
|
||||||
if player.rate > 0 {
|
if player.rate > 0 {
|
||||||
player.rate = 0
|
player.rate = 0
|
||||||
} else {
|
} else {
|
||||||
|
if player.currentTime() >= player.currentItem!.duration {
|
||||||
|
player.seek(to: .zero)
|
||||||
|
}
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
player.play()
|
player.play()
|
||||||
#else
|
#else
|
||||||
|
|
|
@ -135,12 +135,11 @@ private struct MockStatusCardView: UIViewRepresentable {
|
||||||
func makeUIView(context: Context) -> StatusCardView {
|
func makeUIView(context: Context) -> StatusCardView {
|
||||||
let view = StatusCardView()
|
let view = StatusCardView()
|
||||||
view.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
let card = Card(
|
let card = StatusCardView.CardData(
|
||||||
url: WebURL("https://vaccor.space/tusker")!,
|
url: WebURL("https://vaccor.space/tusker")!,
|
||||||
title: "Tusker",
|
|
||||||
description: "Tusker is an iOS app for Mastodon",
|
|
||||||
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
||||||
kind: .link
|
title: "Tusker",
|
||||||
|
description: "Tusker is an iOS app for Mastodon"
|
||||||
)
|
)
|
||||||
view.updateUI(card: card, sensitive: false)
|
view.updateUI(card: card, sensitive: false)
|
||||||
return view
|
return view
|
||||||
|
|
|
@ -9,12 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
import OSLog
|
|
||||||
#if canImport(Sentry)
|
|
||||||
import Sentry
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ProfileStatusesViewController")
|
|
||||||
|
|
||||||
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||||
|
|
||||||
|
@ -255,20 +249,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
|
|
||||||
state = .setupInitialSnapshot
|
state = .setupInitialSnapshot
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let (all, _) = try await mastodonController.run(Client.getRelationships(accounts: [accountID]))
|
|
||||||
if let relationship = all.first {
|
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logger.error("Error fetching relationship: \(String(describing: error))")
|
|
||||||
#if canImport(Sentry)
|
|
||||||
SentrySDK.capture(error: error)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await controller.loadInitial()
|
await controller.loadInitial()
|
||||||
await tryLoadPinned()
|
await tryLoadPinned()
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,12 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
import OSLog
|
||||||
|
#if canImport(Sentry)
|
||||||
|
import Sentry
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ProfileViewController")
|
||||||
|
|
||||||
class ProfileViewController: UIViewController, StateRestorableViewController {
|
class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
|
|
||||||
|
@ -120,6 +126,19 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
guard let accountID else {
|
guard let accountID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let (all, _) = try await mastodonController.run(Client.getRelationships(accounts: [accountID]))
|
||||||
|
if let relationship = all.first {
|
||||||
|
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Error fetching relationship: \(String(describing: error))")
|
||||||
|
#if canImport(Sentry)
|
||||||
|
SentrySDK.capture(error: error)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
if let account = mastodonController.persistentContainer.account(for: accountID) {
|
if let account = mastodonController.persistentContainer.account(for: accountID) {
|
||||||
updateAccountUI(account: account)
|
updateAccountUI(account: account)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import SwiftUI
|
||||||
private var converter = HTMLConverter(
|
private var converter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
fontMetrics: .default,
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: .default
|
paragraphStyle: .default
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// AccountDisplayAndUserNameLabel.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 7/20/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class AccountDisplayAndUserNameLabel: EmojiLabel {
|
||||||
|
var baseFont: UIFontDescriptor = .preferredFontDescriptor(withTextStyle: .body)
|
||||||
|
|
||||||
|
private var state: State?
|
||||||
|
|
||||||
|
func updateUI(account: some AccountProtocol) {
|
||||||
|
let state = State(accountID: account.id, displayName: account.displayName, acct: account.acct)
|
||||||
|
guard state != self.state || Preferences.shared.hideCustomEmojiInUsernames != hasEmojis else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.state = state
|
||||||
|
self.attributedText = makeAttributedText(state: state)
|
||||||
|
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||||
|
self.removeEmojis()
|
||||||
|
} else {
|
||||||
|
self.setEmojis(account.emojis, identifier: state.accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeAttributedText(state: State) -> NSAttributedString {
|
||||||
|
let s = NSMutableAttributedString()
|
||||||
|
s.append(NSAttributedString(string: state.displayName, attributes: [
|
||||||
|
.font: UIFont(descriptor: baseFont.addingAttributes([
|
||||||
|
.traits: [
|
||||||
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
||||||
|
]
|
||||||
|
]), size: 0),
|
||||||
|
]))
|
||||||
|
s.append(NSAttributedString(string: " "))
|
||||||
|
s.append(NSAttributedString(string: "@\(state.acct)", attributes: [
|
||||||
|
.font: UIFont(descriptor: baseFont.addingAttributes([
|
||||||
|
.traits: [
|
||||||
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue,
|
||||||
|
]
|
||||||
|
]), size: 0),
|
||||||
|
.foregroundColor: UIColor.secondaryLabel,
|
||||||
|
]))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct State: Equatable {
|
||||||
|
var accountID: String
|
||||||
|
var displayName: String
|
||||||
|
var acct: String
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ class AccountDisplayNameLabel: EmojiLabel {
|
||||||
private var accountDisplayName: String?
|
private var accountDisplayName: String?
|
||||||
|
|
||||||
func updateForAccountDisplayName(account: some AccountProtocol) {
|
func updateForAccountDisplayName(account: some AccountProtocol) {
|
||||||
guard accountID != account.id || accountDisplayName != account.displayName || Preferences.shared.hideCustomEmojiInUsernames == hasEmojis else {
|
guard accountID != account.id || accountDisplayName != account.displayName || Preferences.shared.hideCustomEmojiInUsernames != hasEmojis else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accountID = account.id
|
accountID = account.id
|
||||||
|
|
|
@ -13,6 +13,7 @@ class ConfirmReblogStatusPreviewView: UIView {
|
||||||
private static let htmlConverter = HTMLConverter(
|
private static let htmlConverter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .caption2),
|
font: .preferredFont(forTextStyle: .caption2),
|
||||||
monospaceFont: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
monospaceFont: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
fontMetrics: UIFontMetrics(forTextStyle: .caption2),
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: .default
|
paragraphStyle: .default
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
private static let defaultBodyHTMLConverter = HTMLConverter(
|
private static let defaultBodyHTMLConverter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
fontMetrics: .default,
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: .default
|
paragraphStyle: .default
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ class ProfileFieldValueView: UIView {
|
||||||
private static let converter = HTMLConverter(
|
private static let converter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
fontMetrics: .default,
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: .default
|
paragraphStyle: .default
|
||||||
)
|
)
|
||||||
|
@ -54,8 +55,8 @@ class ProfileFieldValueView: UIView {
|
||||||
textView.isScrollEnabled = false
|
textView.isScrollEnabled = false
|
||||||
textView.isSelectable = false
|
textView.isSelectable = false
|
||||||
textView.isEditable = false
|
textView.isEditable = false
|
||||||
textView.textContainerInset = .zero
|
|
||||||
textView.font = .preferredFont(forTextStyle: .body)
|
textView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
updateTextContainerInset()
|
||||||
textView.adjustsFontForContentSizeCategory = true
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
textView.attributedText = converted
|
textView.attributedText = converted
|
||||||
textView.setEmojis(account.emojis, identifier: account.id)
|
textView.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
@ -108,6 +109,27 @@ class ProfileFieldValueView: UIView {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||||
|
updateTextContainerInset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTextContainerInset() {
|
||||||
|
// blergh
|
||||||
|
switch traitCollection.preferredContentSizeCategory {
|
||||||
|
case .extraSmall:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0)
|
||||||
|
case .small:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0)
|
||||||
|
case .medium, .large:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
|
||||||
|
default:
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
func setTextAlignment(_ alignment: NSTextAlignment) {
|
||||||
textView.textAlignment = alignment
|
textView.textAlignment = alignment
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ class ProfileHeaderMovedOverlayView: UIView {
|
||||||
weak var delegate: TuskerNavigationDelegate?
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
|
|
||||||
var collapse: (() -> Void)?
|
var collapse: (() -> Void)?
|
||||||
|
var hide: (() -> Void)?
|
||||||
|
|
||||||
private var avatarImageView: CachedImageView!
|
private var avatarImageView: CachedImageView!
|
||||||
private var displayNameLabel: EmojiLabel!
|
private var displayNameLabel: EmojiLabel!
|
||||||
private var usernameLabel: UILabel!
|
private var usernameLabel: UILabel!
|
||||||
|
@ -144,7 +145,46 @@ class ProfileHeaderMovedOverlayView: UIView {
|
||||||
@objc private func accountTapped() {
|
@objc private func accountTapped() {
|
||||||
delegate?.selected(account: movedToID)
|
delegate?.selected(account: movedToID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Accessibility
|
||||||
|
|
||||||
|
override var isAccessibilityElement: Bool {
|
||||||
|
get { true }
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var accessibilityLabel: String? {
|
||||||
|
get {
|
||||||
|
guard let movedToID,
|
||||||
|
let account = delegate?.apiController?.persistentContainer.account(for: movedToID) else {
|
||||||
|
return "This account has moved"
|
||||||
|
}
|
||||||
|
return "This account has moved to @\(account.acct)"
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func accessibilityActivate() -> Bool {
|
||||||
|
guard let movedToID,
|
||||||
|
let delegate else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
delegate.selected(account: movedToID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
|
||||||
|
get {
|
||||||
|
[
|
||||||
|
UIAccessibilityCustomAction(name: "Hide banner", actionHandler: { [unowned self] _ in
|
||||||
|
self.hide?()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderMovedOverlayView: UIPointerInteractionDelegate {
|
extension ProfileHeaderMovedOverlayView: UIPointerInteractionDelegate {
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ProfileHeaderView: UIView {
|
||||||
@IBOutlet weak var followersCountButton: UIButton!
|
@IBOutlet weak var followersCountButton: UIButton!
|
||||||
private(set) var pagesSegmentedControl: ScrollingSegmentedControl<ProfileViewController.Page>!
|
private(set) var pagesSegmentedControl: ScrollingSegmentedControl<ProfileViewController.Page>!
|
||||||
private var movedOverlayView: ProfileHeaderMovedOverlayView?
|
private var movedOverlayView: ProfileHeaderMovedOverlayView?
|
||||||
|
private var hideMovedOverlayView = false
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
|
@ -178,7 +179,8 @@ class ProfileHeaderView: UIView {
|
||||||
followersCountButton.setAttributedTitle(followersCountTitle, for: .normal)
|
followersCountButton.setAttributedTitle(followersCountTitle, for: .normal)
|
||||||
followersCountButton.accessibilityLabel = "\(followersSpelledOut) followers"
|
followersCountButton.accessibilityLabel = "\(followersSpelledOut) followers"
|
||||||
|
|
||||||
if let movedTo = account.movedTo {
|
if let movedTo = account.movedTo,
|
||||||
|
!hideMovedOverlayView {
|
||||||
if let movedOverlayView {
|
if let movedOverlayView {
|
||||||
movedOverlayView.updateUI(movedTo: movedTo)
|
movedOverlayView.updateUI(movedTo: movedTo)
|
||||||
} else {
|
} else {
|
||||||
|
@ -207,6 +209,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
private func createMovedOverlayView(movedTo: AccountMO) -> ProfileHeaderMovedOverlayView {
|
private func createMovedOverlayView(movedTo: AccountMO) -> ProfileHeaderMovedOverlayView {
|
||||||
let overlay = ProfileHeaderMovedOverlayView()
|
let overlay = ProfileHeaderMovedOverlayView()
|
||||||
|
overlay.layer.zPosition = 1000
|
||||||
overlay.delegate = delegate
|
overlay.delegate = delegate
|
||||||
overlay.updateUI(movedTo: movedTo)
|
overlay.updateUI(movedTo: movedTo)
|
||||||
overlay.translatesAutoresizingMaskIntoConstraints = false
|
overlay.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -234,6 +237,12 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
animator.startAnimation()
|
animator.startAnimation()
|
||||||
}
|
}
|
||||||
|
overlay.hide = { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.hideMovedOverlayView = true
|
||||||
|
self.updateUI(for: self.accountID)
|
||||||
|
UIAccessibility.post(notification: .layoutChanged, argument: self)
|
||||||
|
}
|
||||||
|
|
||||||
return overlay
|
return overlay
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
private static let htmlConverter = HTMLConverter(
|
private static let htmlConverter = HTMLConverter(
|
||||||
font: ConversationMainStatusCollectionViewCell.contentFont,
|
font: ConversationMainStatusCollectionViewCell.contentFont,
|
||||||
monospaceFont: ConversationMainStatusCollectionViewCell.monospaceFont,
|
monospaceFont: ConversationMainStatusCollectionViewCell.monospaceFont,
|
||||||
|
fontMetrics: .default,
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
paragraphStyle: ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
||||||
)
|
)
|
||||||
|
@ -514,8 +515,15 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAccountUI(account: AccountMO) {
|
||||||
|
baseUpdateAccountUI(account: account)
|
||||||
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
}
|
||||||
|
|
||||||
func updateUIForPreferences(status: StatusMO) {
|
func updateUIForPreferences(status: StatusMO) {
|
||||||
baseUpdateUIForPreferences(status: status)
|
baseUpdateUIForPreferences(status: status)
|
||||||
|
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import WebURL
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ class StatusCardView: UIView {
|
||||||
weak var actionProvider: MenuActionProvider?
|
weak var actionProvider: MenuActionProvider?
|
||||||
|
|
||||||
private var statusID: String?
|
private var statusID: String?
|
||||||
private(set) var card: Card?
|
private(set) var card: CardData?
|
||||||
|
|
||||||
private static let activeBackgroundColor = UIColor.secondarySystemFill
|
private static let activeBackgroundColor = UIColor.secondarySystemFill
|
||||||
private static let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
private static let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
||||||
|
@ -163,20 +164,22 @@ class StatusCardView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
func updateUI(status: StatusMO) {
|
||||||
guard status.id != statusID else {
|
let newData = status.card.map { CardData(card: $0) }
|
||||||
|
guard self.card != newData else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.card = status.card
|
self.card = newData
|
||||||
self.statusID = status.id
|
self.statusID = status.id
|
||||||
|
|
||||||
guard let card = status.card else {
|
guard let newData else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI(card: card, sensitive: status.sensitive)
|
updateUI(card: newData, sensitive: status.sensitive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(card: Card, sensitive: Bool) {
|
// This method is internal for use by MockStatusView
|
||||||
|
func updateUI(card: CardData, sensitive: Bool) {
|
||||||
if let image = card.image {
|
if let image = card.image {
|
||||||
if sensitive {
|
if sensitive {
|
||||||
if let blurhash = card.blurhash {
|
if let blurhash = card.blurhash {
|
||||||
|
@ -243,6 +246,30 @@ class StatusCardView: UIView {
|
||||||
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CardData: Equatable {
|
||||||
|
let url: WebURL
|
||||||
|
let image: WebURL?
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
let blurhash: String?
|
||||||
|
|
||||||
|
init(card: Card) {
|
||||||
|
self.url = card.url
|
||||||
|
self.image = card.image
|
||||||
|
self.title = card.title
|
||||||
|
self.description = card.description
|
||||||
|
self.blurhash = card.blurhash
|
||||||
|
}
|
||||||
|
|
||||||
|
init(url: WebURL, image: WebURL? = nil, title: String, description: String, blurhash: String? = nil) {
|
||||||
|
self.url = url
|
||||||
|
self.image = image
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.blurhash = blurhash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,6 @@ protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate,
|
||||||
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
|
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
|
||||||
// MARK: Subviews
|
// MARK: Subviews
|
||||||
var avatarImageView: CachedImageView { get }
|
var avatarImageView: CachedImageView { get }
|
||||||
var displayNameLabel: AccountDisplayNameLabel { get }
|
|
||||||
var usernameLabel: UILabel { get }
|
|
||||||
var contentWarningLabel: EmojiLabel { get }
|
var contentWarningLabel: EmojiLabel { get }
|
||||||
var collapseButton: StatusCollapseButton { get }
|
var collapseButton: StatusCollapseButton { get }
|
||||||
var contentContainer: StatusContentContainer { get }
|
var contentContainer: StatusContentContainer { get }
|
||||||
|
@ -49,6 +47,7 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
||||||
var isGrayscale: Bool { get set }
|
var isGrayscale: Bool { get set }
|
||||||
var cancellables: Set<AnyCancellable> { get set }
|
var cancellables: Set<AnyCancellable> { get set }
|
||||||
|
|
||||||
|
func updateAccountUI(account: AccountMO)
|
||||||
func updateAttachmentsUI(status: StatusMO)
|
func updateAttachmentsUI(status: StatusMO)
|
||||||
func updateUIForPreferences(status: StatusMO)
|
func updateUIForPreferences(status: StatusMO)
|
||||||
func updateStatusState(status: StatusMO)
|
func updateStatusState(status: StatusMO)
|
||||||
|
@ -177,10 +176,8 @@ extension StatusCollectionViewCell {
|
||||||
attachmentsView.updateUI(attachments: status.attachments)
|
attachmentsView.updateUI(attachments: status.attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAccountUI(account: AccountMO) {
|
func baseUpdateAccountUI(account: AccountMO) {
|
||||||
avatarImageView.update(for: account.avatar)
|
avatarImageView.update(for: account.avatar)
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
|
||||||
usernameLabel.text = "@\(account.acct)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseUpdateUIForPreferences(status: StatusMO) {
|
func baseUpdateUIForPreferences(status: StatusMO) {
|
||||||
|
@ -219,7 +216,6 @@ extension StatusCollectionViewCell {
|
||||||
if contentTextView.hasEmojis {
|
if contentTextView.hasEmojis {
|
||||||
contentTextView.setEmojis(status.emojis, identifier: status.id)
|
contentTextView.setEmojis(status.emojis, identifier: status.id)
|
||||||
}
|
}
|
||||||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseUpdateStatusState(status: StatusMO) {
|
func baseUpdateStatusState(status: StatusMO) {
|
||||||
|
|
|
@ -70,8 +70,7 @@ class StatusMetaIndicatorsView: UIView {
|
||||||
|
|
||||||
private func configureImageView(_ imageView: UIImageView) {
|
private func configureImageView(_ imageView: UIImageView) {
|
||||||
let weight: UIImage.SymbolWeight = UIAccessibility.isBoldTextEnabled ? .regular : traitCollection.preferredContentSizeCategory > .large ? .light : .thin
|
let weight: UIImage.SymbolWeight = UIAccessibility.isBoldTextEnabled ? .regular : traitCollection.preferredContentSizeCategory > .large ? .light : .thin
|
||||||
let scale: UIImage.SymbolScale = traitCollection.preferredContentSizeCategory > .extraLarge ? .large : .default
|
imageView.preferredSymbolConfiguration = .init(pointSize: 0, weight: weight, scale: .default)
|
||||||
imageView.preferredSymbolConfiguration = .init(pointSize: 0, weight: weight, scale: scale)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
func updateUI(status: StatusMO) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
static let htmlConverter = HTMLConverter(
|
static let htmlConverter = HTMLConverter(
|
||||||
font: TimelineStatusCollectionViewCell.contentFont,
|
font: TimelineStatusCollectionViewCell.contentFont,
|
||||||
monospaceFont: TimelineStatusCollectionViewCell.monospaceFont,
|
monospaceFont: TimelineStatusCollectionViewCell.monospaceFont,
|
||||||
|
fontMetrics: .default,
|
||||||
color: .label,
|
color: .label,
|
||||||
paragraphStyle: TimelineStatusCollectionViewCell.contentParagraphStyle
|
paragraphStyle: TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||||
)
|
)
|
||||||
|
@ -121,8 +122,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var nameHStack = UIStackView(arrangedSubviews: [
|
private lazy var nameHStack = UIStackView(arrangedSubviews: [
|
||||||
displayNameLabel,
|
displayAndUserNameLabel,
|
||||||
usernameLabel,
|
|
||||||
pinImageView,
|
pinImageView,
|
||||||
timestampLabel,
|
timestampLabel,
|
||||||
]).configure {
|
]).configure {
|
||||||
|
@ -130,27 +130,10 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.spacing = 4
|
$0.spacing = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayNameLabel = AccountDisplayNameLabel().configure {
|
let displayAndUserNameLabel = AccountDisplayAndUserNameLabel().configure {
|
||||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
|
||||||
.traits: [
|
|
||||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
|
||||||
]
|
|
||||||
]), size: 0)
|
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
|
||||||
$0.setContentHuggingPriority(.init(251), for: .horizontal)
|
|
||||||
$0.setContentCompressionResistancePriority(.init(749), for: .horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
let usernameLabel = UILabel().configure {
|
|
||||||
$0.textColor = .secondaryLabel
|
|
||||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
|
||||||
.traits: [
|
|
||||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue,
|
|
||||||
]
|
|
||||||
]), size: 0)
|
|
||||||
$0.adjustsFontForContentSizeCategory = true
|
$0.adjustsFontForContentSizeCategory = true
|
||||||
$0.setContentHuggingPriority(.init(249), for: .horizontal)
|
$0.setContentHuggingPriority(.init(249), for: .horizontal)
|
||||||
$0.setContentCompressionResistancePriority(.init(748), for: .horizontal)
|
$0.setContentCompressionResistancePriority(.init(749), for: .horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let pinImageView = UIImageView(image: UIImage(systemName: "pin.fill")).configure {
|
private let pinImageView = UIImageView(image: UIImage(systemName: "pin.fill")).configure {
|
||||||
|
@ -693,6 +676,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAccountUI(account: AccountMO) {
|
||||||
|
baseUpdateAccountUI(account: account)
|
||||||
|
displayAndUserNameLabel.updateUI(account: account)
|
||||||
|
}
|
||||||
|
|
||||||
func updateUIForPreferences(status: StatusMO) {
|
func updateUIForPreferences(status: StatusMO) {
|
||||||
baseUpdateUIForPreferences(status: status)
|
baseUpdateUIForPreferences(status: status)
|
||||||
|
|
||||||
|
@ -704,6 +692,8 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
metaIndicatorsView.updateUI(status: status)
|
metaIndicatorsView.updateUI(status: status)
|
||||||
|
|
||||||
timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
|
timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
|
||||||
|
|
||||||
|
displayAndUserNameLabel.updateUI(account: status.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStatusState(status: StatusMO) {
|
func updateStatusState(status: StatusMO) {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2024.3
|
MARKETING_VERSION = 2024.3
|
||||||
CURRENT_PROJECT_VERSION = 128
|
CURRENT_PROJECT_VERSION = 129
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
|
Loading…
Reference in New Issue