Improve notification view filtering UX

- Add subtitle below the toolbar title to indicate the state of the filter
- Add settings icon to take user to the notification settings page, and
  thus make that more discoverable

Testing
-------

PASS

Device: iPhone 13 mini
iOS: 17.6.1
Coverage:
1. Switching back and forth between the notifications tab and other tabs
   causes subtitle to show/hide as expected in both filter options
   (all, friends)
2. Subtitle follows the friends filter
3. Subtitle shows after restarting the app
4. Settings icon appears and takes user to the notification setting view
5. Notification settings can be updated from that view.

Changelog-Changed: Improve notification view filtering UX
Closes: https://github.com/damus-io/damus/issues/2480
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2024-09-16 12:21:10 -07:00
parent ce63f6a96b
commit 6254cea600
5 changed files with 46 additions and 13 deletions

View File

@@ -77,7 +77,12 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil @State var active_sheet: Sheets? = nil
@State var damus_state: DamusState! @State var damus_state: DamusState!
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home @State var menu_subtitle: String? = nil
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home {
willSet {
self.menu_subtitle = nil
}
}
@State var muting: MuteItem? = nil @State var muting: MuteItem? = nil
@State var confirm_mute: Bool = false @State var confirm_mute: Bool = false
@State var hide_bar: Bool = false @State var hide_bar: Bool = false
@@ -159,9 +164,16 @@ struct ContentView: View {
isSideBarOpened = false isSideBarOpened = false
} }
var timelineNavItem: Text { var timelineNavItem: some View {
return Text(timeline_name(selected_timeline)) VStack {
Text(timeline_name(selected_timeline))
.bold() .bold()
if let menu_subtitle {
Text(menu_subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
} }
func MainContent(damus: DamusState) -> some View { func MainContent(damus: DamusState) -> some View {
@@ -180,7 +192,7 @@ struct ContentView: View {
PostingTimelineView PostingTimelineView
case .notifications: case .notifications:
NotificationsView(state: damus, notifications: home.notifications) NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
case .dms: case .dms:
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings) DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)

View File

@@ -9,7 +9,7 @@ import Foundation
enum FriendFilter: String, StringCodable { enum FriendFilter: String, StringCodable {
case all case all
case friends case friends_of_friends
init?(from string: String) { init?(from string: String) {
guard let ff = FriendFilter(rawValue: string) else { guard let ff = FriendFilter(rawValue: string) else {
@@ -27,8 +27,17 @@ enum FriendFilter: String, StringCodable {
switch self { switch self {
case .all: case .all:
return true return true
case .friends: case .friends_of_friends:
return contacts.is_in_friendosphere(pubkey) return contacts.is_in_friendosphere(pubkey)
} }
} }
func description() -> String {
switch self {
case .all:
return NSLocalizedString("All", comment: "Human-readable short description of the 'friends filter' when it is set to 'all'")
case .friends_of_friends:
return NSLocalizedString("Friends of friends", comment: "Human-readable short description of the 'friends filter' when it is set to 'friends-of-friends'")
}
}
} }

View File

@@ -14,12 +14,12 @@ struct FriendsButton: View {
Button(action: { Button(action: {
switch self.filter { switch self.filter {
case .all: case .all:
self.filter = .friends self.filter = .friends_of_friends
case .friends: case .friends_of_friends:
self.filter = .all self.filter = .all
} }
}) { }) {
if filter == .friends { if filter == .friends_of_friends {
LINEAR_GRADIENT LINEAR_GRADIENT
.mask(Image("user-added") .mask(Image("user-added")
.resizable() .resizable()

View File

@@ -103,7 +103,7 @@ struct DirectMessagesView: View {
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool { func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
for dm in dms { for dm in dms {
if !FriendFilter.friends.filter(contacts: contacts, pubkey: dm.pubkey) { if !FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: dm.pubkey) {
return true return true
} }
} }

View File

@@ -56,6 +56,7 @@ struct NotificationsView: View {
@ObservedObject var notifications: NotificationsModel @ObservedObject var notifications: NotificationsModel
@StateObject var filter = NotificationFilter() @StateObject var filter = NotificationFilter()
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all @SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
@Binding var subtitle: String?
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -99,6 +100,15 @@ struct NotificationsView: View {
.tag(NotificationFilterState.replies) .tag(NotificationFilterState.replies)
} }
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: { state.nav.push(route: Route.NotificationSettings(settings: state.settings)) },
label: {
Image("settings")
.foregroundColor(.gray)
}
)
}
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) { if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
FriendsButton(filter: $filter.fine_filter) FriendsButton(filter: $filter.fine_filter)
@@ -107,12 +117,14 @@ struct NotificationsView: View {
} }
.onChange(of: filter.fine_filter) { val in .onChange(of: filter.fine_filter) { val in
state.settings.friend_filter = val state.settings.friend_filter = val
self.subtitle = filter.fine_filter.description()
} }
.onChange(of: filter_state) { val in .onChange(of: filter_state) { val in
filter.state = val filter.state = val
} }
.onAppear { .onAppear {
self.filter.fine_filter = state.settings.friend_filter self.filter.fine_filter = state.settings.friend_filter
self.subtitle = filter.fine_filter.description()
filter.state = filter_state filter.state = filter_state
} }
.safeAreaInset(edge: .top, spacing: 0) { .safeAreaInset(edge: .top, spacing: 0) {
@@ -163,7 +175,7 @@ struct NotificationsView: View {
struct NotificationsView_Previews: PreviewProvider { struct NotificationsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NotificationsView(state: test_damus_state, notifications: NotificationsModel(), filter: NotificationFilter()) NotificationsView(state: test_damus_state, notifications: NotificationsModel(), filter: NotificationFilter(), subtitle: .constant(nil))
} }
} }
@@ -174,7 +186,7 @@ func would_filter_non_friends_from_notifications(contacts: Contacts, state: Noti
continue continue
} }
if item.would_filter({ ev in FriendFilter.friends.filter(contacts: contacts, pubkey: ev.pubkey) }) { if item.would_filter({ ev in FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: ev.pubkey) }) {
return true return true
} }
} }