core, iOS: hidden and muted user profiles (#2025)

* core, ios: profile privacy design

* migration

* core: user profile privacy

* update nix dependencies

* update simplexmq

* import stateTVar

* update core library

* update UI

* update hide/show user profile

* update API, UI, fix test

* update api, UI, test

* update api call

* fix api

* update UI for hidden profiles

* filter notifications on hidden/muted profiles when inactive, alerts

* updates

* update schema, test, icon
This commit is contained in:
Evgeny Poberezkin
2023-03-22 15:58:01 +00:00
committed by GitHub
parent bcdf502ce6
commit 06a0dbd0f2
29 changed files with 1067 additions and 228 deletions

View File

@@ -16,7 +16,7 @@ let logger = Logger()
let suspendingDelay: UInt64 = 2_000_000_000
typealias NtfStream = AsyncStream<UNMutableNotificationContent>
typealias NtfStream = AsyncStream<NSENotification>
actor PendingNtfs {
static let shared = PendingNtfs()
@@ -33,13 +33,13 @@ actor PendingNtfs {
}
}
func readStream(_ id: String, for nse: NotificationService, msgCount: Int = 1) async {
func readStream(_ id: String, for nse: NotificationService, msgCount: Int = 1, showNotifications: Bool) async {
logger.debug("PendingNtfs.readStream: \(id, privacy: .public) \(msgCount, privacy: .public)")
if let s = ntfStreams[id] {
logger.debug("PendingNtfs.readStream: has stream")
var rcvCount = max(1, msgCount)
for await ntf in s {
nse.setBestAttemptNtf(ntf)
nse.setBestAttemptNtf(showNotifications ? ntf : .empty)
rcvCount -= 1
if rcvCount == 0 || ntf.categoryIdentifier == ntfCategoryCallInvitation { break }
}
@@ -47,7 +47,7 @@ actor PendingNtfs {
}
}
func writeStream(_ id: String, _ ntf: UNMutableNotificationContent) {
func writeStream(_ id: String, _ ntf: NSENotification) {
logger.debug("PendingNtfs.writeStream: \(id, privacy: .public)")
if let cont = ntfConts[id] {
logger.debug("PendingNtfs.writeStream: writing ntf")
@@ -56,16 +56,30 @@ actor PendingNtfs {
}
}
enum NSENotification {
case nse(notification: UNMutableNotificationContent)
case callkit(invitation: RcvCallInvitation)
case empty
var categoryIdentifier: String? {
switch self {
case let .nse(ntf): return ntf.categoryIdentifier
case .callkit: return ntfCategoryCallInvitation
case .empty: return nil
}
}
}
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptNtf: UNMutableNotificationContent?
var bestAttemptNtf: NSENotification?
var badgeCount: Int = 0
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("NotificationService.didReceive")
setBestAttemptNtf(request.content.mutableCopy() as? UNMutableNotificationContent)
if let ntf = request.content.mutableCopy() as? UNMutableNotificationContent {
setBestAttemptNtf(ntf)
}
self.contentHandler = contentHandler
registerGroupDefaults()
let appState = appStateGroupDefault.get()
@@ -112,12 +126,16 @@ class NotificationService: UNNotificationServiceExtension {
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
if let connEntity = ntfMsgInfo.connEntity {
setBestAttemptNtf(createConnectionEventNtf(ntfMsgInfo.user, connEntity))
setBestAttemptNtf(
ntfMsgInfo.user.showNotifications
? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity))
: .empty
)
if let id = connEntity.id {
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count)
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count, showNotifications: ntfMsgInfo.user.showNotifications)
deliverBestAttemptNtf()
}
}
@@ -140,16 +158,40 @@ class NotificationService: UNNotificationServiceExtension {
ntfBadgeCountGroupDefault.set(badgeCount)
}
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent?) {
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
setBestAttemptNtf(.nse(notification: ntf))
}
func setBestAttemptNtf(_ ntf: NSENotification) {
logger.debug("NotificationService.setBestAttemptNtf")
bestAttemptNtf = ntf
bestAttemptNtf?.badge = badgeCount as NSNumber
if case let .nse(notification) = ntf {
notification.badge = badgeCount as NSNumber
bestAttemptNtf = .nse(notification: notification)
} else {
bestAttemptNtf = ntf
}
}
private func deliverBestAttemptNtf() {
logger.debug("NotificationService.deliverBestAttemptNtf")
if let handler = contentHandler, let content = bestAttemptNtf {
handler(content)
if let handler = contentHandler, let ntf = bestAttemptNtf {
switch ntf {
case let .nse(content): handler(content)
case let .callkit(invitation):
CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName,
"contactId": invitation.contact.id,
"media": invitation.callType.media.rawValue
]) { error in
if error == nil {
handler(UNMutableNotificationContent())
} else {
logger.debug("reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
handler(createCallInvitationNtf(invitation))
}
}
case .empty: handler(UNMutableNotificationContent())
}
bestAttemptNtf = nil
}
}
@@ -211,15 +253,15 @@ func chatRecvMsg() async -> ChatResponse? {
private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
switch res {
case let .contactConnected(user, contact, _):
return (contact.id, createContactConnectedNtf(user, contact))
return (contact.id, .nse(notification: createContactConnectedNtf(user, contact)))
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, createContactRequestNtf(user, contactRequest))
return (UserContact(contactRequest: contactRequest).id, .nse(notification: createContactRequestNtf(user, contactRequest)))
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
var cItem = aChatItem.chatItem
@@ -240,23 +282,13 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
}
}
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(user, cInfo, cItem)) : nil
return cItem.showMutableNotification ? (aChatItem.chatId, .nse(notification: createMessageReceivedNtf(user, cInfo, cItem))) : nil
case let .callInvitation(invitation):
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
if useCallKit() {
do {
try await CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName,
"contactId": invitation.contact.id,
"media": invitation.callType.media.rawValue
])
logger.debug("reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
return (invitation.contact.id, (UNNotificationContent().mutableCopy() as! UNMutableNotificationContent))
} catch let error {
logger.error("reportNewIncomingVoIPPushPayload error \(String(describing: error), privacy: .public)")
}
}
return (invitation.contact.id, createCallInvitationNtf(invitation))
return (
invitation.contact.id,
useCallKit() ? .callkit(invitation: invitation) : .nse(notification: createCallInvitationNtf(invitation))
)
default:
logger.debug("NotificationService processReceivedMsg ignored event: \(res.responseType)")
return nil