From c77fe717c57776f9dfa77cee9810c3c0174c0df7 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Sun, 8 Dec 2024 00:37:36 +0100 Subject: [PATCH 1/4] add mentions toggle this creates a new NotificationsViewController, that also allows to open the system's notification settings --- deltachat-ios.xcodeproj/project.pbxproj | 4 + .../NotificationsViewController.swift | 145 ++++++++++++++++++ .../Settings/SettingsViewController.swift | 34 +--- deltachat-ios/DC/DcContext.swift | 8 + deltachat-ios/en.lproj/Localizable.strings | 2 + scripts/untranslated.xml | 2 + 6 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 deltachat-ios/Controller/Settings/NotificationsViewController.swift diff --git a/deltachat-ios.xcodeproj/project.pbxproj b/deltachat-ios.xcodeproj/project.pbxproj index 148a64411..7d07bb99e 100644 --- a/deltachat-ios.xcodeproj/project.pbxproj +++ b/deltachat-ios.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ B20462E42440A4A600367A57 /* AutodelOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20462E32440A4A600367A57 /* AutodelOverviewViewController.swift */; }; B20462E62440C99600367A57 /* AutodelOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20462E52440C99600367A57 /* AutodelOptionsViewController.swift */; }; B206C2AB2B7B8088003ACBE6 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B206C2AA2B7B8088003ACBE6 /* MapViewController.swift */; }; + B20A907B2D05112800F44463 /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20A907A2D05112700F44463 /* NotificationsViewController.swift */; }; B21005DB23383664004C70C5 /* EmailOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21005DA23383664004C70C5 /* EmailOptionsViewController.swift */; }; B2172F3C29C125F2002C289E /* AdvancedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2172F3B29C125F2002C289E /* AdvancedViewController.swift */; }; B259D64329B771D5008FB706 /* BackupTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B259D64229B771D5008FB706 /* BackupTransferViewController.swift */; }; @@ -548,6 +549,7 @@ B209B2042B10139700FBBECF /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = ""; }; B209B2052B10139700FBBECF /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; B209B2062B10139700FBBECF /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; + B20A907A2D05112700F44463 /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; }; B21005DA23383664004C70C5 /* EmailOptionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailOptionsViewController.swift; sourceTree = ""; }; B2172F3B29C125F2002C289E /* AdvancedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedViewController.swift; sourceTree = ""; }; B2537DD625E2F92F0010D739 /* ckb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ckb; path = ckb.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1178,6 +1180,7 @@ D8CF2DDB2CDD110F001C2352 /* Proxy */, 78E45E3921D3CFBC00D4B15E /* SettingsViewController.swift */, B2D4B63A29C38D1900B47DA8 /* ChatsAndMediaViewController.swift */, + B20A907A2D05112700F44463 /* NotificationsViewController.swift */, B2172F3B29C125F2002C289E /* AdvancedViewController.swift */, AEE6EC472283045D00EDC689 /* SelfProfileViewController.swift */, B21005DA23383664004C70C5 /* EmailOptionsViewController.swift */, @@ -1779,6 +1782,7 @@ 70B8882E2091B8550074812E /* ContactCell.swift in Sources */, 305961CD2346125100C80F33 /* UIEdgeInsets+Extensions.swift in Sources */, 30EF7324252FF15F00E2C54A /* MessageLabel.swift in Sources */, + B20A907B2D05112800F44463 /* NotificationsViewController.swift in Sources */, 30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */, 3080A034277DE30100E74565 /* NSNotification+Extensions.swift in Sources */, 3080A01B277DDB8A00E74565 /* InputPlugin.swift in Sources */, diff --git a/deltachat-ios/Controller/Settings/NotificationsViewController.swift b/deltachat-ios/Controller/Settings/NotificationsViewController.swift new file mode 100644 index 000000000..a8e964cdb --- /dev/null +++ b/deltachat-ios/Controller/Settings/NotificationsViewController.swift @@ -0,0 +1,145 @@ +import UIKit +import DcCore +import Intents + +internal final class NotificationsViewController: UITableViewController { + + private struct SectionConfigs { + let headerTitle: String? + let footerTitle: String? + let cells: [UITableViewCell] + } + + private enum CellTags: Int { + case defaultTagValue = 0 + case systemSettings + } + + private var dcContext: DcContext + internal let dcAccounts: DcAccounts + + // MARK: - cells + private lazy var notificationsCell: SwitchCell = { + return SwitchCell( + textLabel: String.localized("pref_notifications"), + on: !dcContext.isMuted(), + action: { [weak self] cell in + guard let self else { return } + + dcContext.setMuted(!cell.isOn) + if cell.isOn { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + appDelegate.registerForNotifications() + } + } else { + NotificationManager.removeAllNotifications() + } + + updateCells() + NotificationManager.updateBadgeCounters() + NotificationCenter.default.post(name: Event.messagesChanged, object: nil, userInfo: ["message_id": Int(0), "chat_id": Int(0)]) + }) + }() + + private lazy var mentionsCell: SwitchCell = { + return SwitchCell( + textLabel: String.localized("pref_mention_notifications"), + on: false, // set in updateCells() + action: { [weak self] cell in + self?.dcContext.setMentionsEnabled(cell.isOn) + }) + }() + + private lazy var systemSettingsCell: UITableViewCell = { + let cell = UITableViewCell(style: .value1, reuseIdentifier: nil) + cell.tag = CellTags.systemSettings.rawValue + cell.textLabel?.text = String.localized("system_settings") + cell.accessoryType = .disclosureIndicator + return cell + }() + + private lazy var sections: [SectionConfigs] = { + let preferencesSection = SectionConfigs( + headerTitle: nil, + footerTitle: String.localized("pref_mention_notifications_explain"), + cells: [notificationsCell, mentionsCell] + ) + let systemSettingsSection = SectionConfigs( + headerTitle: nil, + footerTitle: String.localized("system_settings_notify_explain_ios"), + cells: [systemSettingsCell] + ) + return [preferencesSection, systemSettingsSection] + }() + + init(dcAccounts: DcAccounts) { + self.dcContext = dcAccounts.getSelected() + self.dcAccounts = dcAccounts + super.init(style: .insetGrouped) + hidesBottomBarWhenPushed = true + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - lifecycle + override func viewDidLoad() { + super.viewDidLoad() + title = String.localized("pref_notifications") + tableView.rowHeight = UITableView.automaticDimension + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateCells() + } + + // MARK: - UITableViewDelegate + UITableViewDatasource + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].cells.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return sections[indexPath.section].cells[indexPath.row] + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section].headerTitle + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return sections[section].footerTitle + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath), let cellTag = CellTags(rawValue: cell.tag) else { safe_fatalError(); return } + tableView.deselectRow(at: indexPath, animated: false) + + switch cellTag { + case .systemSettings: + let urlString = if #available(iOS 16, *) { + UIApplication.openNotificationSettingsURLString + } else if #available(iOS 15.4, *) { + UIApplicationOpenNotificationSettingsURLString + } else { + UIApplication.openSettingsURLString + } + + if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + case .defaultTagValue: + break + } + } + + private func updateCells() { + mentionsCell.uiSwitch.isEnabled = !dcContext.isMuted() + mentionsCell.uiSwitch.isOn = !dcContext.isMuted() && dcContext.isMentionsEnabled + } +} diff --git a/deltachat-ios/Controller/Settings/SettingsViewController.swift b/deltachat-ios/Controller/Settings/SettingsViewController.swift index cdb4fab0a..4c4d519a5 100644 --- a/deltachat-ios/Controller/Settings/SettingsViewController.swift +++ b/deltachat-ios/Controller/Settings/SettingsViewController.swift @@ -52,22 +52,14 @@ internal final class SettingsViewController: UITableViewController { return cell }() - private lazy var notificationSwitch: UISwitch = { - let switchControl = UISwitch() - switchControl.isOn = !dcContext.isMuted() - switchControl.addTarget(self, action: #selector(handleNotificationToggle(_:)), for: .valueChanged) - return switchControl - }() - private lazy var notificationCell: UITableViewCell = { - let cell = UITableViewCell(style: .default, reuseIdentifier: nil) + let cell = UITableViewCell(style: .value1, reuseIdentifier: nil) cell.tag = CellTags.notifications.rawValue cell.textLabel?.text = String.localized("pref_notifications") if #available(iOS 16.0, *) { cell.imageView?.image = UIImage(systemName: "bell") } - cell.accessoryView = notificationSwitch - cell.selectionStyle = .none + cell.accessoryType = .disclosureIndicator return cell }() @@ -220,7 +212,7 @@ internal final class SettingsViewController: UITableViewController { case .profile: showEditSettingsController() case .chatsAndMedia: showChatsAndMedia() case .addAnotherDevice: showBackupProviderViewController() - case .notifications: break + case .notifications: showNotificationsViewController() case .advanced: showAdvanced() case .help: showHelp() case .connectivity: showConnectivity() @@ -248,26 +240,12 @@ internal final class SettingsViewController: UITableViewController { } } - // MARK: - actions - @objc private func handleNotificationToggle(_ sender: UISwitch) { - dcContext.setMuted(!sender.isOn) - if sender.isOn { - if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - appDelegate.registerForNotifications() - } - } else { - NotificationManager.removeAllNotifications() - } - - NotificationManager.updateBadgeCounters() - NotificationCenter.default.post(name: Event.messagesChanged, object: nil, userInfo: ["message_id": Int(0), "chat_id": Int(0)]) - } - // MARK: - updates private func updateCells() { profileCell.updateCell(cellViewModel: ProfileViewModel(context: dcContext)) connectivityCell.detailTextLabel?.text = DcUtils.getConnectivityString(dcContext: dcContext, connectedString: String.localized("connectivity_connected")) + notificationCell.detailTextLabel?.text = String.localized(dcContext.isMuted() ? "off" : "on") } // MARK: - coordinator @@ -280,6 +258,10 @@ internal final class SettingsViewController: UITableViewController { navigationController?.pushViewController(ChatsAndMediaViewController(dcAccounts: dcAccounts), animated: true) } + private func showNotificationsViewController() { + navigationController?.pushViewController(NotificationsViewController(dcAccounts: dcAccounts), animated: true) + } + private func showBackupProviderViewController() { let alert = UIAlertController(title: String.localized("multidevice_title"), message: String.localized("multidevice_this_creates_a_qr_code"), preferredStyle: .alert) alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)) diff --git a/deltachat-ios/DC/DcContext.swift b/deltachat-ios/DC/DcContext.swift index aefbe5db8..7f39be170 100644 --- a/deltachat-ios/DC/DcContext.swift +++ b/deltachat-ios/DC/DcContext.swift @@ -552,6 +552,14 @@ public class DcContext { setConfigBool("is_muted", muted) } + public var isMentionsEnabled: Bool { + return !getConfigBool("ui.mute_mentions_if_muted") + } + + public func setMentionsEnabled(_ enabled: Bool) { + setConfigBool("ui.mute_mentions_if_muted", !enabled) + } + public func getUnreadMessages(chatId: Int) -> Int { return Int(dc_get_fresh_msg_cnt(contextPointer, UInt32(chatId))) } diff --git a/deltachat-ios/en.lproj/Localizable.strings b/deltachat-ios/en.lproj/Localizable.strings index 62eff99be..f47bbb4df 100644 --- a/deltachat-ios/en.lproj/Localizable.strings +++ b/deltachat-ios/en.lproj/Localizable.strings @@ -1073,3 +1073,5 @@ "ios_widget_apps_description" = "Shortcuts to In-Chat Utilities and Games"; "ios_remove_from_home_screen" = "Remove from Homescreen"; "ios_add_to_home_screen" = "Add to Homescreen"; +"system_settings" = "System Settings"; +"system_settings_notify_explain_ios" = "Edit type, badges, preview and more"; diff --git a/scripts/untranslated.xml b/scripts/untranslated.xml index 5f7f637d3..a21d48f29 100644 --- a/scripts/untranslated.xml +++ b/scripts/untranslated.xml @@ -15,4 +15,6 @@ Shortcuts to In-Chat Utilities and Games Remove from Homescreen Add to Homescreen + System Settings + Edit type, badges, preview and more From c3ac97e5eae78712232cd472b0344e1fa7f0038b Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Sat, 4 Jan 2025 00:08:41 +0100 Subject: [PATCH 2/4] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9960200dd..763571dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add option to toggle mention notifications as replies or reactions in groups +- Access system notification settings directly from the new notification settings - Access your favorite apps and chats from the Homescreen using our shiny new widget (#2406, #2449) - Add and remove apps and chats to that widget (#2426, #2449) - Don't show message-input when forwarding (#2435) From 44e9e9bcaa362f8454ec5c758aaccec38f7b762b Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Mon, 9 Dec 2024 19:28:20 +0100 Subject: [PATCH 3/4] apply logic for reactions and webxdc-notifications --- DcNotificationService/NotificationService.swift | 4 ++-- deltachat-ios/Helper/NotificationManager.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DcNotificationService/NotificationService.swift b/DcNotificationService/NotificationService.swift index 5c1e3b2b8..b02015643 100644 --- a/DcNotificationService/NotificationService.swift +++ b/DcNotificationService/NotificationService.swift @@ -76,7 +76,7 @@ class NotificationService: UNNotificationServiceExtension { if !dcContext.isMuted() { let msg = dcContext.getMessage(id: event.data2Int) let chat = dcContext.getChat(chatId: msg.chatId) - if !chat.isMuted { + if !chat.isMuted || (chat.isGroup && dcContext.isMentionsEnabled) { let sender = dcContext.getContact(id: event.data1Int).displayName let summary = (msg.summary(chars: 80) ?? "") bestAttemptContent.title = chat.name @@ -94,7 +94,7 @@ class NotificationService: UNNotificationServiceExtension { if !dcContext.isMuted() { let msg = dcContext.getMessage(id: event.data2Int) let chat = dcContext.getChat(chatId: msg.chatId) - if !chat.isMuted { + if !chat.isMuted || (chat.isGroup && dcContext.isMentionsEnabled) { bestAttemptContent.title = chat.name bestAttemptContent.body = msg.getWebxdcAppName() + ": " + event.data2String bestAttemptContent.userInfo["account_id"] = dcContext.id diff --git a/deltachat-ios/Helper/NotificationManager.swift b/deltachat-ios/Helper/NotificationManager.swift index 3e0f3bb92..b73376319 100644 --- a/deltachat-ios/Helper/NotificationManager.swift +++ b/deltachat-ios/Helper/NotificationManager.swift @@ -136,7 +136,7 @@ public class NotificationManager { if !eventContext.isMuted() { let msg = eventContext.getMessage(id: ui["msg_id"] as? Int ?? 0) let chat = eventContext.getChat(chatId: msg.chatId) - if !chat.isMuted { + if !chat.isMuted || (chat.isGroup && eventContext.isMentionsEnabled) { let contact = eventContext.getContact(id: ui["contact_id"] as? Int ?? 0) let summary = (msg.summary(chars: 80) ?? "") let reaction = ui["reaction"] as? String ?? "" @@ -168,7 +168,7 @@ public class NotificationManager { if !eventContext.isMuted() { let msg = eventContext.getMessage(id: ui["msg_id"] as? Int ?? 0) let chat = eventContext.getChat(chatId: msg.chatId) - if !chat.isMuted { + if !chat.isMuted || (chat.isGroup && eventContext.isMentionsEnabled) { let content = UNMutableNotificationContent() content.title = chat.name content.body = msg.getWebxdcAppName() + ": " + (ui["text"] as? String ?? "") From cb0cd07ab6651eb573181a5c6f1510de0d417a54 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Tue, 7 Jan 2025 20:59:27 +0100 Subject: [PATCH 4/4] apply logic for reply mentions --- .../NotificationService.swift | 30 ++++++++++--------- deltachat-ios/DC/DcMsg.swift | 8 +++++ .../Helper/NotificationManager.swift | 4 +-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/DcNotificationService/NotificationService.swift b/DcNotificationService/NotificationService.swift index b02015643..6f4d18dec 100644 --- a/DcNotificationService/NotificationService.swift +++ b/DcNotificationService/NotificationService.swift @@ -54,22 +54,24 @@ class NotificationService: UNNotificationServiceExtension { if event.id == DC_EVENT_INCOMING_MSG { let dcContext = dcAccounts.get(id: event.accountId) let chat = dcContext.getChat(chatId: event.data1Int) - if !dcContext.isMuted() && !chat.isMuted { + if !dcContext.isMuted() { let msg = dcContext.getMessage(id: event.data2Int) - let sender = msg.getSenderName(dcContext.getContact(id: msg.fromContactId)) - if chat.isGroup { - bestAttemptContent.title = chat.name - bestAttemptContent.body = "\(sender): " + (msg.summary(chars: 80) ?? "") - } else { - bestAttemptContent.title = sender - bestAttemptContent.body = msg.summary(chars: 80) ?? "" - } - bestAttemptContent.userInfo["account_id"] = dcContext.id - bestAttemptContent.userInfo["chat_id"] = chat.id - bestAttemptContent.userInfo["message_id"] = msg.id + if !chat.isMuted || (chat.isGroup && msg.isReplyToSelf && dcContext.isMentionsEnabled) { + let sender = msg.getSenderName(dcContext.getContact(id: msg.fromContactId)) + if chat.isGroup { + bestAttemptContent.title = chat.name + bestAttemptContent.body = "\(sender): " + (msg.summary(chars: 80) ?? "") + } else { + bestAttemptContent.title = sender + bestAttemptContent.body = msg.summary(chars: 80) ?? "" + } + bestAttemptContent.userInfo["account_id"] = dcContext.id + bestAttemptContent.userInfo["chat_id"] = chat.id + bestAttemptContent.userInfo["message_id"] = msg.id - uniqueChats["\(dcContext.id)-\(chat.id)"] = bestAttemptContent.title - messageCount += 1 + uniqueChats["\(dcContext.id)-\(chat.id)"] = bestAttemptContent.title + messageCount += 1 + } } } else if event.id == DC_EVENT_INCOMING_REACTION { let dcContext = dcAccounts.get(id: event.accountId) diff --git a/deltachat-ios/DC/DcMsg.swift b/deltachat-ios/DC/DcMsg.swift index 1ab0bc125..99460180e 100644 --- a/deltachat-ios/DC/DcMsg.swift +++ b/deltachat-ios/DC/DcMsg.swift @@ -111,6 +111,14 @@ public class DcMsg { } } + public var isReplyToSelf: Bool { + if let quoteMessage { + return quoteMessage.isFromCurrentSender + } else { + return false + } + } + public var parent: DcMsg? { guard let msgpointer = dc_msg_get_parent(messagePointer) else { return nil } return DcMsg(pointer: msgpointer) diff --git a/deltachat-ios/Helper/NotificationManager.swift b/deltachat-ios/Helper/NotificationManager.swift index b73376319..86f582cdd 100644 --- a/deltachat-ios/Helper/NotificationManager.swift +++ b/deltachat-ios/Helper/NotificationManager.swift @@ -100,9 +100,9 @@ public class NotificationManager { !eventContext.isMuted() { let chat = eventContext.getChat(chatId: chatId) + let msg = eventContext.getMessage(id: messageId) - if !chat.isMuted { - let msg = eventContext.getMessage(id: messageId) + if !chat.isMuted || (chat.isGroup && msg.isReplyToSelf && eventContext.isMentionsEnabled) { let fromContact = eventContext.getContact(id: msg.fromContactId) let sender = msg.getSenderName(fromContact) let content = UNMutableNotificationContent()