IceCubesApp/Packages/Timeline/Sources/Timeline/TimelineView.swift

272 lines
8.2 KiB
Swift
Raw Normal View History

2023-01-17 10:36:01 +00:00
import DesignSystem
import Env
2022-12-17 12:37:46 +00:00
import Models
2023-01-17 10:36:01 +00:00
import Network
2022-12-17 12:37:46 +00:00
import Shimmer
2022-12-18 19:30:19 +00:00
import Status
2023-01-17 10:36:01 +00:00
import SwiftUI
2023-07-19 05:46:25 +00:00
import SwiftUIIntrospect
2022-11-21 08:31:32 +00:00
public struct TimelineView: View {
2022-12-27 06:51:44 +00:00
private enum Constants {
static let scrollToTop = "top"
}
2023-01-17 10:36:01 +00:00
2022-12-26 07:24:55 +00:00
@Environment(\.scenePhase) private var scenePhase
2022-12-29 09:39:34 +00:00
@EnvironmentObject private var theme: Theme
2022-12-25 18:18:19 +00:00
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var watcher: StreamWatcher
2022-11-29 11:18:06 +00:00
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
2023-01-17 10:36:01 +00:00
2022-11-29 11:18:06 +00:00
@StateObject private var viewModel = TimelineViewModel()
@StateObject private var prefetcher = TimelinePrefetcher()
2023-02-01 11:49:59 +00:00
@State private var wasBackgrounded: Bool = false
@State private var collectionView: UICollectionView?
2023-02-01 11:49:59 +00:00
@Binding var timeline: TimelineFilter
2022-12-31 11:28:27 +00:00
@Binding var scrollToTopSignal: Int
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
private let canFilterTimeline: Bool
2023-01-17 10:36:01 +00:00
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool) {
_timeline = timeline
2022-12-31 11:28:27 +00:00
_scrollToTopSignal = scrollToTopSignal
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
self.canFilterTimeline = canFilterTimeline
2022-12-20 14:37:51 +00:00
}
2023-01-17 10:36:01 +00:00
2022-11-21 08:31:32 +00:00
public var body: some View {
2022-12-25 17:43:15 +00:00
ScrollViewReader { proxy in
ZStack(alignment: .top) {
List {
2023-07-19 05:46:25 +00:00
if viewModel.tagGroup != nil {
tagGroupHeaderView
} else if viewModel.tag == nil {
scrollToTopView
} else {
2022-12-25 17:43:15 +00:00
tagHeaderView
}
switch viewModel.timeline {
case .remoteLocal:
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true)
default:
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
}
2022-12-25 17:43:15 +00:00
}
.id(client.id)
.environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain)
.scrollContentBackground(.hidden)
.introspect(.list, on: .iOS(.v16, .v17)) { (collectionView: UICollectionView) in
2023-07-18 11:58:00 +00:00
DispatchQueue.main.async {
self.collectionView = collectionView
}
self.prefetcher.viewModel = viewModel
collectionView.isPrefetchingEnabled = true
collectionView.prefetchDataSource = self.prefetcher
}
2023-02-25 18:47:15 +00:00
if viewModel.timeline.supportNewestPagination {
PendingStatusesObserverView(observer: viewModel.pendingStatusesObserver)
2022-12-25 17:43:15 +00:00
}
2022-11-25 11:00:01 +00:00
}
.onChange(of: viewModel.scrollToIndex) { index in
2023-02-05 07:13:38 +00:00
if let collectionView,
let index,
let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
2023-02-12 15:29:41 +00:00
rows > index
{
2023-02-05 07:13:38 +00:00
collectionView.scrollToItem(at: .init(row: index, section: 0),
at: .top,
animated: viewModel.scrollToIndexAnimated)
viewModel.scrollToIndexAnimated = false
viewModel.scrollToIndex = nil
}
2022-12-31 11:28:27 +00:00
}
.onChange(of: scrollToTopSignal, perform: { _ in
withAnimation {
proxy.scrollTo(Constants.scrollToTop, anchor: .top)
}
})
2022-11-21 08:31:32 +00:00
}
2023-02-06 11:24:48 +00:00
.toolbar {
ToolbarItem(placement: .principal) {
VStack(alignment: .center) {
switch timeline {
case let .remoteLocal(_, filter):
Text(filter.localizedTitle())
.font(.headline)
Text(timeline.localizedTitle())
.font(.caption)
.foregroundColor(.gray)
default:
Text(timeline.localizedTitle())
.font(.headline)
}
}
.accessibilityRepresentation {
Timeline & Timeline detail accessibility uplift (#1323) * Improve accessibility of StatusPollView Previously, this view did not provide the proper context to indicate that it represented a poll. Now, we’ve added - A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options; - A prefix to say “Option X of Y” before each option; - A Selected trait on the selected option(s), if present - Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown. * Add poll description in StatusRowView combinedAccessibilityLabel This largely duplicates the logic in `StatusPollView`. * Improve accessibility of media attachments Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen. Now, they are presented with their attachment type. * Change accessibilityRepresentation of AppAcountsSelectorView * Change Notifications tab title view accessibility representation to Menu Previously it would present as a button * Hide layout `Rectangle`s from accessibility * Consolidate `StatusRowDetailView` accessibility representation * Improve readability of Poll accessibility label * Ensure poll options don’t present as interactive when the poll is finished * Improve accessibility of StatusRowCardView Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait. Now, it presents as a single element with the `.link` trait * Improve accessibility of StatusRowHeaderView Previously, it had no traits and no actions except inherited ones. Now it presents as a button, triggering its primary action. It also has custom actions corresponding to its context menu * Avoid applying the StatusRowView custom actions to every view when contained * Provide context for the application name * Add pauses to StatusRowView combinedAccessibilityLabel * Hide `TimelineView.scrollToTopView` from accessibility * Set appropriate font style on Notification header After the change the Text needed a `.headline` style to match the prior appearance. * Fix bug in accessibilityRepresentation of TimelineView nav bar title Previously, it would not display the proper label for .remoteLocal filter options. * Ensure that pop-up button nav bar titles are interactive * Ensure TextView responds to Environment.sizeCategory This resolves #1309 * Fix button --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-28 16:48:58 +00:00
switch timeline {
2023-07-19 05:46:25 +00:00
case let .remoteLocal(_, filter):
if canFilterTimeline {
Menu(filter.localizedTitle()) {}
} else {
Text(filter.localizedTitle())
}
default:
if canFilterTimeline {
Menu(timeline.localizedTitle()) {}
} else {
Text(timeline.localizedTitle())
}
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
}
}
.accessibilityAddTraits(.isHeader)
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
.accessibilityRemoveTraits(.isButton)
Timeline & Timeline detail accessibility uplift (#1323) * Improve accessibility of StatusPollView Previously, this view did not provide the proper context to indicate that it represented a poll. Now, we’ve added - A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options; - A prefix to say “Option X of Y” before each option; - A Selected trait on the selected option(s), if present - Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown. * Add poll description in StatusRowView combinedAccessibilityLabel This largely duplicates the logic in `StatusPollView`. * Improve accessibility of media attachments Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen. Now, they are presented with their attachment type. * Change accessibilityRepresentation of AppAcountsSelectorView * Change Notifications tab title view accessibility representation to Menu Previously it would present as a button * Hide layout `Rectangle`s from accessibility * Consolidate `StatusRowDetailView` accessibility representation * Improve readability of Poll accessibility label * Ensure poll options don’t present as interactive when the poll is finished * Improve accessibility of StatusRowCardView Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait. Now, it presents as a single element with the `.link` trait * Improve accessibility of StatusRowHeaderView Previously, it had no traits and no actions except inherited ones. Now it presents as a button, triggering its primary action. It also has custom actions corresponding to its context menu * Avoid applying the StatusRowView custom actions to every view when contained * Provide context for the application name * Add pauses to StatusRowView combinedAccessibilityLabel * Hide `TimelineView.scrollToTopView` from accessibility * Set appropriate font style on Notification header After the change the Text needed a `.headline` style to match the prior appearance. * Fix bug in accessibilityRepresentation of TimelineView nav bar title Previously, it would not display the proper label for .remoteLocal filter options. * Ensure that pop-up button nav bar titles are interactive * Ensure TextView responds to Environment.sizeCategory This resolves #1309 * Fix button --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-28 16:48:58 +00:00
.accessibilityRespondsToUserInteraction(canFilterTimeline)
2023-02-06 11:24:48 +00:00
}
}
.navigationBarTitleDisplayMode(.inline)
2022-12-20 14:37:51 +00:00
.onAppear {
viewModel.isTimelineVisible = true
2023-02-26 05:45:57 +00:00
if viewModel.client == nil {
viewModel.client = client
}
2023-02-26 05:45:57 +00:00
2023-02-25 09:10:27 +00:00
viewModel.timeline = timeline
}
.onDisappear {
viewModel.isTimelineVisible = false
}
.refreshable {
2023-02-28 17:55:08 +00:00
SoundEffectManager.shared.playSound(of: .pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
await viewModel.pullToRefresh()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
2023-02-28 17:55:08 +00:00
SoundEffectManager.shared.playSound(of: .refresh)
}
.onChange(of: watcher.latestEvent?.id) { _ in
if let latestEvent = watcher.latestEvent {
2022-12-25 18:18:19 +00:00
viewModel.handleEvent(event: latestEvent, currentAccount: account)
}
}
.onChange(of: timeline) { newTimeline in
switch newTimeline {
2023-02-06 11:24:48 +00:00
case let .remoteLocal(server, _):
viewModel.client = Client(server: server)
default:
viewModel.client = client
}
viewModel.timeline = newTimeline
}
.onChange(of: viewModel.timeline, perform: { newValue in
timeline = newValue
})
2022-12-26 07:24:55 +00:00
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
case .active:
if wasBackgrounded {
wasBackgrounded = false
viewModel.refreshTimeline()
2022-12-26 07:24:55 +00:00
}
case .background:
wasBackgrounded = true
2023-02-01 11:49:59 +00:00
2022-12-26 07:24:55 +00:00
default:
break
}
})
}
2023-01-17 10:36:01 +00:00
2022-12-21 11:39:29 +00:00
@ViewBuilder
private var tagHeaderView: some View {
if let tag = viewModel.tag {
2023-07-19 05:46:25 +00:00
headerView {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("#\(tag.name)")
.font(.scaledHeadline)
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
.font(.scaledFootnote)
.foregroundColor(.gray)
}
.accessibilityElement(children: .combine)
Spacer()
Button {
Task {
if tag.following {
viewModel.tag = await account.unfollowTag(id: tag.name)
} else {
viewModel.tag = await account.followTag(id: tag.name)
}
}
2023-07-19 05:46:25 +00:00
} label: {
Text(tag.following ? "account.follow.following" : "account.follow.follow")
}.buttonStyle(.bordered)
}
2023-07-19 05:46:25 +00:00
}
2022-12-21 11:39:29 +00:00
}
}
2023-07-19 05:46:25 +00:00
@ViewBuilder
private var tagGroupHeaderView: some View {
if let group = viewModel.tagGroup {
headerView {
2023-08-04 10:40:21 +00:00
HStack {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(group.tags, id: \.self) { tag in
Button {
routerPath.navigate(to: .hashTag(tag: tag, account: nil))
} label: {
Text("#\(tag)")
.font(.scaledHeadline)
}
.buttonStyle(.plain)
}
}
}
2023-08-04 10:40:21 +00:00
.scrollIndicators(.hidden)
Button("status.action.edit") {
routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: { group in
viewModel.timeline = .tagGroup(group)
})
}
.buttonStyle(.bordered)
2023-07-19 05:46:25 +00:00
}
}
}
2023-07-19 05:46:25 +00:00
}
@ViewBuilder
private func headerView(
@ViewBuilder content: () -> some View
) -> some View {
VStack(alignment: .leading) {
Spacer()
content()
Spacer()
}
2023-07-19 05:46:25 +00:00
.listRowBackground(theme.secondaryBackgroundColor)
.listRowSeparator(.hidden)
.listRowInsets(.init(top: 8,
leading: .layoutPadding,
bottom: 8,
trailing: .layoutPadding))
}
2023-02-01 11:49:59 +00:00
private var scrollToTopView: some View {
2023-02-01 11:49:59 +00:00
HStack { EmptyView() }
.listRowSeparator(.hidden)
.listRowInsets(.init())
.frame(height: .layoutPadding)
.id(Constants.scrollToTop)
.onAppear {
viewModel.scrollToTopVisible = true
}
.onDisappear {
viewModel.scrollToTopVisible = false
}
Timeline & Timeline detail accessibility uplift (#1323) * Improve accessibility of StatusPollView Previously, this view did not provide the proper context to indicate that it represented a poll. Now, we’ve added - A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options; - A prefix to say “Option X of Y” before each option; - A Selected trait on the selected option(s), if present - Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown. * Add poll description in StatusRowView combinedAccessibilityLabel This largely duplicates the logic in `StatusPollView`. * Improve accessibility of media attachments Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen. Now, they are presented with their attachment type. * Change accessibilityRepresentation of AppAcountsSelectorView * Change Notifications tab title view accessibility representation to Menu Previously it would present as a button * Hide layout `Rectangle`s from accessibility * Consolidate `StatusRowDetailView` accessibility representation * Improve readability of Poll accessibility label * Ensure poll options don’t present as interactive when the poll is finished * Improve accessibility of StatusRowCardView Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait. Now, it presents as a single element with the `.link` trait * Improve accessibility of StatusRowHeaderView Previously, it had no traits and no actions except inherited ones. Now it presents as a button, triggering its primary action. It also has custom actions corresponding to its context menu * Avoid applying the StatusRowView custom actions to every view when contained * Provide context for the application name * Add pauses to StatusRowView combinedAccessibilityLabel * Hide `TimelineView.scrollToTopView` from accessibility * Set appropriate font style on Notification header After the change the Text needed a `.headline` style to match the prior appearance. * Fix bug in accessibilityRepresentation of TimelineView nav bar title Previously, it would not display the proper label for .remoteLocal filter options. * Ensure that pop-up button nav bar titles are interactive * Ensure TextView responds to Environment.sizeCategory This resolves #1309 * Fix button --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-28 16:48:58 +00:00
.accessibilityHidden(true)
}
2022-11-21 08:31:32 +00:00
}