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
This release introduces push notifications as well as an enhanced multi-column interface on iPadOS!

View File

@ -1,5 +1,16 @@
# 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)
Bugfixes:
- Fix excessive network traffic on profile pages

View File

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

View File

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

View File

@ -39,6 +39,7 @@ extension TextViewCaretScrolling {
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) {
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
scrollView.layoutIfNeeded()
}
self.caretScrollPositionAnimator = animator
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
offset = min(offset, maxOffset)
return AvatarImageView(
url: status.account.avatar,
size: 50,
style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar
)
.offset(x: 0, y: offset)
return AvatarContainerRepresentable(offset: offset) {
AvatarImageView(
url: status.account.avatar,
size: 50,
style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar
)
}
.frame(width: 50, height: 50)
.accessibilityHidden(true)
}
@ -94,3 +96,39 @@ private struct DisplayNameHeightPrefKey: PreferenceKey {
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
// 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
DispatchQueue.global(qos: .userInitiated).async {

View File

@ -32,7 +32,7 @@ struct MenuController {
static let sidebarItemKeyCommands: [UIKeyCommand] = [
sidebarCommand(item: .tab(.timelines), command: "1", action: #selector(MainSplitViewController.handleSidebarCommandTimelines)),
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: .tab(.myProfile), command: "5", action: #selector(MainSplitViewController.handleSidebarCommandMyProfile)),
]

View File

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

View File

@ -279,7 +279,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
$0.1 > $1.1
}
if let mostRecentExploreItem = mostRecentExploreItem?.0,
mostRecentExploreItem != .explore {
mostRecentExploreItem != .tab(.explore) {
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
exploreNav.popToRootViewController(animated: false)
@ -292,11 +292,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case nil:
break
case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false)
case .explore:
case .tab(.explore):
// 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
// 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
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)
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(_):
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
@ -396,9 +396,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// 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.
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
exploreItem = .explore
exploreItem = .tab(.explore)
// 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
searchVC.loadViewIfNeeded()
let explore = tabNavigationStack.first as! ExploreViewController
@ -426,16 +426,16 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case let instanceVC as InstanceTimelineViewController:
exploreItem = .savedInstance(instanceVC.instanceURL)
case is TrendsViewController:
exploreItem = .explore
exploreItem = .tab(.explore)
// skip transferring the ExploreViewController and TrendsViewController
skipFirst = 2
// prepend the InlineTrendsViewController
toPrepend = getOrCreateNavigationStack(item: .explore).first!
toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
default:
// transfer the navigation stack prepending, the existing explore VC
// if there was other stuff on the explore stack, it will get discarded
toPrepend = getOrCreateNavigationStack(item: .explore).first!
exploreItem = .explore
toPrepend = getOrCreateNavigationStack(item: .tab(.explore)).first!
exploreItem = .tab(.explore)
}
}
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
@ -502,10 +502,10 @@ fileprivate extension MainSidebarViewController.Item {
@MainActor
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
switch self {
case .tab(.explore):
return InlineTrendsViewController(mastodonController: mastodonController)
case let .tab(tab):
return tab.createViewController(mastodonController)
case .explore:
return InlineTrendsViewController(mastodonController: mastodonController)
case .bookmarks:
return BookmarksViewController(mastodonController: mastodonController)
case .favorites:
@ -562,7 +562,7 @@ extension MainSplitViewController: TuskerRootViewController {
case .myProfile:
item = .tab(.myProfile)
case .explore:
item = .explore
item = .tab(.explore)
case .bookmarks:
item = .bookmarks
case .list(id: let id):
@ -616,8 +616,8 @@ extension MainSplitViewController: TuskerRootViewController {
return
}
if sidebar.selectedItem != .explore {
select(newItem: .explore, oldItem: sidebar.selectedItem)
if sidebar.selectedItem != .tab(.explore) {
select(newItem: .tab(.explore), oldItem: sidebar.selectedItem)
}
guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else {

View File

@ -95,7 +95,7 @@ class NotificationLoadingViewController: UIViewController {
return
}
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
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@
// https://help.apple.com/xcode/#/dev745c5c974
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_BUILD_SUFFIX_Debug=-dev