NotificationsView.swift (6667B)
1 // 2 // NotificationsView.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-02-21. 6 // 7 8 import SwiftUI 9 10 class NotificationFilter: ObservableObject, Equatable { 11 @Published var state: NotificationFilterState 12 @Published var fine_filter: FriendFilter 13 14 static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool { 15 return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter 16 } 17 18 init(state: NotificationFilterState = .all, fine_filter: FriendFilter = .all) { 19 self.state = state 20 self.fine_filter = fine_filter 21 } 22 23 func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] { 24 25 return items.reduce(into: []) { acc, item in 26 if !self.state.filter(item) { 27 return 28 } 29 30 if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) { 31 acc.append(item) 32 } 33 } 34 } 35 } 36 37 enum NotificationFilterState: String { 38 case all 39 case zaps 40 case replies 41 42 func filter(_ item: NotificationItem) -> Bool { 43 switch self { 44 case .all: 45 return true 46 case .replies: 47 return item.is_reply != nil 48 case .zaps: 49 return item.is_zap != nil 50 } 51 } 52 } 53 54 struct NotificationsView: View { 55 let state: DamusState 56 @ObservedObject var notifications: NotificationsModel 57 @StateObject var filter = NotificationFilter() 58 @SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all 59 @Binding var subtitle: String? 60 61 @Environment(\.colorScheme) var colorScheme 62 63 var body: some View { 64 TabView(selection: $filter_state) { 65 NotificationTab( 66 NotificationFilter( 67 state: .all, 68 fine_filter: filter.fine_filter 69 ) 70 ) 71 .tag(NotificationFilterState.all) 72 73 NotificationTab( 74 NotificationFilter( 75 state: .zaps, 76 fine_filter: filter.fine_filter 77 ) 78 ) 79 .tag(NotificationFilterState.zaps) 80 81 NotificationTab( 82 NotificationFilter( 83 state: .replies, 84 fine_filter: filter.fine_filter 85 ) 86 ) 87 .tag(NotificationFilterState.replies) 88 } 89 .toolbar { 90 ToolbarItem(placement: .navigationBarTrailing) { 91 Button( 92 action: { state.nav.push(route: Route.NotificationSettings(settings: state.settings)) }, 93 label: { 94 Image("settings") 95 .foregroundColor(.gray) 96 } 97 ) 98 } 99 ToolbarItem(placement: .navigationBarTrailing) { 100 if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) { 101 FriendsButton(filter: $filter.fine_filter) 102 } 103 } 104 } 105 .onChange(of: filter.fine_filter) { val in 106 state.settings.friend_filter = val 107 self.subtitle = filter.fine_filter.description() 108 } 109 .onChange(of: filter_state) { val in 110 filter.state = val 111 } 112 .onAppear { 113 self.filter.fine_filter = state.settings.friend_filter 114 self.subtitle = filter.fine_filter.description() 115 filter.state = filter_state 116 } 117 .safeAreaInset(edge: .top, spacing: 0) { 118 VStack(spacing: 0) { 119 CustomPicker(tabs: [ 120 (NSLocalizedString("All", comment: "Label for filter for all notifications."), NotificationFilterState.all), 121 (NSLocalizedString("Zaps", comment: "Label for filter for zap notifications."), NotificationFilterState.zaps), 122 (NSLocalizedString("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc)."), NotificationFilterState.replies), 123 ], selection: $filter_state) 124 Divider() 125 .frame(height: 1) 126 } 127 .background(colorScheme == .dark ? Color.black : Color.white) 128 } 129 } 130 131 func NotificationTab(_ filter: NotificationFilter) -> some View { 132 ScrollViewReader { scroller in 133 ScrollView { 134 let notifs = Array(zip(1..., filter.filter(contacts: state.contacts, items: notifications.notifications))) 135 if notifs.isEmpty { 136 EmptyTimelineView() 137 } else { 138 LazyVStack(alignment: .leading) { 139 Color.white.opacity(0) 140 .id("startblock") 141 .frame(height: 5) 142 ForEach(notifs, id: \.0) { zip in 143 NotificationItemView(state: state, item: zip.1) 144 } 145 } 146 .background(GeometryReader { proxy -> Color in 147 DispatchQueue.main.async { 148 handle_scroll_queue(proxy, queue: self.notifications) 149 } 150 return Color.clear 151 }) 152 } 153 } 154 .coordinateSpace(name: "scroll") 155 .onReceive(handle_notify(.scroll_to_top)) { notif in 156 let _ = notifications.flush(state) 157 self.notifications.should_queue = false 158 scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top) 159 } 160 } 161 .onAppear { 162 let _ = notifications.flush(state) 163 } 164 } 165 } 166 167 struct NotificationsView_Previews: PreviewProvider { 168 static var previews: some View { 169 NotificationsView(state: test_damus_state, notifications: NotificationsModel(), filter: NotificationFilter(), subtitle: .constant(nil)) 170 } 171 } 172 173 func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool { 174 for item in items { 175 // this is only valid depending on which tab we're looking at 176 if !state.filter(item) { 177 continue 178 } 179 180 if item.would_filter({ ev in FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: ev.pubkey) }) { 181 return true 182 } 183 } 184 185 return false 186 } 187