Compare commits
4 Commits
76fc73de95
...
f23d3dfa3f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | f23d3dfa3f | |
Shadowfacts | 23f9e200dc | |
Shadowfacts | 366834e2e4 | |
Shadowfacts | d409d26478 |
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue