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
## 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)
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_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2263,7 +2263,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2413,7 +2413,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2442,7 +2442,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2552,7 +2552,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2579,7 +2579,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 46;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -16,7 +16,7 @@ protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
}
class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewController {
class ComposeHostingController: UIHostingController<ComposeHostingController.Wrapper>, DuckableViewController {
weak var delegate: ComposeHostingControllerDelegate?
weak var duckableDelegate: DuckableViewControllerDelegate?
@ -36,11 +36,11 @@ class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewCo
self.uiState = ComposeUIState(draft: realDraft)
let compose = ComposeView(
let wrapper = Wrapper(
mastodonController: mastodonController,
uiState: uiState
)
super.init(rootView: compose)
super.init(rootView: wrapper)
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 {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }

View File

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

View File

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