Configurable notification dots
Changelog-Added: Make notification dots configurable
This commit is contained in:
@@ -291,7 +291,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
|
|
||||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
|
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, settings: damus.settings, action: switch_timeline)
|
||||||
.padding([.bottom], 8)
|
.padding([.bottom], 8)
|
||||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,19 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct NewEventsBits {
|
struct NewEventsBits: OptionSet {
|
||||||
let bits: Int
|
let rawValue: Int
|
||||||
|
|
||||||
init() {
|
static let home = NewEventsBits(rawValue: 1 << 0)
|
||||||
bits = 0
|
static let zaps = NewEventsBits(rawValue: 1 << 1)
|
||||||
}
|
static let mentions = NewEventsBits(rawValue: 1 << 2)
|
||||||
|
static let reposts = NewEventsBits(rawValue: 1 << 3)
|
||||||
init (prev: NewEventsBits, setting: Timeline) {
|
static let likes = NewEventsBits(rawValue: 1 << 4)
|
||||||
self.bits = prev.bits | timeline_bit(setting)
|
static let search = NewEventsBits(rawValue: 1 << 5)
|
||||||
}
|
static let dms = NewEventsBits(rawValue: 1 << 6)
|
||||||
|
|
||||||
init (prev: NewEventsBits, unsetting: Timeline) {
|
static let all = NewEventsBits(rawValue: 0xFFFFFFFF)
|
||||||
self.bits = prev.bits & ~timeline_bit(unsetting)
|
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||||
}
|
|
||||||
|
|
||||||
func is_set(_ timeline: Timeline) -> Bool {
|
|
||||||
let notification_bit = timeline_bit(timeline)
|
|
||||||
return (bits & notification_bit) == notification_bit
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeModel: ObservableObject {
|
class HomeModel: ObservableObject {
|
||||||
@@ -901,6 +894,45 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
|
|||||||
return new_events
|
return new_events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
|
||||||
|
guard let kind = ev.known_kind else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == .zap {
|
||||||
|
return [.zaps]
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == .boost {
|
||||||
|
return [.reposts]
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == .text {
|
||||||
|
return [.mentions]
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == .like {
|
||||||
|
return [.likes]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
return [.home]
|
||||||
|
case .notifications:
|
||||||
|
if let ev {
|
||||||
|
return determine_event_notifications(ev)
|
||||||
|
}
|
||||||
|
return [.notifications]
|
||||||
|
case .search:
|
||||||
|
return [.search]
|
||||||
|
case .dms:
|
||||||
|
return [.dms]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A helper to determine if we need to notify the user of new events
|
/// A helper to determine if we need to notify the user of new events
|
||||||
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
|
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
|
||||||
@@ -909,7 +941,7 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
|
|||||||
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
||||||
save_last_event(ev, timeline: timeline)
|
save_last_event(ev, timeline: timeline)
|
||||||
if shouldNotify {
|
if shouldNotify {
|
||||||
return NewEventsBits(prev: new_events, setting: timeline)
|
return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,12 @@ class UserSettingsStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var notification_indicators: Int {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var truncate_mention_text: Bool {
|
@Published var truncate_mention_text: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
|
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
|
||||||
@@ -280,6 +286,7 @@ class UserSettingsStore: ObservableObject {
|
|||||||
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
|
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
|
||||||
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
|
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
|
||||||
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
|
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
|
||||||
|
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
|
||||||
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
|
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
|
||||||
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
|
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
|
||||||
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
|
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ func timeline_bit(_ timeline: Timeline) -> Int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func show_indicator(timeline: Timeline, current: NewEventsBits, indicator_setting: Int) -> Bool {
|
||||||
|
if timeline == .notifications {
|
||||||
|
return (current.rawValue & indicator_setting & NewEventsBits.notifications.rawValue) > 0
|
||||||
|
}
|
||||||
|
return (current.rawValue & indicator_setting) == timeline_to_notification_bits(timeline, ev: nil).rawValue
|
||||||
|
}
|
||||||
|
|
||||||
struct TabButton: View {
|
struct TabButton: View {
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
@@ -35,13 +41,14 @@ struct TabButton: View {
|
|||||||
@Binding var new_events: NewEventsBits
|
@Binding var new_events: NewEventsBits
|
||||||
@Binding var isSidebarVisible: Bool
|
@Binding var isSidebarVisible: Bool
|
||||||
|
|
||||||
|
let settings: UserSettingsStore
|
||||||
let action: (Timeline) -> ()
|
let action: (Timeline) -> ()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
Tab
|
Tab
|
||||||
|
|
||||||
if new_events.is_set(timeline) {
|
if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) {
|
||||||
Circle()
|
Circle()
|
||||||
.size(CGSize(width: 8, height: 8))
|
.size(CGSize(width: 8, height: 8))
|
||||||
.frame(width: 10, height: 10, alignment: .topTrailing)
|
.frame(width: 10, height: 10, alignment: .topTrailing)
|
||||||
@@ -55,7 +62,8 @@ struct TabButton: View {
|
|||||||
var Tab: some View {
|
var Tab: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
action(timeline)
|
action(timeline)
|
||||||
new_events = NewEventsBits(prev: new_events, unsetting: timeline)
|
let bits = timeline_to_notification_bits(timeline, ev: nil)
|
||||||
|
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
|
||||||
isSidebarVisible = false
|
isSidebarVisible = false
|
||||||
}) {
|
}) {
|
||||||
Label("", systemImage: selected == timeline ? "\(img).fill" : img)
|
Label("", systemImage: selected == timeline ? "\(img).fill" : img)
|
||||||
@@ -72,16 +80,17 @@ struct TabBar: View {
|
|||||||
@Binding var selected: Timeline?
|
@Binding var selected: Timeline?
|
||||||
@Binding var isSidebarVisible: Bool
|
@Binding var isSidebarVisible: Bool
|
||||||
|
|
||||||
|
let settings: UserSettingsStore
|
||||||
let action: (Timeline) -> ()
|
let action: (Timeline) -> ()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("1")
|
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, settings: settings, action: action).keyboardShortcut("1")
|
||||||
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("2")
|
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, settings: settings, action: action).keyboardShortcut("2")
|
||||||
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("3")
|
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, settings: settings, action: action).keyboardShortcut("3")
|
||||||
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("4")
|
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, settings: settings, action: action).keyboardShortcut("4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ enum NotificationFilterState: String {
|
|||||||
case zaps
|
case zaps
|
||||||
case replies
|
case replies
|
||||||
|
|
||||||
|
func is_other( item: NotificationItem) -> Bool {
|
||||||
|
return item.is_zap == nil && item.is_reply == nil
|
||||||
|
}
|
||||||
|
|
||||||
func filter(_ item: NotificationItem) -> Bool {
|
func filter(_ item: NotificationItem) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
@@ -27,16 +31,10 @@ enum NotificationFilterState: String {
|
|||||||
struct NotificationsView: View {
|
struct NotificationsView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@ObservedObject var notifications: NotificationsModel
|
@ObservedObject var notifications: NotificationsModel
|
||||||
@State var filter_state: NotificationFilterState
|
@State var filter_state: NotificationFilterState = .all
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
init(state: DamusState, notifications: NotificationsModel) {
|
|
||||||
self.state = state
|
|
||||||
self._notifications = ObservedObject(initialValue: notifications)
|
|
||||||
self._filter_state = State(initialValue: load_notification_filter_state(pubkey: state.pubkey))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $filter_state) {
|
TabView(selection: $filter_state) {
|
||||||
NotificationTab(NotificationFilterState.all)
|
NotificationTab(NotificationFilterState.all)
|
||||||
@@ -54,6 +52,9 @@ struct NotificationsView: View {
|
|||||||
.onChange(of: filter_state) { val in
|
.onChange(of: filter_state) { val in
|
||||||
save_notification_filter_state(pubkey: state.pubkey, state: val)
|
save_notification_filter_state(pubkey: state.pubkey, state: val)
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
self.filter_state = load_notification_filter_state(pubkey: state.pubkey)
|
||||||
|
}
|
||||||
.safeAreaInset(edge: .top, spacing: 0) {
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
CustomPicker(selection: $filter_state, content: {
|
CustomPicker(selection: $filter_state, content: {
|
||||||
@@ -107,7 +108,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())
|
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilterState.all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,19 @@ struct NotificationSettingsView: View {
|
|||||||
@ObservedObject var settings: UserSettingsStore
|
@ObservedObject var settings: UserSettingsStore
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
func indicator_binding(_ val: NewEventsBits) -> Binding<Bool> {
|
||||||
|
return Binding.init(get: {
|
||||||
|
(settings.notification_indicators & val.rawValue) > 0
|
||||||
|
}, set: { v in
|
||||||
|
if v {
|
||||||
|
settings.notification_indicators |= val.rawValue
|
||||||
|
} else {
|
||||||
|
settings.notification_indicators &= ~val.rawValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
|
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
|
||||||
@@ -31,6 +43,17 @@ struct NotificationSettingsView: View {
|
|||||||
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
|
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(header: Text(NSLocalizedString("Notification Dots", comment: "Section header for notification indicator dot settings"))) {
|
||||||
|
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps))
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions))
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: indicator_binding(.reposts))
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: indicator_binding(.likes))
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Notifications")
|
.navigationTitle("Notifications")
|
||||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||||
|
|||||||
Reference in New Issue
Block a user