2022-12-19 11:28:55 +00:00
|
|
|
import DesignSystem
|
2022-12-25 12:09:43 +00:00
|
|
|
import Env
|
2023-01-17 10:36:01 +00:00
|
|
|
import Models
|
|
|
|
import Network
|
|
|
|
import Shimmer
|
|
|
|
import SwiftUI
|
2022-12-19 11:28:55 +00:00
|
|
|
|
2023-09-18 19:03:52 +00:00
|
|
|
@MainActor
|
2022-12-19 11:28:55 +00:00
|
|
|
public struct NotificationsListView: View {
|
2023-01-09 17:52:33 +00:00
|
|
|
@Environment(\.scenePhase) private var scenePhase
|
2023-09-18 19:03:52 +00:00
|
|
|
@Environment(Theme.self) private var theme
|
2023-09-18 05:01:23 +00:00
|
|
|
@Environment(StreamWatcher.self) private var watcher
|
|
|
|
@Environment(Client.self) private var client
|
|
|
|
@Environment(RouterPath.self) private var routerPath
|
|
|
|
@Environment(CurrentAccount.self) private var account
|
|
|
|
@State private var viewModel = NotificationsViewModel()
|
2023-10-05 06:22:45 +00:00
|
|
|
@Binding var scrollToTopSignal: Int
|
2023-01-22 05:38:30 +00:00
|
|
|
|
2023-01-19 06:24:24 +00:00
|
|
|
let lockedType: Models.Notification.NotificationType?
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-10-05 06:22:45 +00:00
|
|
|
public init(lockedType: Models.Notification.NotificationType?, scrollToTopSignal: Binding<Int>) {
|
2023-01-19 06:24:24 +00:00
|
|
|
self.lockedType = lockedType
|
2023-10-05 06:22:45 +00:00
|
|
|
_scrollToTopSignal = scrollToTopSignal
|
2023-01-19 06:24:24 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-19 11:28:55 +00:00
|
|
|
public var body: some View {
|
2023-10-05 06:22:45 +00:00
|
|
|
ScrollViewReader { proxy in
|
|
|
|
List {
|
|
|
|
scrollToTopView
|
|
|
|
topPaddingView
|
|
|
|
notificationsView
|
|
|
|
}
|
|
|
|
.id(account.account?.id)
|
|
|
|
.environment(\.defaultMinListRowHeight, 1)
|
|
|
|
.listStyle(.plain)
|
|
|
|
.onChange(of: scrollToTopSignal) {
|
|
|
|
withAnimation {
|
|
|
|
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
|
|
|
}
|
|
|
|
}
|
2022-12-19 11:28:55 +00:00
|
|
|
}
|
2023-11-16 08:53:16 +00:00
|
|
|
.onAppear { viewModel.loadSelectedType() }
|
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
|
|
|
.toolbar {
|
|
|
|
ToolbarItem(placement: .principal) {
|
|
|
|
let title = lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title"
|
|
|
|
if lockedType == nil {
|
|
|
|
Text(title)
|
|
|
|
.font(.headline)
|
|
|
|
.accessibilityRepresentation {
|
|
|
|
Menu(title) {}
|
|
|
|
}
|
|
|
|
.accessibilityAddTraits(.isHeader)
|
|
|
|
.accessibilityRemoveTraits(.isButton)
|
|
|
|
.accessibilityRespondsToUserInteraction(true)
|
|
|
|
} else {
|
|
|
|
Text(title)
|
|
|
|
.font(.headline)
|
|
|
|
.accessibilityAddTraits(.isHeader)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-04 07:14:37 +00:00
|
|
|
.toolbar {
|
2023-01-19 06:24:24 +00:00
|
|
|
if lockedType == nil {
|
|
|
|
ToolbarTitleMenu {
|
2023-01-04 07:14:37 +00:00
|
|
|
Button {
|
2023-01-19 06:24:24 +00:00
|
|
|
viewModel.selectedType = nil
|
2023-01-04 07:14:37 +00:00
|
|
|
} label: {
|
2023-01-19 17:14:08 +00:00
|
|
|
Label("notifications.navigation-title", systemImage: "bell.fill")
|
2023-01-19 06:24:24 +00:00
|
|
|
}
|
|
|
|
Divider()
|
|
|
|
ForEach(Notification.NotificationType.allCases, id: \.self) { type in
|
|
|
|
Button {
|
|
|
|
viewModel.selectedType = type
|
|
|
|
} label: {
|
2023-02-28 17:48:22 +00:00
|
|
|
Label {
|
|
|
|
Text(type.menuTitle())
|
|
|
|
} icon: {
|
|
|
|
type.icon(isPrivate: false)
|
|
|
|
}
|
2023-01-19 06:24:24 +00:00
|
|
|
}
|
2023-01-04 07:14:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-01-05 13:11:55 +00:00
|
|
|
.scrollContentBackground(.hidden)
|
2022-12-29 09:39:34 +00:00
|
|
|
.background(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2022-12-19 11:28:55 +00:00
|
|
|
.task {
|
2023-09-22 20:39:35 +00:00
|
|
|
viewModel.client = client
|
|
|
|
viewModel.currentAccount = account
|
|
|
|
if let lockedType {
|
|
|
|
viewModel.selectedType = lockedType
|
2023-01-19 06:24:24 +00:00
|
|
|
}
|
2023-09-22 20:39:35 +00:00
|
|
|
await viewModel.fetchNotifications()
|
2022-12-19 11:28:55 +00:00
|
|
|
}
|
2022-12-19 15:02:55 +00:00
|
|
|
.refreshable {
|
2023-11-07 10:22:36 +00:00
|
|
|
SoundEffectManager.shared.playSound(.pull)
|
|
|
|
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
|
2022-12-24 11:20:42 +00:00
|
|
|
await viewModel.fetchNotifications()
|
2023-11-07 10:22:36 +00:00
|
|
|
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
|
|
|
|
SoundEffectManager.shared.playSound(.refresh)
|
2022-12-19 15:02:55 +00:00
|
|
|
}
|
2023-09-18 05:01:23 +00:00
|
|
|
.onChange(of: watcher.latestEvent?.id) {
|
2022-12-25 12:09:43 +00:00
|
|
|
if let latestEvent = watcher.latestEvent {
|
|
|
|
viewModel.handleEvent(event: latestEvent)
|
|
|
|
}
|
2023-09-18 05:01:23 +00:00
|
|
|
}
|
|
|
|
.onChange(of: scenePhase) { _, newValue in
|
|
|
|
switch newValue {
|
2023-01-09 17:52:33 +00:00
|
|
|
case .active:
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchNotifications()
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
2023-09-18 05:01:23 +00:00
|
|
|
}
|
2022-12-19 11:28:55 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-21 11:39:29 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var notificationsView: some View {
|
|
|
|
switch viewModel.state {
|
|
|
|
case .loading:
|
2023-01-27 15:58:04 +00:00
|
|
|
ForEach(ConsolidatedNotification.placeholders()) { notification in
|
2023-02-17 17:17:51 +00:00
|
|
|
NotificationRowView(notification: notification,
|
|
|
|
client: client,
|
|
|
|
routerPath: routerPath,
|
|
|
|
followRequests: account.followRequests)
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowInsets(.init(top: 12,
|
|
|
|
leading: .layoutPadding + 4,
|
|
|
|
bottom: 12,
|
|
|
|
trailing: .layoutPadding))
|
2023-12-19 08:51:20 +00:00
|
|
|
#if os(visionOS)
|
|
|
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
|
|
|
.foregroundStyle(Material.regular))
|
|
|
|
#else
|
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
|
|
|
#endif
|
2023-01-30 18:14:43 +00:00
|
|
|
.redacted(reason: .placeholder)
|
2023-09-18 16:55:11 +00:00
|
|
|
.allowsHitTesting(false)
|
2022-12-21 11:39:29 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-21 11:39:29 +00:00
|
|
|
case let .display(notifications, nextPageState):
|
2023-01-05 11:21:54 +00:00
|
|
|
if notifications.isEmpty {
|
|
|
|
EmptyView(iconName: "bell.slash",
|
2023-01-19 17:14:08 +00:00
|
|
|
title: "notifications.empty.title",
|
|
|
|
message: "notifications.empty.message")
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-02-12 15:29:41 +00:00
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-02-12 15:29:41 +00:00
|
|
|
.listSectionSeparator(.hidden)
|
2023-01-05 11:21:54 +00:00
|
|
|
} else {
|
|
|
|
ForEach(notifications) { notification in
|
2023-02-17 17:17:51 +00:00
|
|
|
NotificationRowView(notification: notification,
|
|
|
|
client: client,
|
|
|
|
routerPath: routerPath,
|
|
|
|
followRequests: account.followRequests)
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowInsets(.init(top: 12,
|
|
|
|
leading: .layoutPadding + 4,
|
|
|
|
bottom: 12,
|
|
|
|
trailing: .layoutPadding))
|
2023-12-19 08:51:20 +00:00
|
|
|
#if os(visionOS)
|
|
|
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
|
|
|
.foregroundStyle(notification.type == .mention && lockedType != .mention ? Material.thick : Material.regular))
|
|
|
|
#else
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowBackground(notification.type == .mention && lockedType != .mention ?
|
2023-01-30 06:27:06 +00:00
|
|
|
theme.secondaryBackgroundColor : theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-02-17 17:17:51 +00:00
|
|
|
.id(notification.id)
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|
2022-12-21 11:39:29 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-21 11:39:29 +00:00
|
|
|
switch nextPageState {
|
2022-12-29 09:39:34 +00:00
|
|
|
case .none:
|
|
|
|
EmptyView()
|
2022-12-21 11:39:29 +00:00
|
|
|
case .hasNextPage:
|
|
|
|
loadingRow
|
|
|
|
.onAppear {
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchNextPage()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case .loadingNextPage:
|
|
|
|
loadingRow
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-07 17:01:06 +00:00
|
|
|
case .error:
|
2023-01-19 17:14:08 +00:00
|
|
|
ErrorView(title: "notifications.error.title",
|
|
|
|
message: "notifications.error.message",
|
2023-03-13 12:38:28 +00:00
|
|
|
buttonTitle: "action.retry")
|
|
|
|
{
|
2023-01-07 17:01:06 +00:00
|
|
|
Task {
|
|
|
|
await viewModel.fetchNotifications()
|
|
|
|
}
|
|
|
|
}
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-02-04 20:30:50 +00:00
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-02-04 20:30:50 +00:00
|
|
|
.listSectionSeparator(.hidden)
|
2022-12-21 11:39:29 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-21 11:39:29 +00:00
|
|
|
private var loadingRow: some View {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
ProgressView()
|
|
|
|
Spacer()
|
|
|
|
}
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowInsets(.init(top: .layoutPadding,
|
|
|
|
leading: .layoutPadding + 4,
|
|
|
|
bottom: .layoutPadding,
|
|
|
|
trailing: .layoutPadding))
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-01-29 15:06:46 +00:00
|
|
|
}
|
2023-01-30 06:27:06 +00:00
|
|
|
|
2023-01-29 15:06:46 +00:00
|
|
|
private var topPaddingView: some View {
|
2023-01-30 06:27:06 +00:00
|
|
|
HStack {}
|
2023-01-29 15:06:46 +00:00
|
|
|
.listRowBackground(Color.clear)
|
|
|
|
.listRowSeparator(.hidden)
|
|
|
|
.listRowInsets(.init())
|
|
|
|
.frame(height: .layoutPadding)
|
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
|
|
|
.accessibilityHidden(true)
|
2022-12-21 11:39:29 +00:00
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
|
|
|
|
private var scrollToTopView: some View {
|
|
|
|
ScrollToView()
|
|
|
|
.frame(height: .scrollToViewHeight)
|
|
|
|
.onAppear {
|
|
|
|
viewModel.scrollToTopVisible = true
|
|
|
|
}
|
|
|
|
.onDisappear {
|
|
|
|
viewModel.scrollToTopVisible = false
|
|
|
|
}
|
|
|
|
}
|
2022-12-19 11:28:55 +00:00
|
|
|
}
|