Compare commits

..

No commits in common. "f23d3dfa3fb77423fb574a02363be0e839abfbc7" and "76fc73de957b9199179656bb88e84b1568090de1" have entirely different histories.

5 changed files with 48 additions and 64 deletions

View File

@ -1,12 +1,5 @@
# 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 = 48; CURRENT_PROJECT_VERSION = 46;
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 = 48; CURRENT_PROJECT_VERSION = 46;
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 = 48; CURRENT_PROJECT_VERSION = 46;
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 = 48; CURRENT_PROJECT_VERSION = 46;
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 = 48; CURRENT_PROJECT_VERSION = 46;
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 = 48; CURRENT_PROJECT_VERSION = 46;
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<ComposeHostingController.Wrapper>, DuckableViewController { class ComposeHostingController: UIHostingController<ComposeView>, DuckableViewController {
weak var delegate: ComposeHostingControllerDelegate? weak var delegate: ComposeHostingControllerDelegate?
weak var duckableDelegate: DuckableViewControllerDelegate? weak var duckableDelegate: DuckableViewControllerDelegate?
@ -36,11 +36,11 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
self.uiState = ComposeUIState(draft: realDraft) self.uiState = ComposeUIState(draft: realDraft)
let wrapper = Wrapper( let compose = ComposeView(
mastodonController: mastodonController, mastodonController: mastodonController,
uiState: uiState uiState: uiState
) )
super.init(rootView: wrapper) super.init(rootView: compose)
self.uiState.delegate = self self.uiState.delegate = self
@ -129,23 +129,6 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
} }
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,9 +42,11 @@ import Combine
} }
struct ComposeView: View { struct ComposeView: View {
@EnvironmentObject var mastodonController: MastodonController @ObservedObject var mastodonController: MastodonController
@EnvironmentObject var uiState: ComposeUIState @ObservedObject var uiState: ComposeUIState
@EnvironmentObject var draft: Draft 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
@ -60,6 +62,11 @@ 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
@ -77,6 +84,12 @@ 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()
@ -160,7 +173,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 uiState.draft.contentWarningEnabled { if 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,44 +184,39 @@ 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 rawCenterVisible = collectionView.indexPathForItem(at: midPoint), let firstVisible = visible.first(where: { $0.section == statusesSection }),
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else { let lastVisible = visible.last(where: { $0.section == statusesSection }) else {
return nil return nil
} }
let allItems = snapshot.itemIdentifiers(inSection: .statuses) let allItems = snapshot.itemIdentifiers(inSection: .statuses)
let startIndex = max(0, centerVisible.row - 20) let startIndex = max(0, firstVisible.row - 20)
let endIndex = min(allItems.count - 1, centerVisible.row + 20) let endIndex = min(allItems.count - 1, lastVisible.row + 20)
let centerVisibleItem: Item let firstVisibleItem: 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 <= centerVisible.row { if gapIndex <= firstVisible.row {
items = allItems[(gapIndex + 1)...endIndex] items = allItems[(gapIndex + 1)...endIndex]
if gapIndex == centerVisible.row { if gapIndex == firstVisible.row {
centerVisibleItem = allItems.first! firstVisibleItem = allItems.first!
} else { } else {
assert(items.indices.contains(centerVisible.row)) assert(items.indices.contains(firstVisible.row))
centerVisibleItem = allItems[centerVisible.row] firstVisibleItem = allItems[firstVisible.row]
} }
} else { } else {
items = allItems[startIndex..<gapIndex] items = allItems[startIndex..<gapIndex]
centerVisibleItem = allItems[centerVisible.row] firstVisibleItem = allItems[firstVisible.row]
} }
} else { } else {
centerVisibleItem = allItems[centerVisible.row] firstVisibleItem = allItems[firstVisible.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 {
@ -230,18 +225,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
fatalError() fatalError()
} }
} }
let centerVisibleID: String let firstVisibleID: String
if case .status(id: let id, state: _) = centerVisibleItem { if case .status(id: let id, state: _) = firstVisibleItem {
centerVisibleID = id firstVisibleID = id
} else { } else {
fatalError() fatalError()
} }
stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(centerVisibleID)") stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(firstVisibleID)")
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,
"centerID": centerVisibleID, "topID": firstVisibleID,
]) ])
activity.isEligibleForPrediction = false activity.isEligibleForPrediction = false
return activity return activity
@ -265,8 +260,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 centerID = activity.userInfo?["centerID"] as? String ?? activity.userInfo?["topID"] as? String, if let topID = activity.userInfo?["topID"] as? String,
let index = statusIDs.firstIndex(of: centerID), let index = statusIDs.firstIndex(of: topID),
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
@ -275,15 +270,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: .centeredVertically, animated: false) self.collectionView.scrollToItem(at: indexPath, at: .top, 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 center ID \(centerID)") stateRestorationLogger.fault("TimelineViewController: restored statuses with top ID \(topID)")
} else { } else {
stateRestorationLogger.fault("TimelineViewController: restored statuses, but couldn't find center ID") stateRestorationLogger.fault("TimelineViewController: restored statuses, but couldn't find top ID")
} }
} }
} }