Compare commits

..

4 Commits

Author SHA1 Message Date
Shadowfacts f23d3dfa3f Bump build number and update changelog 2022-11-24 12:24:38 -05:00
Shadowfacts 23f9e200dc Fix potential crash when trying to save timeline state 2022-11-24 12:14:19 -05:00
Shadowfacts 366834e2e4 Tweak timeline state restoration to maintain scroll position of center item 2022-11-24 11:05:56 -05:00
Shadowfacts d409d26478 Fix pressing CW button in Compose not toggling field visibility
Bring back the wrapper view, turn's out it was load bearing. We need to
be able to observe both the ui state and the draft object, while also
updating the observed draft object when the ui state's draft changes,
and this seems like the most straightforward way of doing that.
2022-11-23 14:07:03 -05:00
5 changed files with 64 additions and 48 deletions

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
## 2022.1 (47)
This build is a hotfix for the CW button in the Compose screen not working. The previous build's changelog is attached below.
Bugfixes:
- Fix pressing CW button in Compose not showing the content warning field
- Tweak timeline state restoration to try and maintain the scroll position of the middle item on screen, rather than the top one
## 2022.1 (46) ## 2022.1 (46)
The headlining feature is state restoration and timeline gaps! When you re-open the app after it's been closed for a while, it will remember your position in the timeline and allow you to keep reading from there. It will also let you jump all the way to the present. The headlining feature is state restoration and timeline gaps! When you re-open the app after it's been closed for a while, it will remember your position in the timeline and allow you to keep reading from there. It will also let you jump all the way to the present.

View File

@ -2195,7 +2195,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2263,7 +2263,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements; CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist; INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2413,7 +2413,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2442,7 +2442,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements; CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2552,7 +2552,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements; CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist; INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2579,7 +2579,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements; CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46; CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist; INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -16,7 +16,7 @@ protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
} }
class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewController { class ComposeHostingController: UIHostingController<ComposeHostingController.Wrapper>, DuckableViewController {
weak var delegate: ComposeHostingControllerDelegate? weak var delegate: ComposeHostingControllerDelegate?
weak var duckableDelegate: DuckableViewControllerDelegate? weak var duckableDelegate: DuckableViewControllerDelegate?
@ -36,11 +36,11 @@ class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewCo
self.uiState = ComposeUIState(draft: realDraft) self.uiState = ComposeUIState(draft: realDraft)
let compose = ComposeView( let wrapper = Wrapper(
mastodonController: mastodonController, mastodonController: mastodonController,
uiState: uiState uiState: uiState
) )
super.init(rootView: compose) super.init(rootView: wrapper)
self.uiState.delegate = self self.uiState.delegate = self
@ -129,6 +129,23 @@ class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewCo
} }
extension ComposeHostingController {
struct Wrapper: View {
let mastodonController: MastodonController
@ObservedObject var uiState: ComposeUIState
var draft: Draft {
uiState.draft
}
var body: some View {
ComposeView()
.environmentObject(mastodonController)
.environmentObject(uiState)
.environmentObject(draft)
}
}
}
extension ComposeHostingController: ComposeUIStateDelegate { extension ComposeHostingController: ComposeUIStateDelegate {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self } var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }

View File

@ -42,11 +42,9 @@ import Combine
} }
struct ComposeView: View { struct ComposeView: View {
@ObservedObject var mastodonController: MastodonController @EnvironmentObject var mastodonController: MastodonController
@ObservedObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
var draft: Draft { @EnvironmentObject var draft: Draft
uiState.draft
}
@State private var globalFrameOutsideList: CGRect = .zero @State private var globalFrameOutsideList: CGRect = .zero
@State private var contentWarningBecomeFirstResponder = false @State private var contentWarningBecomeFirstResponder = false
@ -62,11 +60,6 @@ struct ComposeView: View {
private let stackPadding: CGFloat = 8 private let stackPadding: CGFloat = 8
init(mastodonController: MastodonController, uiState: ComposeUIState) {
self.mastodonController = mastodonController
self.uiState = uiState
}
private var charactersRemaining: Int { private var charactersRemaining: Int {
let limit = mastodonController.instanceFeatures.maxStatusChars let limit = mastodonController.instanceFeatures.maxStatusChars
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0 let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
@ -84,12 +77,6 @@ struct ComposeView: View {
} }
var body: some View { var body: some View {
bodyWithoutEnvironment
.environmentObject(uiState)
.environmentObject(mastodonController)
}
private var bodyWithoutEnvironment: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
mainList mainList
.scrollDismissesKeyboardInteractivelyIfAvailable() .scrollDismissesKeyboardInteractivelyIfAvailable()
@ -173,7 +160,7 @@ struct ComposeView: View {
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
if draft.contentWarningEnabled { if uiState.draft.contentWarningEnabled {
ComposeEmojiTextField( ComposeEmojiTextField(
text: $uiState.draft.contentWarning, text: $uiState.draft.contentWarning,
placeholder: "Write your warning here", placeholder: "Write your warning here",

View File

@ -184,39 +184,44 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
func stateRestorationActivity() -> NSUserActivity? { func stateRestorationActivity() -> NSUserActivity? {
guard isViewLoaded else {
return nil
}
let visible = collectionView.indexPathsForVisibleItems.sorted() let visible = collectionView.indexPathsForVisibleItems.sorted()
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
guard let currentAccountID = mastodonController.accountInfo?.id, guard let currentAccountID = mastodonController.accountInfo?.id,
!visible.isEmpty, !visible.isEmpty,
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses), let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
let firstVisible = visible.first(where: { $0.section == statusesSection }), let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
let lastVisible = visible.last(where: { $0.section == statusesSection }) else { let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
return nil return nil
} }
let allItems = snapshot.itemIdentifiers(inSection: .statuses) let allItems = snapshot.itemIdentifiers(inSection: .statuses)
let startIndex = max(0, firstVisible.row - 20) let startIndex = max(0, centerVisible.row - 20)
let endIndex = min(allItems.count - 1, lastVisible.row + 20) let endIndex = min(allItems.count - 1, centerVisible.row + 20)
let firstVisibleItem: Item let centerVisibleItem: Item
var items = allItems[startIndex...endIndex] var items = allItems[startIndex...endIndex]
if let gapIndex = items.firstIndex(of: .gap) { if let gapIndex = items.firstIndex(of: .gap) {
// if the gap is above the top visible item, we take everything below the gap // if the gap is above the top visible item, we take everything below the gap
// otherwise, we take everything above the gap // otherwise, we take everything above the gap
if gapIndex <= firstVisible.row { if gapIndex <= centerVisible.row {
items = allItems[(gapIndex + 1)...endIndex] items = allItems[(gapIndex + 1)...endIndex]
if gapIndex == firstVisible.row { if gapIndex == centerVisible.row {
firstVisibleItem = allItems.first! centerVisibleItem = allItems.first!
} else { } else {
assert(items.indices.contains(firstVisible.row)) assert(items.indices.contains(centerVisible.row))
firstVisibleItem = allItems[firstVisible.row] centerVisibleItem = allItems[centerVisible.row]
} }
} else { } else {
items = allItems[startIndex..<gapIndex] items = allItems[startIndex..<gapIndex]
firstVisibleItem = allItems[firstVisible.row] centerVisibleItem = allItems[centerVisible.row]
} }
} else { } else {
firstVisibleItem = allItems[firstVisible.row] centerVisibleItem = allItems[centerVisible.row]
} }
let ids = items.map { let ids = items.map {
if case .status(id: let id, state: _) = $0 { if case .status(id: let id, state: _) = $0 {
@ -225,18 +230,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
fatalError() fatalError()
} }
} }
let firstVisibleID: String let centerVisibleID: String
if case .status(id: let id, state: _) = firstVisibleItem { if case .status(id: let id, state: _) = centerVisibleItem {
firstVisibleID = id centerVisibleID = id
} else { } else {
fatalError() fatalError()
} }
stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(firstVisibleID)") stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(centerVisibleID)")
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)! let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
activity.addUserInfoEntries(from: [ activity.addUserInfoEntries(from: [
"statusIDs": ids, "statusIDs": ids,
"topID": firstVisibleID, "centerID": centerVisibleID,
]) ])
activity.isEligibleForPrediction = false activity.isEligibleForPrediction = false
return activity return activity
@ -260,8 +265,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
let items = statusIDs.map { Item.status(id: $0, state: .unknown) } let items = statusIDs.map { Item.status(id: $0, state: .unknown) }
snapshot.appendItems(items, toSection: .statuses) snapshot.appendItems(items, toSection: .statuses)
dataSource.apply(snapshot, animatingDifferences: false) { dataSource.apply(snapshot, animatingDifferences: false) {
if let topID = activity.userInfo?["topID"] as? String, if let centerID = activity.userInfo?["centerID"] as? String ?? activity.userInfo?["topID"] as? String,
let index = statusIDs.firstIndex(of: topID), let index = statusIDs.firstIndex(of: centerID),
let indexPath = self.dataSource.indexPath(for: items[index]) { let indexPath = self.dataSource.indexPath(for: items[index]) {
// it sometimes takes multiple attempts to convert on the right scroll position // it sometimes takes multiple attempts to convert on the right scroll position
// since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop // since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop
@ -270,15 +275,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
count += 1 count += 1
let origOffset = self.collectionView.contentOffset let origOffset = self.collectionView.contentOffset
self.collectionView.layoutIfNeeded() self.collectionView.layoutIfNeeded()
self.collectionView.scrollToItem(at: indexPath, at: .top, animated: false) self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
let newOffset = self.collectionView.contentOffset let newOffset = self.collectionView.contentOffset
if abs(origOffset.y - newOffset.y) <= 1 { if abs(origOffset.y - newOffset.y) <= 1 {
break break
} }
} }
stateRestorationLogger.fault("TimelineViewController: restored statuses with top ID \(topID)") stateRestorationLogger.fault("TimelineViewController: restored statuses with center ID \(centerID)")
} else { } else {
stateRestorationLogger.fault("TimelineViewController: restored statuses, but couldn't find top ID") stateRestorationLogger.fault("TimelineViewController: restored statuses, but couldn't find center ID")
} }
} }
} }