Compare commits

...

9 Commits

Author SHA1 Message Date
Shadowfacts 18f6445a7c Bump build number and update changelog 2024-07-30 22:28:44 -07:00
Shadowfacts c5f42719a0 Fix Cmd+3 not properly selecting Explore tab
Having MainSidebarViewController.Item.explore and .tab(.explore) was a
mistake and made it easy to accidentally use the wrong one for the key
command, so use .tab(.explore) for everything.

Closes #519
2024-07-30 22:03:36 -07:00
Shadowfacts eb89aec00f Bump build number and update changelog 2024-07-24 21:09:42 -07:00
Shadowfacts 61576bce58 Fix Drafts button never turning into Post on Mac Catalyst
Closes #504
2024-07-24 20:57:23 -07:00
Shadowfacts f7d4737782 Add more details to notification loading crash 2024-07-24 20:48:01 -07:00
Shadowfacts 3dd0f3a154 Report DraftsPersistentContainer initializer errors to Sentry 2024-07-24 20:42:35 -07:00
Shadowfacts 145ffbfcf0 Fix crash when selection changes to nil in custom alert
Closes #517
2024-07-24 20:33:17 -07:00
Shadowfacts bcf2a2f026 Improve compose reply view avatar scrolling animation 2024-07-24 20:26:33 -07:00
Shadowfacts 1358152dec Fix discrepancy between SearchResultsViewController.Item == and hash 2024-07-22 22:19:31 -07:00
14 changed files with 148 additions and 52 deletions

View File

@ -1,3 +1,34 @@
## 2024.3
This update includes a number of bugfixes and performance improvements. See below for a list of fixes.
Bugfixes:
- Fix an issue displaying rich text in certain cases
- Fix crash when video attachment finishes playing
- Fix video attachment thumbnails being flipped on Compose screen
- Fix profile header images being blurry
- Fix crash when opening push notifications in certain circumstances
- Fix certain links in profile fields not being tappable
- Fix gifv playback pausing audio from other apps
- Fix gifv playback being paused when returning from background
- Fix badges on gifv attachments not appearing
- Fix excessive network traffic when opening profile pages
- Fix controls visibility not matching across attachment gallery pages
- Fix add hashtag/instance pinned timeline sheet in Customize Timelines dismissing instantly
- Fix Dynamic Type not applying to status content
- Fix mention/status push notifications not showing CW
- Fix sensitive attachment thumbnails being shown in push notifications
- Fix profile moved overlay visual and VoiceOver issues
- Fix opening Mastodon remote status links
- Fix reply author avatar on Compose screen not being pinned to top when scrolling while typing
- Pleroma/Akkoma: Fix editing attachment descriptions not working
- Pixelfed/Firefish: Fix error loading certain accounts
- Pixelfed: Fix error loading relationships and follow/block/etc. actions
- iPadOS: Fix pointer interactions throughout the app
- iPadOS: Fix multiple close buttons being added in multi-column interface
- iPadOS: Fix Cmd+1/etc. removing columns when returning to previous tab
- iPadOS: Fix multi-column interface not animating for some actions
- iPadOS: Fix selecting search results always adding new column
## 2024.2 ## 2024.2
This release introduces push notifications as well as an enhanced multi-column interface on iPadOS! This release introduces push notifications as well as an enhanced multi-column interface on iPadOS!

View File

@ -1,5 +1,16 @@
# Changelog # Changelog
## 2024.3 (131)
Bugfixes:
- Fix Cmd+3 not correctly switching to Explore tab
## 2024.3 (130)
Bugfixes:
- Fix reply author avatar on Compose screen not being pinned to top when scrolling while typing
- Fix crash when dragging between buttons in reblog confirmation alert
- Fix potential crash when displaying search results
- Mac: Fix Post button not displaying on Compose screen
## 2024.3 (129) ## 2024.3 (129)
Bugfixes: Bugfixes:
- Fix excessive network traffic on profile pages - Fix excessive network traffic on profile pages

View File

@ -333,7 +333,12 @@ public final class ComposeController: ViewController {
} }
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { cancelButton } ToolbarItem(placement: .cancellationAction) { cancelButton }
#if targetEnvironment(macCatalyst)
ToolbarItem(placement: .topBarTrailing) { draftsButton }
ToolbarItem(placement: .confirmationAction) { postButton } ToolbarItem(placement: .confirmationAction) { postButton }
#else
ToolbarItem(placement: .confirmationAction) { postOrDraftsButton }
#endif
#if os(visionOS) #if os(visionOS)
ToolbarItem(placement: .bottomOrnament) { ToolbarItem(placement: .bottomOrnament) {
ControllerView(controller: { controller.toolbarController }) ControllerView(controller: { controller.toolbarController })
@ -461,18 +466,26 @@ public final class ComposeController: ViewController {
} }
@ViewBuilder @ViewBuilder
private var postButton: some View { private var postOrDraftsButton: some View {
if draft.hasContent || draft.editedStatusID != nil || !controller.config.allowSwitchingDrafts { if draft.hasContent || draft.editedStatusID != nil || !controller.config.allowSwitchingDrafts {
postButton
} else {
draftsButton
}
}
private var draftsButton: some View {
Button(action: controller.showDrafts) {
Text("Drafts")
}
}
private var postButton: some View {
Button(action: controller.postStatus) { Button(action: controller.postStatus) {
Text(draft.editedStatusID == nil ? "Post" : "Edit") Text(draft.editedStatusID == nil ? "Post" : "Edit")
} }
.keyboardShortcut(.return, modifiers: .command) .keyboardShortcut(.return, modifiers: .command)
.disabled(!controller.postButtonEnabled) .disabled(!controller.postButtonEnabled)
} else {
Button(action: controller.showDrafts) {
Text("Drafts")
}
}
} }
#if !os(visionOS) #if !os(visionOS)

View File

@ -16,6 +16,8 @@ public class DraftsPersistentContainer: NSPersistentContainer {
public static let shared = DraftsPersistentContainer() public static let shared = DraftsPersistentContainer()
public static var captureError: ((any Error) -> Void)?
private static let managedObjectModel: NSManagedObjectModel = { private static let managedObjectModel: NSManagedObjectModel = {
let url = Bundle.module.url(forResource: "Drafts", withExtension: "momd")! let url = Bundle.module.url(forResource: "Drafts", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: url)! return NSManagedObjectModel(contentsOf: url)!
@ -39,6 +41,7 @@ public class DraftsPersistentContainer: NSPersistentContainer {
loadPersistentStores { _, error in loadPersistentStores { _, error in
if let error { if let error {
DraftsPersistentContainer.captureError?(error)
fatalError("Loading persistent store: \(error)") fatalError("Loading persistent store: \(error)")
} }
} }

View File

@ -39,6 +39,7 @@ extension TextViewCaretScrolling {
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) { let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) {
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false) scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
scrollView.layoutIfNeeded()
} }
self.caretScrollPositionAnimator = animator self.caretScrollPositionAnimator = animator
animator.startAnimation() animator.startAnimation()

View File

@ -76,13 +76,15 @@ struct ReplyStatusView: View {
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content // once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
offset = min(offset, maxOffset) offset = min(offset, maxOffset)
return AvatarImageView( return AvatarContainerRepresentable(offset: offset) {
AvatarImageView(
url: status.account.avatar, url: status.account.avatar,
size: 50, size: 50,
style: controller.config.avatarStyle, style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar fetchAvatar: controller.fetchAvatar
) )
.offset(x: 0, y: offset) }
.frame(width: 50, height: 50)
.accessibilityHidden(true) .accessibilityHidden(true)
} }
@ -94,3 +96,39 @@ private struct DisplayNameHeightPrefKey: PreferenceKey {
value = nextValue() value = nextValue()
} }
} }
// This whole dance is necessary so that the offset can be animatable from
// UIKit animations, like TextViewCaretScrolling.
private struct AvatarContainerRepresentable<Content: View>: UIViewControllerRepresentable {
let offset: CGFloat
@ViewBuilder let content: Content
func makeUIViewController(context: Context) -> Controller {
Controller(host: UIHostingController(rootView: content))
}
func updateUIViewController(_ uiViewController: Controller, context: Context) {
uiViewController.host.rootView = content
uiViewController.host.view.transform = CGAffineTransform(translationX: 0, y: offset)
}
// This extra layer is necessary because applying a transform to the
// representable's VC's view doesn't seem to have an effect.
class Controller: UIViewController {
let host: UIHostingController<Content>
init(host: UIHostingController<Content>) {
self.host = host
super.init(nibName: nil, bundle: nil)
addChild(host)
host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(host.view)
host.view.frame = view.bounds
host.didMove(toParent: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}

View File

@ -56,6 +56,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// make sure the persistent container is initialized on the main thread // make sure the persistent container is initialized on the main thread
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere // otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
#if canImport(Sentry)
DraftsPersistentContainer.captureError = { SentrySDK.capture(error: $0) }
#endif
_ = DraftsPersistentContainer.shared _ = DraftsPersistentContainer.shared
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {

View File

@ -32,7 +32,7 @@ struct MenuController {
static let sidebarItemKeyCommands: [UIKeyCommand] = [ static let sidebarItemKeyCommands: [UIKeyCommand] = [
sidebarCommand(item: .tab(.timelines), command: "1", action: #selector(MainSplitViewController.handleSidebarCommandTimelines)), sidebarCommand(item: .tab(.timelines), command: "1", action: #selector(MainSplitViewController.handleSidebarCommandTimelines)),
sidebarCommand(item: .tab(.notifications), command: "2", action: #selector(MainSplitViewController.handleSidebarCommandNotifications)), sidebarCommand(item: .tab(.notifications), command: "2", action: #selector(MainSplitViewController.handleSidebarCommandNotifications)),
sidebarCommand(item: .explore, command: "3", action: #selector(MainSplitViewController.handleSidebarCommandExplore)), sidebarCommand(item: .tab(.explore), command: "3", action: #selector(MainSplitViewController.handleSidebarCommandExplore)),
sidebarCommand(item: .bookmarks, command: "4", action: #selector(MainSplitViewController.handleSidebarCommandBookmarks)), sidebarCommand(item: .bookmarks, command: "4", action: #selector(MainSplitViewController.handleSidebarCommandBookmarks)),
sidebarCommand(item: .tab(.myProfile), command: "5", action: #selector(MainSplitViewController.handleSidebarCommandMyProfile)), sidebarCommand(item: .tab(.myProfile), command: "5", action: #selector(MainSplitViewController.handleSidebarCommandMyProfile)),
] ]

View File

@ -43,7 +43,7 @@ class MainSidebarViewController: UIViewController {
} }
var exploreTabItems: [Item] { var exploreTabItems: [Item] {
var items: [Item] = [.explore, .bookmarks, .favorites] var items: [Item] = [.tab(.explore), .bookmarks, .favorites]
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) { for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
items.append(.list(list)) items.append(.list(list))
@ -170,7 +170,7 @@ class MainSidebarViewController: UIViewController {
snapshot.appendItems([ snapshot.appendItems([
.tab(.timelines), .tab(.timelines),
.tab(.notifications), .tab(.notifications),
.explore, .tab(.explore),
.bookmarks, .bookmarks,
.favorites, .favorites,
.tab(.myProfile) .tab(.myProfile)
@ -302,7 +302,7 @@ class MainSidebarViewController: UIViewController {
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id) return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id)
case .tab(.compose): case .tab(.compose):
return UserActivityManager.newPostActivity(accountID: id) return UserActivityManager.newPostActivity(accountID: id)
case .explore: case .tab(.explore):
return UserActivityManager.searchActivity(query: nil, accountID: id) return UserActivityManager.searchActivity(query: nil, accountID: id)
case .bookmarks: case .bookmarks:
return UserActivityManager.bookmarksActivity(accountID: id) return UserActivityManager.bookmarksActivity(accountID: id)
@ -340,7 +340,7 @@ extension MainSidebarViewController {
} }
enum Item: Hashable { enum Item: Hashable {
case tab(MainTabBarViewController.Tab) case tab(MainTabBarViewController.Tab)
case explore, bookmarks, favorites case bookmarks, favorites
case listsHeader, list(List), addList case listsHeader, list(List), addList
case savedHashtagsHeader, savedHashtag(String), addSavedHashtag case savedHashtagsHeader, savedHashtag(String), addSavedHashtag
case savedInstancesHeader, savedInstance(URL), addSavedInstance case savedInstancesHeader, savedInstance(URL), addSavedInstance
@ -349,8 +349,6 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.title return tab.title
case .explore:
return "Explore"
case .bookmarks: case .bookmarks:
return "Bookmarks" return "Bookmarks"
case .favorites: case .favorites:
@ -380,8 +378,6 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.imageName return tab.imageName
case .explore:
return "magnifyingglass"
case .bookmarks: case .bookmarks:
return "bookmark" return "bookmark"
case .favorites: case .favorites:

View File

@ -279,7 +279,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
$0.1 > $1.1 $0.1 > $1.1
} }
if let mostRecentExploreItem = mostRecentExploreItem?.0, if let mostRecentExploreItem = mostRecentExploreItem?.0,
mostRecentExploreItem != .explore { mostRecentExploreItem != .tab(.explore) {
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
// Pop back to root, so we're appending to the Explore VC instead of some other VC // Pop back to root, so we're appending to the Explore VC instead of some other VC
exploreNav.popToRootViewController(animated: false) exploreNav.popToRootViewController(animated: false)
@ -292,11 +292,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case nil: case nil:
break break
case let .tab(tab): case .tab(.explore):
// sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false)
case .explore:
// Search sidebar item maps to the Explore tab with the search controller/results visible // Search sidebar item maps to the Explore tab with the search controller/results visible
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController // The nav stack can't be copied directly, since the split VC uses a different SearchViewController
// so that explore items aren't shown multiple times. // so that explore items aren't shown multiple times.
@ -335,10 +331,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
} }
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .explore, to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false)
case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false)
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false)
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
@ -396,9 +396,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to. // For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
// Search screen has special considerations, all others can be transferred directly. // Search screen has special considerations, all others can be transferred directly.
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) { if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
exploreItem = .explore exploreItem = .tab(.explore)
// reuse the existing VC, if there is one // reuse the existing VC, if there is one
let searchVC = getOrCreateNavigationStack(item: .explore).first! as! InlineTrendsViewController let searchVC = getOrCreateNavigationStack(item: .tab(.explore)).first! as! InlineTrendsViewController
// load the view so that the search controller is accessible // load the view so that the search controller is accessible
searchVC.loadViewIfNeeded() searchVC.loadViewIfNeeded()
let explore = tabNavigationStack.first as! ExploreViewController let explore = tabNavigationStack.first as! ExploreViewController
@ -426,16 +426,16 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case let instanceVC as InstanceTimelineViewController: case let instanceVC as InstanceTimelineViewController:
exploreItem = .savedInstance(instanceVC.instanceURL) exploreItem = .savedInstance(instanceVC.instanceURL)
case is TrendsViewController: case is TrendsViewController:
exploreItem = .explore exploreItem = .tab(.explore)
// skip transferring the ExploreViewController and TrendsViewController // skip transferring the ExploreViewController and TrendsViewController
skipFirst = 2 skipFirst = 2
// prepend the InlineTrendsViewController // prepend the InlineTrendsViewController
toPrepend = getOrCreateNavigationStack(item: .explore).first! toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
default: default:
// transfer the navigation stack prepending, the existing explore VC // transfer the navigation stack prepending, the existing explore VC
// if there was other stuff on the explore stack, it will get discarded // if there was other stuff on the explore stack, it will get discarded
toPrepend = getOrCreateNavigationStack(item: .explore).first! toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
exploreItem = .explore exploreItem = .tab(.explore)
} }
} }
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend) transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
@ -502,10 +502,10 @@ fileprivate extension MainSidebarViewController.Item {
@MainActor @MainActor
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? { func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
switch self { switch self {
case .tab(.explore):
return InlineTrendsViewController(mastodonController: mastodonController)
case let .tab(tab): case let .tab(tab):
return tab.createViewController(mastodonController) return tab.createViewController(mastodonController)
case .explore:
return InlineTrendsViewController(mastodonController: mastodonController)
case .bookmarks: case .bookmarks:
return BookmarksViewController(mastodonController: mastodonController) return BookmarksViewController(mastodonController: mastodonController)
case .favorites: case .favorites:
@ -562,7 +562,7 @@ extension MainSplitViewController: TuskerRootViewController {
case .myProfile: case .myProfile:
item = .tab(.myProfile) item = .tab(.myProfile)
case .explore: case .explore:
item = .explore item = .tab(.explore)
case .bookmarks: case .bookmarks:
item = .bookmarks item = .bookmarks
case .list(id: let id): case .list(id: let id):
@ -616,8 +616,8 @@ extension MainSplitViewController: TuskerRootViewController {
return return
} }
if sidebar.selectedItem != .explore { if sidebar.selectedItem != .tab(.explore) {
select(newItem: .explore, oldItem: sidebar.selectedItem) select(newItem: .tab(.explore), oldItem: sidebar.selectedItem)
} }
guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else { guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else {

View File

@ -95,7 +95,7 @@ class NotificationLoadingViewController: UIViewController {
return return
} }
guard let navigationController else { guard let navigationController else {
fatalError("Don't know how to show notification VC outside of navigation controller") fatalError("Don't know how to show notification VC outside of navigation controller: parent is \(parent?.description ?? "<nil>")")
} }
navigationController.viewControllers[navigationController.viewControllers.count - 1] = vc navigationController.viewControllers[navigationController.viewControllers.count - 1] = vc
} }

View File

@ -414,7 +414,7 @@ extension SearchResultsViewController {
hasher.combine(id) hasher.combine(id)
case let .hashtag(hashtag): case let .hashtag(hashtag):
hasher.combine("hashtag") hasher.combine("hashtag")
hasher.combine(hashtag.url) hasher.combine(hashtag.name)
case let .status(id, _): case let .status(id, _):
hasher.combine("status") hasher.combine("status")
hasher.combine(id) hasher.combine(id)

View File

@ -320,13 +320,13 @@ class CustomAlertActionsView: UIControl {
actionButtons[currentSelectedActionIndex].backgroundColor = nil actionButtons[currentSelectedActionIndex].backgroundColor = nil
} }
#if !os(visionOS) #if !os(visionOS)
if selectedButton != nil {
if #available(iOS 17.5, *) { if #available(iOS 17.5, *) {
let view = selectedButton!.element generator.selectionChanged(at: recognizer.location(in: generator.view))
let location = convert(CGPoint(x: view.bounds.midX, y: view.bounds.midY), from: view)
generator.selectionChanged(at: location)
} else { } else {
generator.selectionChanged() generator.selectionChanged()
} }
}
#endif #endif
if let showPressedMenuWorkItem { if let showPressedMenuWorkItem {

View File

@ -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 = 129 CURRENT_PROJECT_VERSION = 131
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