diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js --- a/keyserver/src/creators/message-creator.js +++ b/keyserver/src/creators/message-creator.js @@ -446,7 +446,7 @@ newMessageInfos: messageInfos, messageDatas, threadsToMessageIndices, - threadIDs: preUserPushInfo.notFocusedThreadIDs, + threadIDs: [...preUserPushInfo.notFocusedThreadIDs], userNotMemberOfSubthreads, fetchMessageInfoByID: (messageID: string) => fetchMessageInfoByID(viewer, messageID), @@ -458,7 +458,7 @@ newMessageInfos: messageInfos, messageDatas, threadsToMessageIndices, - threadIDs: preUserPushInfo.notFocusedThreadIDs, + threadIDs: [...preUserPushInfo.notFocusedThreadIDs], userNotMemberOfSubthreads, fetchMessageInfoByID: (messageID: string) => fetchMessageInfoByID(viewer, messageID), diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -20,7 +20,11 @@ } from 'lib/push/android-notif-creators.js'; import { apnMaxNotificationPayloadByteSize } from 'lib/push/apns-notif-creators.js'; import type { PushUserInfo, PushInfo, Device } from 'lib/push/send-utils.js'; -import { stringToVersionKey, getDevicesByPlatform } from 'lib/push/utils.js'; +import { + stringToVersionKey, + getDevicesByPlatform, + userAllowsNotif, +} from 'lib/push/utils.js'; import { type WebNotifInputData, webNotifInputDataValidator, @@ -31,8 +35,6 @@ wnsNotifInputDataValidator, createWNSNotification, } from 'lib/push/wns-notif-creators.js'; -import { oldValidUsernameRegex } from 'lib/shared/account-utils.js'; -import { isUserMentioned } from 'lib/shared/mention-utils.js'; import { createMessageInfo, shimUnsupportedRawMessageInfos, @@ -245,36 +247,24 @@ const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; - const updateBadge = threadInfo.currentUser.subscription.home; - const displayBanner = threadInfo.currentUser.subscription.pushNotifs; + const username = userInfos[userID] && userInfos[userID].username; - let resolvedUsername; - if (getENSNames) { - const userInfosWithENSNames = await getENSNames([userInfos[userID]]); - resolvedUsername = userInfosWithENSNames[0].username; - } + const { notifAllowed, badgeOnly } = await userAllowsNotif({ + subscription: { + ...threadInfo.currentUser.subscription, + role: threadInfo.currentUser.role, + }, + userID, + newMessageInfos, + userInfos, + username, + getENSNames, + }); - const userWasMentioned = - username && - threadInfo.currentUser.role && - oldValidUsernameRegex.test(username) && - newMessageInfos.some(newMessageInfo => { - const unwrappedMessageInfo = - newMessageInfo.type === messageTypes.SIDEBAR_SOURCE - ? newMessageInfo.sourceMessage - : newMessageInfo; - return ( - unwrappedMessageInfo.type === messageTypes.TEXT && - (isUserMentioned(username, unwrappedMessageInfo.text) || - (resolvedUsername && - isUserMentioned(resolvedUsername, unwrappedMessageInfo.text))) - ); - }); - if (!updateBadge && !displayBanner && !userWasMentioned) { + if (!notifAllowed) { return null; } - const badgeOnly = !displayBanner && !userWasMentioned; const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( diff --git a/lib/push/apns-notif-creators.js b/lib/push/apns-notif-creators.js --- a/lib/push/apns-notif-creators.js +++ b/lib/push/apns-notif-creators.js @@ -32,14 +32,12 @@ export type APNsNotifInputData = { ...CommonNativeNotifInputData, - +badgeOnly: boolean, +uniqueID: string, }; export const apnsNotifInputDataValidator: TInterface = tShape({ ...commonNativeNotifInputDataValidator.meta.props, - badgeOnly: t.Boolean, uniqueID: t.String, }); diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -9,6 +9,7 @@ stringToVersionKey, getDevicesByPlatform, generateNotifUserInfoPromise, + userAllowsNotif, } from './utils.js'; import { createWebNotification } from './web-notif-creators.js'; import { createWNSNotification } from './wns-notif-creators.js'; @@ -44,6 +45,7 @@ SenderDeviceDescriptor, EncryptedNotifUtilsAPI, } from '../types/notif-types.js'; +import type { ThreadSubscription } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { type GetENSNames } from '../utils/ens-helpers.js'; @@ -56,19 +58,22 @@ +cryptoID: string, }; +export type ThreadSubscriptionWithRole = $ReadOnly<{ + ...ThreadSubscription, + +role: ?string, +}>; + export type PushUserInfo = { +devices: $ReadOnlyArray, +messageInfos: RawMessageInfo[], +messageDatas: MessageData[], + +subscriptions?: { + +[threadID: string]: ThreadSubscriptionWithRole, + }, }; export type PushInfo = { +[userID: string]: PushUserInfo }; -type PushUserThreadInfo = { - +devices: $ReadOnlyArray, - +threadIDs: Set, -}; - function identityPlatformDetailsToPlatformDetails( identityPlatformDetails: IdentityPlatformDetails, ): PlatformDetails { @@ -117,7 +122,15 @@ newMessageInfos.push(rawMessageInfo); } - const pushUserThreadInfos: { [userID: string]: PushUserThreadInfo } = {}; + const pushUserThreadInfos: { + [userID: string]: { + devices: $ReadOnlyArray, + threadsWithSubscriptions: { + [threadID: string]: ThreadSubscriptionWithRole, + }, + }, + } = {}; + for (const threadID of threadsToMessageIndices.keys()) { const threadInfo = rawThreadInfos[threadID]; for (const memberInfo of threadInfo.members) { @@ -130,7 +143,8 @@ } if (pushUserThreadInfos[memberInfo.id]) { - pushUserThreadInfos[memberInfo.id].threadIDs.add(threadID); + pushUserThreadInfos[memberInfo.id].threadsWithSubscriptions[threadID] = + { ...memberInfo.subscription, role: memberInfo.role }; continue; } @@ -153,7 +167,9 @@ pushUserThreadInfos[memberInfo.id] = { devices, - threadIDs: new Set([threadID]), + threadsWithSubscriptions: { + [threadID]: { ...memberInfo.subscription, role: memberInfo.role }, + }, }; } } @@ -164,30 +180,49 @@ for (const userID in pushUserThreadInfos) { const pushUserThreadInfo = pushUserThreadInfos[userID]; - userPushInfoPromises[userID] = generateNotifUserInfoPromise({ - pushType: pushTypes.NOTIF, - devices: pushUserThreadInfo.devices, - newMessageInfos, - messageDatas, - threadsToMessageIndices, - threadIDs: pushUserThreadInfo.threadIDs, - userNotMemberOfSubthreads: new Set(), - fetchMessageInfoByID: (messageID: string) => - (async () => messageInfos[messageID])(), - userID, - }); - userRescindInfoPromises[userID] = generateNotifUserInfoPromise({ - pushType: pushTypes.RESCIND, - devices: pushUserThreadInfo.devices, - newMessageInfos, - messageDatas, - threadsToMessageIndices, - threadIDs: pushUserThreadInfo.threadIDs, - userNotMemberOfSubthreads: new Set(), - fetchMessageInfoByID: (messageID: string) => - (async () => messageInfos[messageID])(), - userID, - }); + userPushInfoPromises[userID] = (async () => { + const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise({ + pushType: pushTypes.NOTIF, + devices: pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + threadIDs: Object.keys(pushUserThreadInfo.threadsWithSubscriptions), + userNotMemberOfSubthreads: new Set(), + fetchMessageInfoByID: (messageID: string) => + (async () => messageInfos[messageID])(), + userID, + }); + if (!pushInfosWithoutSubscriptions) { + return null; + } + return { + ...pushInfosWithoutSubscriptions, + subscriptions: pushUserThreadInfo.threadsWithSubscriptions, + }; + })(); + + userRescindInfoPromises[userID] = (async () => { + const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise({ + pushType: pushTypes.RESCIND, + devices: pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + threadIDs: Object.keys(pushUserThreadInfo.threadsWithSubscriptions), + userNotMemberOfSubthreads: new Set(), + fetchMessageInfoByID: (messageID: string) => + (async () => messageInfos[messageID])(), + userID, + }); + if (!pushInfosWithoutSubscriptions) { + return null; + } + return { + ...pushInfosWithoutSubscriptions, + subscriptions: pushUserThreadInfo.threadsWithSubscriptions, + }; + })(); } const [pushInfo, rescindInfo] = await Promise.all([ @@ -205,13 +240,19 @@ rawMessageInfos: $ReadOnlyArray, userID: string, threadInfos: { +[id: string]: ThreadInfo }, + subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, + +badgeOnly: boolean, }> { + if (!subscriptions) { + return null; + } + const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); @@ -236,9 +277,25 @@ ? threadInfos[threadInfo.parentThreadID] : null; - // TODO: Using types from @Ashoat check ThreadSubscription and mentioning + const subscription = subscriptions[threadID]; + if (!subscription) { + return null; + } const username = userInfos[userID] && userInfos[userID].username; + const { notifAllowed, badgeOnly } = await userAllowsNotif({ + subscription, + userID, + newMessageInfos, + userInfos, + username, + getENSNames, + }); + + if (!notifAllowed) { + return null; + } + const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( newMessageInfos, @@ -252,7 +309,7 @@ return null; } - return { notifTexts, newRawMessageInfos }; + return { notifTexts, newRawMessageInfos, badgeOnly }; } type BuildNotifsForUserDevicesInputData = { @@ -261,6 +318,7 @@ +rawMessageInfos: $ReadOnlyArray, +userID: string, +threadInfos: { +[id: string]: ThreadInfo }, + +subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, @@ -279,6 +337,7 @@ rawMessageInfos, userID, threadInfos, + subscriptions, userInfos, getENSNames, getFCNames, @@ -289,6 +348,7 @@ rawMessageInfos, userID, threadInfos, + subscriptions, userInfos, getENSNames, getFCNames, @@ -298,7 +358,8 @@ return null; } - const { notifTexts, newRawMessageInfos } = notifTextWithNewRawMessageInfos; + const { notifTexts, newRawMessageInfos, badgeOnly } = + notifTextWithNewRawMessageInfos; const [{ threadID }] = newRawMessageInfos; const promises: Array< @@ -330,7 +391,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, - badgeOnly: false, + badgeOnly, unreadCount: undefined, platformDetails, uniqueID: uuidv4(), @@ -371,7 +432,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, - badgeOnly: false, + badgeOnly, unreadCount: undefined, platformDetails, notifID: uuidv4(), @@ -414,7 +475,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, - badgeOnly: false, + badgeOnly, unreadCount: undefined, platformDetails, uniqueID: uuidv4(), @@ -571,6 +632,7 @@ rawMessageInfos: [rawMessageInfos], userID, threadInfos, + subscriptions: pushInfo[userID].subscriptions, userInfos, getENSNames, getFCNames, diff --git a/lib/push/utils.js b/lib/push/utils.js --- a/lib/push/utils.js +++ b/lib/push/utils.js @@ -2,15 +2,21 @@ import invariant from 'invariant'; -import type { Device } from './send-utils.js'; +import type { Device, ThreadSubscriptionWithRole } from './send-utils.js'; +import { oldValidUsernameRegex } from '../shared/account-utils.js'; +import { isUserMentioned } from '../shared/mention-utils.js'; import { type PushType } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import type { Platform } from '../types/device-types.js'; -import { - type MessageData, - type RawMessageInfo, +import { messageTypes } from '../types/message-types-enum.js'; +import type { + MessageData, + RawMessageInfo, + MessageInfo, } from '../types/message-types.js'; import type { NotificationTargetDevice } from '../types/notif-types.js'; +import type { GlobalUserInfo, UserInfo } from '../types/user-types.js'; +import type { GetENSNames } from '../utils/ens-helpers.js'; export type VersionKey = { +codeVersion: number, @@ -89,7 +95,7 @@ +newMessageInfos: $ReadOnlyArray, +messageDatas: $ReadOnlyArray, +threadsToMessageIndices: $ReadOnlyMap, - +threadIDs: $ReadOnlySet, + +threadIDs: $ReadOnlyArray, +userNotMemberOfSubthreads: Set, +fetchMessageInfoByID: (messageID: string) => Promise, +userID: string, @@ -175,9 +181,63 @@ }; } +export type UserAllowsNotifInputData = { + +subscription: ThreadSubscriptionWithRole, + +userID: string, + +newMessageInfos: $ReadOnlyArray, + +userInfos: { +[string]: UserInfo | GlobalUserInfo }, + +username: ?string, + +getENSNames: ?GetENSNames, +}; + +async function userAllowsNotif(inputData: UserAllowsNotifInputData): Promise<{ + +notifAllowed: boolean, + +badgeOnly: boolean, +}> { + const { + subscription, + userID, + newMessageInfos, + userInfos, + username, + getENSNames, + } = inputData; + const updateBadge = subscription.home; + const displayBanner = subscription.pushNotifs; + + let resolvedUsername; + if (getENSNames) { + const userInfosWithENSNames = await getENSNames([userInfos[userID]]); + resolvedUsername = userInfosWithENSNames[0].username; + } + + const userWasMentioned = + username && + subscription.role && + oldValidUsernameRegex.test(username) && + newMessageInfos.some(newMessageInfo => { + const unwrappedMessageInfo = + newMessageInfo.type === messageTypes.SIDEBAR_SOURCE + ? newMessageInfo.sourceMessage + : newMessageInfo; + return ( + unwrappedMessageInfo.type === messageTypes.TEXT && + (isUserMentioned(username, unwrappedMessageInfo.text) || + (resolvedUsername && + isUserMentioned(resolvedUsername, unwrappedMessageInfo.text))) + ); + }); + + const notifAllowed = !!updateBadge || !!displayBanner || !!userWasMentioned; + const badgeOnly = !displayBanner && !userWasMentioned; + + return { notifAllowed, badgeOnly }; +} + export { stringToVersionKey, versionKeyToString, getDevicesByPlatform, generateNotifUserInfoPromise, + userAllowsNotif, }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -92,7 +92,7 @@ +prefix?: string, +threadID: string, +collapseID?: string, - +badgeOnly?: '0', + +badgeOnly?: '0' | '1', +encryptionFailed?: '1', }>;