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 @@ messageInfos, messageDatas, threadsToMessageIndices, - preUserPushInfo.notFocusedThreadIDs, + [...preUserPushInfo.notFocusedThreadIDs], userNotMemberOfSubthreads, (messageID: string) => fetchMessageInfoByID(viewer, messageID), userID, @@ -457,7 +457,7 @@ messageInfos, messageDatas, threadsToMessageIndices, - preUserPushInfo.notFocusedThreadIDs, + [...preUserPushInfo.notFocusedThreadIDs], userNotMemberOfSubthreads, (messageID: string) => fetchMessageInfoByID(viewer, messageID), userID, 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 } 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( + { + ...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( @@ -369,6 +359,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID: threadInfo.id, collapseKey: notifInfo.collapseKey, + badgeOnly, unreadCount, platformDetails, notifID: dbID, diff --git a/lib/push/android-notif-creators.js b/lib/push/android-notif-creators.js --- a/lib/push/android-notif-creators.js +++ b/lib/push/android-notif-creators.js @@ -38,6 +38,7 @@ +newRawMessageInfos: RawMessageInfo[], +threadID: string, +collapseKey: ?string, + +badgeOnly: boolean, +unreadCount?: number, +platformDetails: PlatformDetails, }>; @@ -49,6 +50,7 @@ newRawMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, collapseKey: t.maybe(t.String), + badgeOnly: t.Boolean, unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); @@ -75,6 +77,7 @@ newRawMessageInfos, threadID, collapseKey, + badgeOnly, unreadCount, platformDetails, notifID, @@ -128,7 +131,7 @@ notification.data = { ...notification.data, id, - badgeOnly: '0', + badgeOnly: badgeOnly ? '1' : '0', }; const messageInfos = JSON.stringify(newRawMessageInfos); 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,17 +58,27 @@ +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, + +threadsWithSubscriptions: { + [threadID: string]: ThreadSubscriptionWithRole, + }, }; function identityPlatformDetailsToPlatformDetails( @@ -123,13 +135,15 @@ for (const memberInfo of threadInfo.members) { if ( !isMemberActive(memberInfo) || - !hasPermission(memberInfo.permissions, 'visible') + !hasPermission(memberInfo.permissions, 'visible') || + !memberInfo.subscription ) { continue; } if (pushUserThreadInfos[memberInfo.id]) { - pushUserThreadInfos[memberInfo.id].threadIDs.add(threadID); + pushUserThreadInfos[memberInfo.id].threadsWithSubscriptions[threadID] = + { ...memberInfo.subscription, role: memberInfo.role }; continue; } @@ -152,7 +166,9 @@ pushUserThreadInfos[memberInfo.id] = { devices, - threadIDs: new Set([threadID]), + threadsWithSubscriptions: { + [threadID]: { ...memberInfo.subscription, role: memberInfo.role }, + }, }; } } @@ -163,28 +179,47 @@ for (const userID in pushUserThreadInfos) { const pushUserThreadInfo = pushUserThreadInfos[userID]; - userPushInfoPromises[userID] = generateNotifUserInfoPromise( - pushTypes.NOTIF, - pushUserThreadInfo.devices, - newMessageInfos, - messageDatas, - threadsToMessageIndices, - pushUserThreadInfo.threadIDs, - new Set(), - (messageID: string) => (async () => messageInfos[messageID])(), - userID, - ); - userRescindInfoPromises[userID] = generateNotifUserInfoPromise( - pushTypes.RESCIND, - pushUserThreadInfo.devices, - newMessageInfos, - messageDatas, - threadsToMessageIndices, - pushUserThreadInfo.threadIDs, - new Set(), - (messageID: string) => (async () => messageInfos[messageID])(), - userID, - ); + userPushInfoPromises[userID] = (async () => { + const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise( + pushTypes.NOTIF, + pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + Object.keys(pushUserThreadInfo.threadsWithSubscriptions), + new Set(), + (messageID: string) => (async () => messageInfos[messageID])(), + userID, + ); + if (!pushInfosWithoutSubscriptions) { + return null; + } + return { + ...pushInfosWithoutSubscriptions, + subscriptions: pushUserThreadInfo.threadsWithSubscriptions, + }; + })(); + + userRescindInfoPromises[userID] = (async () => { + const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise( + pushTypes.RESCIND, + pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + Object.keys(pushUserThreadInfo.threadsWithSubscriptions), + new Set(), + (messageID: string) => (async () => messageInfos[messageID])(), + userID, + ); + if (!pushInfosWithoutSubscriptions) { + return null; + } + return { + ...pushInfosWithoutSubscriptions, + subscriptions: pushUserThreadInfo.threadsWithSubscriptions, + }; + })(); } const [pushInfo, rescindInfo] = await Promise.all([ @@ -202,13 +237,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); @@ -233,9 +274,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, @@ -249,7 +306,7 @@ return null; } - return { notifTexts, newRawMessageInfos }; + return { notifTexts, newRawMessageInfos, badgeOnly }; } async function buildNotifsForUserDevices( @@ -258,6 +315,7 @@ rawMessageInfos: $ReadOnlyArray, userID: string, threadInfos: { +[id: string]: ThreadInfo }, + subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, @@ -270,6 +328,7 @@ rawMessageInfos, userID, threadInfos, + subscriptions, userInfos, getENSNames, getFCNames, @@ -279,7 +338,8 @@ return null; } - const { notifTexts, newRawMessageInfos } = notifTextWithNewRawMessageInfos; + const { notifTexts, newRawMessageInfos, badgeOnly } = + notifTextWithNewRawMessageInfos; const [{ threadID }] = newRawMessageInfos; const promises: Array< @@ -311,7 +371,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, - badgeOnly: false, + badgeOnly, unreadCount: undefined, platformDetails, uniqueID: uuidv4(), @@ -352,6 +412,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, + badgeOnly, unreadCount: undefined, platformDetails, notifID: uuidv4(), @@ -394,7 +455,7 @@ newRawMessageInfos: shimmedNewRawMessageInfos, threadID, collapseKey: undefined, - badgeOnly: false, + badgeOnly, unreadCount: undefined, platformDetails, uniqueID: uuidv4(), @@ -534,6 +595,7 @@ [rawMessageInfos], userID, threadInfos, + 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 @@ -1,15 +1,21 @@ // @flow 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, @@ -88,7 +94,7 @@ newMessageInfos: $ReadOnlyArray, messageDatas: $ReadOnlyArray, threadsToMessageIndices: $ReadOnlyMap, - threadIDs: $ReadOnlySet, + threadIDs: $ReadOnlyArray, userNotMemberOfSubthreads: Set, fetchMessageInfoByID: (messageID: string) => Promise, userID: string, @@ -158,9 +164,53 @@ }; } +async function userAllowsNotif( + subscription: ThreadSubscriptionWithRole, + userID: string, + newMessageInfos: $ReadOnlyArray, + userInfos: { +[string]: UserInfo | GlobalUserInfo }, + username: ?string, + getENSNames: ?GetENSNames, +): Promise<{ + +notifAllowed: boolean, + +badgeOnly: boolean, +}> { + 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/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -45,6 +45,7 @@ MemberInfoWithPermissions, RoleInfo, ThreadInfo, + MinimallyEncodedThickMemberInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { decodeMinimallyEncodedRoleInfo, @@ -223,7 +224,9 @@ ); } -function isMemberActive(memberInfo: MemberInfoWithPermissions): boolean { +function isMemberActive( + memberInfo: MemberInfoWithPermissions | MinimallyEncodedThickMemberInfo, +): boolean { const role = memberInfo.role; return role !== null && role !== undefined; } 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', }>;