From a65d6e0697bc30c241ea2c6a07fde749b1759a54 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Fri, 21 Mar 2025 14:02:16 +0400 Subject: [PATCH] Notification: Fix sound playback conditions (#5734) --- src/api/gramjs/apiBuilders/chats.ts | 14 +-- src/api/gramjs/apiBuilders/messages.ts | 1 + src/api/gramjs/apiBuilders/misc.ts | 38 ++------ src/api/gramjs/methods/chats.ts | 59 +++++++----- src/api/gramjs/methods/messages.ts | 18 +++- src/api/gramjs/methods/settings.ts | 74 ++++++--------- src/api/gramjs/scheduleUnmute.ts | 28 +++--- src/api/gramjs/updates/mtpUpdateHandler.ts | 68 +++++++------- src/api/types/chats.ts | 7 +- src/api/types/misc.ts | 16 ++-- src/api/types/updates.ts | 35 ++++--- src/components/common/profile/ChatExtra.tsx | 10 +- src/components/left/MuteChatModal.tsx | 10 +- src/components/left/main/Chat.tsx | 8 +- src/components/left/main/ChatBadge.tsx | 40 +++++--- src/components/left/main/Topic.tsx | 17 +++- .../left/main/hooks/useTopicContextActions.ts | 8 +- .../left/search/LeftSearchResultChat.tsx | 10 +- .../left/settings/SettingsNotifications.tsx | 91 ++++++++----------- .../middle/FloatingActionButtons.tsx | 19 +++- src/components/middle/HeaderMenuContainer.tsx | 8 +- .../middle/hooks/useMessageObservers.ts | 2 +- .../middle/message/ActionMessage.tsx | 4 +- src/components/middle/message/Message.tsx | 4 +- .../right/management/ManageUser.tsx | 9 +- src/global/actions/api/chats.ts | 73 ++++++++++----- src/global/actions/api/messages.ts | 26 +++--- src/global/actions/api/reactions.ts | 21 +++-- src/global/actions/api/settings.ts | 43 ++++++--- src/global/actions/apiUpdaters/settings.ts | 24 ++--- src/global/actions/ui/initial.ts | 7 +- src/global/cache.ts | 6 +- src/global/helpers/chats.ts | 36 +------- src/global/helpers/misc.ts | 6 +- src/global/helpers/notifications.ts | 65 +++++++++++++ src/global/initialState.ts | 2 +- src/global/reducers/settings.ts | 75 ++++++++------- src/global/selectors/settings.ts | 14 ++- src/global/types/actions.ts | 25 +++-- src/global/types/globalState.ts | 6 +- src/hooks/useChatContextActions.ts | 7 +- src/types/index.ts | 25 +---- src/util/folderManager.ts | 40 ++++---- src/util/notifications.tsx | 58 +++++------- 44 files changed, 620 insertions(+), 537 deletions(-) create mode 100644 src/global/helpers/notifications.ts diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 971b673ff..678066d95 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -24,13 +24,14 @@ import type { } from '../../types'; import { pick, pickTruthy } from '../../../util/iteratees'; -import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; +import { getServerTimeOffset } from '../../../util/serverTime'; import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { serializeBytes } from '../helpers/misc'; import { buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, } from './common'; import { omitVirtualClassFields } from './helpers'; +import { buildApiPeerNotifySettings } from './misc'; import { buildApiEmojiStatus, buildApiPeerColor, @@ -119,10 +120,8 @@ export function buildApiChatFromDialog( ): ApiChat { const { peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount, - notifySettings: { silent, muteUntil }, readOutboxMaxId, readInboxMaxId, draft, viewForumAsMessages, } = dialog; - const isMuted = silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil); return { id: getApiChatIdFromMtpPeer(peer), @@ -134,8 +133,6 @@ export function buildApiChatFromDialog( unreadCount, unreadMentionsCount, unreadReactionsCount, - isMuted, - muteUntil, ...(unreadMark && { hasUnreadMark: true }), ...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }), ...(viewForumAsMessages && { isForumAsMessages: true }), @@ -579,9 +576,7 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und unreadMentionsCount, unreadReactionsCount, fromId, - notifySettings: { - silent, muteUntil, - }, + notifySettings, } = forumTopic; return { @@ -600,8 +595,7 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und unreadMentionsCount, unreadReactionsCount, fromId: getApiChatIdFromMtpPeer(fromId), - isMuted: silent || (typeof muteUntil === 'number' ? getServerTime() < muteUntil : undefined), - muteUntil, + notifySettings: buildApiPeerNotifySettings(notifySettings), }; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 559304652..be750d50e 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -163,6 +163,7 @@ export function buildApiMessageFromNotification( chatId: SERVICE_NOTIFICATIONS_USER_ID, date: notification.inboxDate || currentDate, content, + isInvertedMedia: notification.invertMedia, isOutgoing: false, }; } diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 686ec064c..5d13e786c 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -8,6 +8,7 @@ import type { ApiLanguage, ApiOldLangString, ApiPeerColors, + ApiPeerNotifySettings, ApiPrivacyKey, ApiSession, ApiTimezone, @@ -21,7 +22,6 @@ import { numberToHexColor } from '../../../util/colors'; import { buildCollectionByCallback, omit, omitUndefined, pick, } from '../../../util/iteratees'; -import { getServerTime } from '../../../util/serverTime'; import { addUserToLocalDb } from '../helpers/localDb'; import { omitVirtualClassFields } from './helpers'; import { buildApiDocument, buildMessageTextContent } from './messageContent'; @@ -106,40 +106,20 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und return undefined; } -export function buildApiNotifyException( - notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer, -) { +export function buildApiPeerNotifySettings( + notifySettings: GramJs.TypePeerNotifySettings, +): ApiPeerNotifySettings { const { silent, muteUntil, showPreviews, otherSound, } = notifySettings; - const hasSound = Boolean(otherSound && !(otherSound instanceof GramJs.NotificationSoundNone)); + const hasSound = !(otherSound instanceof GramJs.NotificationSoundNone); return { - chatId: getApiChatIdFromMtpPeer(peer), - isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), - ...(!hasSound && { isSilent: true }), - ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), - muteUntil, - }; -} - -export function buildApiNotifyExceptionTopic( - notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer, topicId: number, -) { - const { - silent, muteUntil, showPreviews, otherSound, - } = notifySettings; - - const hasSound = Boolean(otherSound && !(otherSound instanceof GramJs.NotificationSoundNone)); - - return { - chatId: getApiChatIdFromMtpPeer(peer), - topicId, - isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), - ...(!hasSound && { isSilent: true }), - ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), - muteUntil, + hasSound, + isSilentPosting: silent, + mutedUntil: muteUntil, + shouldShowPreviews: showPreviews, }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 6020ed85c..0e5a0a8e0 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -14,6 +14,7 @@ import type { ApiMessage, ApiMissingInvitedUser, ApiPeer, + ApiPeerNotifySettings, ApiPhoto, ApiTopic, ApiUser, @@ -31,7 +32,7 @@ import { SERVICE_NOTIFICATIONS_USER_ID, TOPICS_SLICE, } from '../../../config'; -import { buildCollectionByKey } from '../../../util/iteratees'; +import { buildCollectionByKey, omitUndefined } from '../../../util/iteratees'; import { buildApiChatBotCommands, buildApiChatFolder, @@ -52,6 +53,7 @@ import { } from '../apiBuilders/chats'; import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; +import { buildApiPeerNotifySettings } from '../apiBuilders/misc'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildStickerSet } from '../apiBuilders/symbols'; import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users'; @@ -96,6 +98,7 @@ type ChatListData = { orderedPinnedIds: string[] | undefined; totalChatCount: number; messages: ApiMessage[]; + notifyExceptionById: Record; lastMessageByChatId: Record; nextOffsetId?: number; nextOffsetPeerId?: string; @@ -150,6 +153,7 @@ export async function fetchChats({ const chats: ApiChat[] = []; const draftsById: Record = {}; + const notifyExceptionById: Record = {}; const dialogs = (resultPinned?.dialogs || []).concat(result.dialogs); @@ -186,7 +190,12 @@ export async function fetchChats({ chats.push(chat); - scheduleMutedChatUpdate(chat.id, chat.muteUntil, sendApiUpdate); + const notifySettings = buildApiPeerNotifySettings(dialog.notifySettings); + if (Object.values(omitUndefined(notifySettings)).length) { + notifyExceptionById[chat.id] = notifySettings; + + scheduleMutedChatUpdate(chat.id, notifySettings.mutedUntil, sendApiUpdate); + } if (withPinned && dialog.pinned) { orderedPinnedIds.push(chat.id); @@ -229,6 +238,7 @@ export async function fetchChats({ totalChatCount, lastMessageByChatId, messages, + notifyExceptionById, nextOffsetId, nextOffsetPeerId, nextOffsetDate, @@ -327,6 +337,7 @@ export async function fetchSavedChats({ lastMessageByChatId, messages, draftsById: {}, + notifyExceptionById: {}, nextOffsetId, nextOffsetPeerId, nextOffsetDate, @@ -474,7 +485,9 @@ export async function requestChatUpdate({ applyState(result.state); - scheduleMutedChatUpdate(chatUpdate.id, chatUpdate.muteUntil, sendApiUpdate); + const notifySettings = buildApiPeerNotifySettings(dialog.notifySettings); + + scheduleMutedChatUpdate(chatUpdate.id, notifySettings.mutedUntil, sendApiUpdate); } export function saveDraft({ @@ -722,25 +735,27 @@ async function getFullChannelInfo( }; } -export async function updateChatMutedState({ - chat, isMuted, muteUntil = 0, +export function updateChatMutedState({ + chat, isMuted, mutedUntil = 0, }: { - chat: ApiChat; isMuted: boolean; muteUntil?: number; + chat: ApiChat; isMuted?: boolean; mutedUntil?: number; }) { - if (isMuted && !muteUntil) { - muteUntil = MAX_INT_32; + if (isMuted && !mutedUntil) { + mutedUntil = MAX_INT_32; } - await invokeRequest(new GramJs.account.UpdateNotifySettings({ + invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyPeer({ peer: buildInputPeer(chat.id, chat.accessHash), }), - settings: new GramJs.InputPeerNotifySettings({ muteUntil }), + settings: new GramJs.InputPeerNotifySettings({ muteUntil: mutedUntil }), })); sendApiUpdate({ - '@type': 'updateNotifyExceptions', + '@type': 'updateChatNotifySettings', chatId: chat.id, - isMuted, + settings: { + mutedUntil, + }, }); void requestChatUpdate({ @@ -749,27 +764,29 @@ export async function updateChatMutedState({ }); } -export async function updateTopicMutedState({ - chat, topicId, isMuted, muteUntil = 0, +export function updateTopicMutedState({ + chat, topicId, isMuted, mutedUntil = 0, }: { - chat: ApiChat; topicId: number; isMuted: boolean; muteUntil?: number; + chat: ApiChat; topicId: number; isMuted?: boolean; mutedUntil?: number; }) { - if (isMuted && !muteUntil) { - muteUntil = MAX_INT_32; + if (isMuted && !mutedUntil) { + mutedUntil = MAX_INT_32; } - await invokeRequest(new GramJs.account.UpdateNotifySettings({ + invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyForumTopic({ peer: buildInputPeer(chat.id, chat.accessHash), topMsgId: topicId, }), - settings: new GramJs.InputPeerNotifySettings({ muteUntil }), + settings: new GramJs.InputPeerNotifySettings({ muteUntil: mutedUntil }), })); sendApiUpdate({ - '@type': 'updateTopicNotifyExceptions', + '@type': 'updateTopicNotifySettings', chatId: chat.id, topicId, - isMuted, + settings: { + mutedUntil, + }, }); // TODO[forums] Request forum topic thread update diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index d25f272f3..ff3e1d290 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1834,11 +1834,14 @@ export async function reportSponsoredMessage({ export async function readAllMentions({ chat, + threadId, }: { chat: ApiChat; + threadId?: ThreadId; }) { const result = await invokeRequest(new GramJs.messages.ReadMentions({ peer: buildInputPeer(chat.id, chat.accessHash), + topMsgId: threadId ? Number(threadId) : undefined, })); if (!result) return; @@ -1846,17 +1849,20 @@ export async function readAllMentions({ processAffectedHistory(chat, result); if (result.offset) { - await readAllMentions({ chat }); + await readAllMentions({ chat, threadId }); } } export async function readAllReactions({ chat, + threadId, }: { chat: ApiChat; + threadId?: ThreadId; }) { const result = await invokeRequest(new GramJs.messages.ReadReactions({ peer: buildInputPeer(chat.id, chat.accessHash), + topMsgId: threadId ? Number(threadId) : undefined, })); if (!result) return; @@ -1864,14 +1870,15 @@ export async function readAllReactions({ processAffectedHistory(chat, result); if (result.offset) { - await readAllReactions({ chat }); + await readAllReactions({ chat, threadId }); } } export async function fetchUnreadMentions({ - chat, ...pagination + chat, threadId, ...pagination }: { chat: ApiChat; + threadId?: ThreadId; offsetId?: number; addOffset?: number; maxId?: number; @@ -1879,6 +1886,7 @@ export async function fetchUnreadMentions({ }) { const result = await invokeRequest(new GramJs.messages.GetUnreadMentions({ peer: buildInputPeer(chat.id, chat.accessHash), + topMsgId: threadId ? Number(threadId) : undefined, limit: MENTION_UNREAD_SLICE, ...pagination, })); @@ -1899,9 +1907,10 @@ export async function fetchUnreadMentions({ } export async function fetchUnreadReactions({ - chat, ...pagination + chat, threadId, ...pagination }: { chat: ApiChat; + threadId?: ThreadId; offsetId?: number; addOffset?: number; maxId?: number; @@ -1909,6 +1918,7 @@ export async function fetchUnreadReactions({ }) { const result = await invokeRequest(new GramJs.messages.GetUnreadReactions({ peer: buildInputPeer(chat.id, chat.accessHash), + topMsgId: threadId ? Number(threadId) : undefined, limit: REACTION_UNREAD_SLICE, ...pagination, })); diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 7d7b99c0e..e721baaef 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -8,7 +8,8 @@ import type { ApiConfig, ApiInputPrivacyRules, ApiLanguage, - ApiNotifyException, + ApiNotifyPeerType, + ApiPeerNotifySettings, ApiPhoto, ApiPrivacyKey, ApiUser, @@ -21,15 +22,14 @@ import { MAX_INT_32, } from '../../../config'; import { buildCollectionByKey } from '../../../util/iteratees'; -import { getServerTime } from '../../../util/serverTime'; import { buildAppConfig } from '../apiBuilders/appConfig'; import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common'; import { buildApiConfig, buildApiCountryList, buildApiLanguage, - buildApiNotifyException, buildApiPeerColors, + buildApiPeerNotifySettings, buildApiSession, buildApiTimezone, buildApiWallpaper, @@ -323,20 +323,26 @@ export async function fetchNotificationExceptions() { return acc; } - acc.push(buildApiNotifyException(update.notifySettings, update.peer.peer)); + const peerId = getApiChatIdFromMtpPeer(update.peer.peer); + + acc[peerId] = buildApiPeerNotifySettings(update.notifySettings); return acc; - }, [] as ApiNotifyException[]); + }, {} as Record); } -export async function fetchNotificationSettings() { +export async function fetchContactSignUpSetting() { + const hasContactJoinedNotifications = await invokeRequest(new GramJs.account.GetContactSignUpNotification()); + + return hasContactJoinedNotifications; +} + +export async function fetchNotifyDefaultSettings() { const [ - isMutedContactSignUpNotification, - privateContactNotificationsSettings, - groupNotificationsSettings, - broadcastNotificationsSettings, + usersSettings, + groupsSettings, + channelsSettings, ] = await Promise.all([ - invokeRequest(new GramJs.account.GetContactSignUpNotification()), invokeRequest(new GramJs.account.GetNotifySettings({ peer: new GramJs.InputNotifyUsers(), })), @@ -348,37 +354,14 @@ export async function fetchNotificationSettings() { })), ]); - if (!privateContactNotificationsSettings || !groupNotificationsSettings || !broadcastNotificationsSettings) { - return false; + if (!usersSettings || !groupsSettings || !channelsSettings) { + return undefined; } - const { - silent: privateSilent, muteUntil: privateMuteUntil, showPreviews: privateShowPreviews, - } = privateContactNotificationsSettings; - const { - silent: groupSilent, muteUntil: groupMuteUntil, showPreviews: groupShowPreviews, - } = groupNotificationsSettings; - const { - silent: broadcastSilent, muteUntil: broadcastMuteUntil, showPreviews: broadcastShowPreviews, - } = broadcastNotificationsSettings; - return { - hasContactJoinedNotifications: !isMutedContactSignUpNotification, - hasPrivateChatsNotifications: !( - privateSilent - || (typeof privateMuteUntil === 'number' && getServerTime() < privateMuteUntil) - ), - hasPrivateChatsMessagePreview: privateShowPreviews, - hasGroupNotifications: !( - groupSilent || (typeof groupMuteUntil === 'number' - && getServerTime() < groupMuteUntil) - ), - hasGroupMessagePreview: groupShowPreviews, - hasBroadcastNotifications: !( - broadcastSilent || (typeof broadcastMuteUntil === 'number' - && getServerTime() < broadcastMuteUntil) - ), - hasBroadcastMessagePreview: broadcastShowPreviews, + users: buildApiPeerNotifySettings(usersSettings), + groups: buildApiPeerNotifySettings(groupsSettings), + channels: buildApiPeerNotifySettings(channelsSettings), }; } @@ -386,17 +369,17 @@ export function updateContactSignUpNotification(isSilent: boolean) { return invokeRequest(new GramJs.account.SetContactSignUpNotification({ silent: isSilent })); } -export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broadcast', { - isSilent, +export function updateNotificationSettings(peerType: ApiNotifyPeerType, { + isMuted, shouldShowPreviews, }: { - isSilent?: boolean; + isMuted?: boolean; shouldShowPreviews?: boolean; }) { let peer: GramJs.TypeInputNotifyPeer; - if (peerType === 'contact') { + if (peerType === 'users') { peer = new GramJs.InputNotifyUsers(); - } else if (peerType === 'group') { + } else if (peerType === 'groups') { peer = new GramJs.InputNotifyChats(); } else { peer = new GramJs.InputNotifyBroadcasts(); @@ -404,8 +387,7 @@ export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broa const settings = { showPreviews: shouldShowPreviews, - silent: isSilent, - muteUntil: isSilent ? MAX_INT_32 : 0, + muteUntil: isMuted ? MAX_INT_32 : 0, }; return invokeRequest(new GramJs.account.UpdateNotifySettings({ diff --git a/src/api/gramjs/scheduleUnmute.ts b/src/api/gramjs/scheduleUnmute.ts index cb97875d8..98fa12d2f 100644 --- a/src/api/gramjs/scheduleUnmute.ts +++ b/src/api/gramjs/scheduleUnmute.ts @@ -3,7 +3,7 @@ import type { OnApiUpdate } from '../types'; import { MAX_INT_32 } from '../../config'; import { getServerTime } from '../../util/serverTime'; -type UnmuteQueueItem = { chatId: string; topicId?: number; muteUntil: number }; +type UnmuteQueueItem = { chatId: string; topicId?: number; mutedUntil: number }; const unmuteTimers = new Map(); const unmuteQueue: Array = []; const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => { @@ -12,9 +12,9 @@ const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => clearTimeout(unmuteTimers.get(id)); unmuteTimers.delete(id); } - if (item.muteUntil === MAX_INT_32 || item.muteUntil <= getServerTime()) return; + if (item.mutedUntil === MAX_INT_32 || item.mutedUntil <= getServerTime()) return; unmuteQueue.push(item); - unmuteQueue.sort((a, b) => b.muteUntil - a.muteUntil); + unmuteQueue.sort((a, b) => b.mutedUntil - a.mutedUntil); const next = unmuteQueue.pop(); if (!next) return; const timer = setTimeout(() => { @@ -23,30 +23,34 @@ const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => const afterNext = unmuteQueue.pop(); if (afterNext) scheduleUnmute(afterNext, onUpdate); } - }, (item.muteUntil - getServerTime()) * 1000); + }, (item.mutedUntil - getServerTime()) * 1000); unmuteTimers.set(id, timer); }; -export function scheduleMutedChatUpdate(chatId: string, muteUntil = 0, onUpdate: OnApiUpdate) { +export function scheduleMutedChatUpdate(chatId: string, mutedUntil = 0, onUpdate: OnApiUpdate) { scheduleUnmute({ chatId, - muteUntil, + mutedUntil, }, () => onUpdate({ - '@type': 'updateNotifyExceptions', + '@type': 'updateChatNotifySettings', chatId, - isMuted: false, + settings: { + mutedUntil: 0, + }, })); } -export function scheduleMutedTopicUpdate(chatId: string, topicId: number, muteUntil = 0, onUpdate: OnApiUpdate) { +export function scheduleMutedTopicUpdate(chatId: string, topicId: number, mutedUntil = 0, onUpdate: OnApiUpdate) { scheduleUnmute({ chatId, topicId, - muteUntil, + mutedUntil, }, () => onUpdate({ - '@type': 'updateTopicNotifyExceptions', + '@type': 'updateTopicNotifySettings', chatId, topicId, - isMuted: false, + settings: { + mutedUntil: 0, + }, })); } diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index cb87fb6f6..ade8922de 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -47,8 +47,7 @@ import { buildMessageDraft, } from '../apiBuilders/messages'; import { - buildApiNotifyException, - buildApiNotifyExceptionTopic, + buildApiPeerNotifySettings, buildLangStrings, buildPrivacyKey, } from '../apiBuilders/misc'; @@ -631,28 +630,6 @@ export function updater(update: Update) { messageIds: update.messages, isPinned: update.pinned, }); - } else if ( - update instanceof GramJs.UpdateNotifySettings - && update.peer instanceof GramJs.NotifyPeer - ) { - const payload = buildApiNotifyException(update.notifySettings, update.peer.peer); - scheduleMutedChatUpdate(payload.chatId, payload.muteUntil, sendApiUpdate); - sendApiUpdate({ - '@type': 'updateNotifyExceptions', - ...payload, - }); - } else if ( - update instanceof GramJs.UpdateNotifySettings - && update.peer instanceof GramJs.NotifyForumTopic - ) { - const payload = buildApiNotifyExceptionTopic( - update.notifySettings, update.peer.peer, update.peer.topMsgId, - ); - scheduleMutedTopicUpdate(payload.chatId, payload.topicId, payload.muteUntil, sendApiUpdate); - sendApiUpdate({ - '@type': 'updateTopicNotifyExceptions', - ...payload, - }); } else if ( update instanceof GramJs.UpdateUserTyping || update instanceof GramJs.UpdateChatUserTyping @@ -837,18 +814,41 @@ export function updater(update: Update) { // Settings } else if (update instanceof GramJs.UpdateNotifySettings) { const { - notifySettings: { - showPreviews, silent, muteUntil, - }, - peer: { className }, + notifySettings, + peer: notifyPeer, } = update; + const className = notifyPeer.className; + const settings = buildApiPeerNotifySettings(notifySettings); + + if (notifyPeer instanceof GramJs.NotifyPeer) { + const peerId = getApiChatIdFromMtpPeer(notifyPeer.peer); + scheduleMutedChatUpdate(peerId, settings.mutedUntil, sendApiUpdate); + sendApiUpdate({ + '@type': 'updateChatNotifySettings', + chatId: peerId, + settings, + }); + return; + } + + if (notifyPeer instanceof GramJs.NotifyForumTopic) { + const peerId = getApiChatIdFromMtpPeer(notifyPeer.peer); + scheduleMutedTopicUpdate(peerId, notifyPeer.topMsgId, settings.mutedUntil, sendApiUpdate); + sendApiUpdate({ + '@type': 'updateTopicNotifySettings', + chatId: peerId, + topicId: notifyPeer.topMsgId, + settings, + }); + return; + } const peerType = className === 'NotifyUsers' - ? 'contact' + ? 'users' : (className === 'NotifyChats' - ? 'group' + ? 'groups' : (className === 'NotifyBroadcasts' - ? 'broadcast' + ? 'channels' : undefined ) ); @@ -858,11 +858,9 @@ export function updater(update: Update) { } sendApiUpdate({ - '@type': 'updateNotifySettings', + '@type': 'updateDefaultNotifySettings', peerType, - isSilent: Boolean(silent - || (typeof muteUntil === 'number' && Date.now() + getServerTimeOffset() * 1000 < muteUntil * 1000)), - shouldShowPreviews: Boolean(showPreviews), + settings, }); } else if (update instanceof GramJs.UpdatePeerBlocked) { sendApiUpdate({ diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index b09e0a7e4..7239c81e3 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -2,7 +2,7 @@ import type { ApiBotCommand } from './bots'; import type { ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet, } from './messages'; -import type { ApiBotVerification, ApiChatInviteImporter } from './misc'; +import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings } from './misc'; import type { ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername, } from './users'; @@ -27,8 +27,6 @@ export interface ApiChat { unreadMentionsCount?: number; unreadReactionsCount?: number; isVerified?: true; - isMuted?: boolean; - muteUntil?: number; areSignaturesShown?: boolean; areProfilesShown?: boolean; hasPrivateLink?: boolean; @@ -262,8 +260,7 @@ export interface ApiTopic { unreadMentionsCount: number; unreadReactionsCount: number; fromId: string; - isMuted?: boolean; - muteUntil?: number; + notifySettings: ApiPeerNotifySettings; } export interface ApiChatlistInviteNew { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 510b39b9f..08e11f7dd 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -106,13 +106,6 @@ export interface ApiSessionData { isTest?: true; } -export type ApiNotifyException = { - chatId: string; - isMuted: boolean; - isSilent?: boolean; - shouldShowPreviews?: boolean; -}; - export type ApiNotification = { localId: string; containerSelector?: string; @@ -352,3 +345,12 @@ export type ApiLimitTypeWithModal = Exclude; + +export type ApiPeerNotifySettings = { + mutedUntil?: number; + hasSound?: boolean; + isSilentPosting?: boolean; + shouldShowPreviews?: boolean; +}; + +export type ApiNotifyPeerType = 'users' | 'groups' | 'channels'; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index f3778bd41..bed3e4aef 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -35,7 +35,11 @@ import type { BoughtPaidMedia, } from './messages'; import type { - ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData, + ApiEmojiInteraction, + ApiError, + ApiNotifyPeerType, + ApiPeerNotifySettings, + ApiSessionData, } from './misc'; import type { ApiStarsAmount } from './payments'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; @@ -501,21 +505,24 @@ export type ApiUpdateTwoFaError = { messageKey: RegularLangFnParameters; }; -export type ApiUpdateNotifySettings = { - '@type': 'updateNotifySettings'; - peerType: 'contact' | 'group' | 'broadcast'; - isSilent: boolean; - shouldShowPreviews: boolean; +export type ApiUpdateDefaultNotifySettings = { + '@type': 'updateDefaultNotifySettings'; + peerType: ApiNotifyPeerType; + settings: Partial; }; -export type ApiUpdateNotifyExceptions = { - '@type': 'updateNotifyExceptions'; -} & ApiNotifyException; +export type ApiUpdatePeerNotifySettings = { + '@type': 'updateChatNotifySettings'; + chatId: string; + settings: Partial; +}; -export type ApiUpdateTopicNotifyExceptions = { - '@type': 'updateTopicNotifyExceptions'; +export type ApiUpdateTopicNotifySettings = { + '@type': 'updateTopicNotifySettings'; + chatId: string; topicId: number; -} & ApiNotifyException; + settings: Partial; +}; export type ApiUpdateTwoFaStateWaitCode = { '@type': 'updateTwoFaStateWaitCode'; @@ -823,14 +830,14 @@ export type ApiUpdate = ( ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | - ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | + ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | - ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | + ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifySettings | ApiUpdatePinnedTopic | ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 27684b0e0..2310e6378 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -22,15 +22,15 @@ import { getHasAdminRight, isChatChannel, isUserRightBanned, - selectIsChatMuted, } from '../../../global/helpers'; +import { getIsChatMuted } from '../../../global/helpers/notifications'; import { selectBotAppPermissions, selectChat, selectChatFullInfo, selectCurrentMessageList, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, + selectNotifyException, selectTopicLink, selectUser, selectUserFullInfo, @@ -419,7 +419,7 @@ const ChatExtra: FC = ({ @@ -487,7 +487,7 @@ export default memo(withGlobal( const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined; const botAppPermissions = chatOrUserId ? selectBotAppPermissions(global, chatOrUserId) : undefined; const isForum = chat?.isForum; - const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); + const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)); const { threadId } = selectCurrentMessageList(global) || {}; const topicId = isForum && threadId ? Number(threadId) : undefined; diff --git a/src/components/left/MuteChatModal.tsx b/src/components/left/MuteChatModal.tsx index 741480027..8b93e8207 100644 --- a/src/components/left/MuteChatModal.tsx +++ b/src/components/left/MuteChatModal.tsx @@ -51,16 +51,16 @@ const MuteChatModal: FC = ({ ], [lang]); const handleSubmit = useCallback(() => { - let muteUntil: number; + let mutedUntil: number; if (muteUntilOption === MuteDuration.Forever) { - muteUntil = MAX_INT_32; + mutedUntil = MAX_INT_32; } else { - muteUntil = Math.floor(Date.now() / 1000) + Number(muteUntilOption); + mutedUntil = Math.floor(Date.now() / 1000) + Number(muteUntilOption); } if (topicId) { - updateTopicMutedState({ chatId, topicId, muteUntil }); + updateTopicMutedState({ chatId, topicId, mutedUntil }); } else { - updateChatMutedState({ chatId, muteUntil }); + updateChatMutedState({ chatId, mutedUntil }); } onClose(); }, [chatId, muteUntilOption, onClose, topicId]); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 4144f2c11..2d4d0205e 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -23,8 +23,8 @@ import { groupStatefulContent, isUserId, isUserOnline, - selectIsChatMuted, } from '../../../global/helpers'; +import { getIsChatMuted } from '../../../global/helpers/notifications'; import { selectCanAnimateInterface, selectChat, @@ -35,8 +35,8 @@ import { selectDraft, selectIsForumPanelClosed, selectIsForumPanelOpen, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, + selectNotifyException, selectOutgoingStatus, selectPeer, selectPeerStory, @@ -470,7 +470,7 @@ export default memo(withGlobal( return { chat, - isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), + isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)), lastMessageSender, draft: selectDraft(global, chatId, MAIN_THREAD_ID), isSelected, diff --git a/src/components/left/main/ChatBadge.tsx b/src/components/left/main/ChatBadge.tsx index 7f6d3baf0..21683d399 100644 --- a/src/components/left/main/ChatBadge.tsx +++ b/src/components/left/main/ChatBadge.tsx @@ -6,6 +6,7 @@ import type { ApiChat, ApiTopic } from '../../../api/types'; import type { Signal } from '../../../util/signals'; import buildClassName from '../../../util/buildClassName'; +import { getServerTime } from '../../../util/serverTime'; import { isSignal } from '../../../util/signals'; import { formatIntegerCompact } from '../../../util/textFormat'; import { extractCurrentThemeParams } from '../../../util/themeStyle'; @@ -62,20 +63,29 @@ const ChatBadge: FC = ({ isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined ), [topics, isForum]); - const unreadCount = useMemo(() => ( - isForum - // If we have unmuted topics, display the count of those. Otherwise, display the count of all topics. - ? ((isMuted && topicsWithUnread?.filter((acc) => acc.isMuted === false).length) - || topicsWithUnread?.length) - : (topic || chat).unreadCount - ), [chat, topic, topicsWithUnread, isForum, isMuted]); + const unreadCount = useMemo(() => { + if (!isForum) { + return (topic || chat).unreadCount; + } - const shouldBeMuted = useMemo(() => { - const hasUnmutedUnreadTopics = topics - && Object.values(topics).some((acc) => !acc.isMuted && acc.unreadCount); + return topicsWithUnread?.length; + }, [chat, topic, topicsWithUnread, isForum]); - return isMuted || (topics && !hasUnmutedUnreadTopics); - }, [topics, isMuted]); + const shouldBeUnMuted = useMemo(() => { + if (!isForum) { + return !isMuted || topic?.notifySettings.mutedUntil === 0; + } + + if (isMuted) { + return topicsWithUnread?.some((acc) => acc.notifySettings.mutedUntil === 0); + } + + const isEveryUnreadMuted = topicsWithUnread?.every((acc) => ( + acc.notifySettings.mutedUntil && acc.notifySettings.mutedUntil > getServerTime() + )); + + return !isEveryUnreadMuted; + }, [isForum, isMuted, topicsWithUnread, topic?.notifySettings.mutedUntil]); const hasUnreadMark = topic ? false : chat.hasUnreadMark; @@ -91,7 +101,7 @@ const ChatBadge: FC = ({ const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog); const className = buildClassName( 'ChatBadge', - shouldBeMuted && 'muted', + !shouldBeUnMuted && 'muted', !isUnread && isPinned && 'pinned', isUnread && 'unread', ); @@ -109,7 +119,7 @@ const ChatBadge: FC = ({ function renderContent() { const unreadReactionsElement = unreadReactionsCount && ( -
+
); @@ -121,7 +131,7 @@ const ChatBadge: FC = ({ ); const unopenedTopicElement = isTopicUnopened && ( -
+
); const unreadCountElement = (hasUnreadMark || unreadCount) ? ( diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index d5bc2293f..dfc88b0d7 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -10,6 +10,7 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ChatAnimationTypes } from './hooks'; import { groupStatefulContent } from '../../../global/helpers'; +import { getIsChatMuted } from '../../../global/helpers/notifications'; import { selectCanAnimateInterface, selectCanDeleteTopic, @@ -17,6 +18,8 @@ import { selectChatMessage, selectCurrentMessageList, selectDraft, + selectNotifyDefaults, + selectNotifyException, selectOutgoingStatus, selectPeerStory, selectSender, @@ -57,6 +60,7 @@ type OwnProps = { type StateProps = { chat: ApiChat; + isChatMuted?: boolean; canDelete?: boolean; lastMessage?: ApiMessage; lastMessageStory?: ApiTypeStory; @@ -75,6 +79,7 @@ const Topic: FC = ({ isSelected, chatId, chat, + isChatMuted, style, lastMessage, lastMessageStory, @@ -106,9 +111,9 @@ const Topic: FC = ({ const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const { - isPinned, isClosed, + isPinned, isClosed, notifySettings, } = topic; - const isMuted = topic.isMuted || (topic.isMuted === undefined && chat.isMuted); + const isMuted = Boolean(notifySettings.mutedUntil || (notifySettings.mutedUntil === undefined && isChatMuted)); const handleOpenDeleteModal = useLastCallback(() => { markRenderDeleteModal(); @@ -154,6 +159,7 @@ const Topic: FC = ({ const contextActions = useTopicContextActions({ topic, chat, + isChatMuted, wasOpened: wasTopicOpened, canDelete, handleDelete: handleOpenDeleteModal, @@ -181,7 +187,7 @@ const Topic: FC = ({

{renderText(topic.title)}

- {topic.isMuted && } + {Boolean(notifySettings.mutedUntil) && }
{isClosed && ( @@ -247,11 +253,16 @@ export default memo(withGlobal( const storyData = lastMessage?.content.storyData; const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id); + const isChatMuted = chat && getIsChatMuted( + chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id), + ); + return { chat, lastMessage, lastMessageSender, typingStatus, + isChatMuted, canDelete: selectCanDeleteTopic(global, chatId, topic.id), withInterfaceAnimations: selectCanAnimateInterface(global), draft, diff --git a/src/components/left/main/hooks/useTopicContextActions.ts b/src/components/left/main/hooks/useTopicContextActions.ts index c1a882a1e..d5b14927a 100644 --- a/src/components/left/main/hooks/useTopicContextActions.ts +++ b/src/components/left/main/hooks/useTopicContextActions.ts @@ -13,6 +13,7 @@ import useOldLang from '../../../../hooks/useOldLang'; export default function useTopicContextActions({ topic, chat, + isChatMuted, wasOpened, canDelete, handleDelete, @@ -20,6 +21,7 @@ export default function useTopicContextActions({ }: { topic: ApiTopic; chat: ApiChat; + isChatMuted?: boolean; wasOpened?: boolean; canDelete?: boolean; handleDelete?: NoneToVoidFunction; @@ -29,7 +31,7 @@ export default function useTopicContextActions({ return useMemo(() => { const { - isPinned, isMuted, isClosed, id: topicId, + isPinned, notifySettings, isClosed, id: topicId, } = topic; const chatId = chat.id; @@ -75,7 +77,7 @@ export default function useTopicContextActions({ handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }), }) : undefined; - const actionMute = ((chat.isMuted && isMuted !== false) || isMuted === true) + const actionMute = ((isChatMuted && notifySettings.mutedUntil === undefined) || notifySettings.mutedUntil) ? { title: lang('ChatList.Unmute'), icon: 'unmute', @@ -114,5 +116,5 @@ export default function useTopicContextActions({ actionCloseTopic, actionDelete, ]) as MenuItemContextAction[]; - }, [topic, chat, wasOpened, lang, canDelete, handleDelete, handleMute]); + }, [topic, chat, isChatMuted, wasOpened, lang, canDelete, handleDelete, handleMute]); } diff --git a/src/components/left/search/LeftSearchResultChat.tsx b/src/components/left/search/LeftSearchResultChat.tsx index dc6f92298..dd57c3a0a 100644 --- a/src/components/left/search/LeftSearchResultChat.tsx +++ b/src/components/left/search/LeftSearchResultChat.tsx @@ -5,10 +5,10 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiChat, ApiUser } from '../../../api/types'; import { StoryViewerOrigin } from '../../../types'; -import { isUserId, selectIsChatMuted } from '../../../global/helpers'; +import { isUserId } from '../../../global/helpers'; +import { getIsChatMuted } from '../../../global/helpers/notifications'; import { - selectChat, selectIsChatPinned, selectNotifyExceptions, - selectNotifySettings, selectUser, + selectChat, selectIsChatPinned, selectNotifyDefaults, selectNotifyException, selectUser, } from '../../../global/selectors'; import { extractCurrentThemeParams } from '../../../util/themeStyle'; @@ -156,9 +156,7 @@ export default memo(withGlobal( const chat = selectChat(global, chatId); const user = selectUser(global, chatId); const isPinned = selectIsChatPinned(global, chatId); - const isMuted = chat - ? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)) - : undefined; + const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)); return { chat, diff --git a/src/components/left/settings/SettingsNotifications.tsx b/src/components/left/settings/SettingsNotifications.tsx index 804e28a55..3f5d10e69 100644 --- a/src/components/left/settings/SettingsNotifications.tsx +++ b/src/components/left/settings/SettingsNotifications.tsx @@ -3,6 +3,8 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { ApiNotifyPeerType, ApiPeerNotifySettings } from '../../../api/types'; + import { checkIfNotificationsSupported, checkIfOfflinePushFailed, @@ -22,12 +24,7 @@ type OwnProps = { }; type StateProps = { - hasPrivateChatsNotifications: boolean; - hasPrivateChatsMessagePreview: boolean; - hasGroupNotifications: boolean; - hasGroupMessagePreview: boolean; - hasBroadcastNotifications: boolean; - hasBroadcastMessagePreview: boolean; + notifyDefaults?: Record; hasContactJoinedNotifications: boolean; hasWebNotifications: boolean; hasPushNotifications: boolean; @@ -37,12 +34,7 @@ type StateProps = { const SettingsNotifications: FC = ({ isActive, onReset, - hasPrivateChatsNotifications, - hasPrivateChatsMessagePreview, - hasGroupNotifications, - hasGroupMessagePreview, - hasBroadcastNotifications, - hasBroadcastMessagePreview, + notifyDefaults, hasContactJoinedNotifications, hasPushNotifications, hasWebNotifications, @@ -66,27 +58,18 @@ const SettingsNotifications: FC = ({ const handleSettingsChange = useCallback(( e: ChangeEvent, - peerType: 'contact' | 'group' | 'broadcast', - setting: 'silent' | 'showPreviews', + peerType: ApiNotifyPeerType, + setting: 'mute' | 'showPreviews', ) => { - const currentIsSilent = peerType === 'contact' - ? !hasPrivateChatsNotifications - : !(peerType === 'group' ? hasGroupNotifications : hasBroadcastNotifications); - const currentShouldShowPreviews = peerType === 'contact' - ? hasPrivateChatsMessagePreview - : (peerType === 'group' ? hasGroupMessagePreview : hasBroadcastMessagePreview); + const currentIsMuted = Boolean(notifyDefaults?.[peerType]?.mutedUntil); + const currentShouldShowPreviews = Boolean(notifyDefaults?.[peerType]?.shouldShowPreviews); updateNotificationSettings({ peerType, - ...(setting === 'silent' && { isSilent: !e.target.checked, shouldShowPreviews: currentShouldShowPreviews }), - ...(setting === 'showPreviews' && { shouldShowPreviews: e.target.checked, isSilent: currentIsSilent }), + isMuted: setting === 'mute' ? !e.target.checked : currentIsMuted, + shouldShowPreviews: setting === 'showPreviews' ? e.target.checked : currentShouldShowPreviews, }); - }, [ - hasBroadcastMessagePreview, hasBroadcastNotifications, - hasGroupMessagePreview, hasGroupNotifications, - hasPrivateChatsMessagePreview, hasPrivateChatsNotifications, - updateNotificationSettings, - ]); + }, [notifyDefaults]); const handleWebNotificationsChange = useCallback((e: ChangeEvent) => { const isEnabled = e.target.checked; @@ -103,27 +86,27 @@ const SettingsNotifications: FC = ({ }, [updateWebNotificationSettings]); const handlePrivateChatsNotificationsChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'contact', 'silent'); + handleSettingsChange(e, 'users', 'mute'); }, [handleSettingsChange]); const handlePrivateChatsPreviewChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'contact', 'showPreviews'); + handleSettingsChange(e, 'users', 'showPreviews'); }, [handleSettingsChange]); const handleGroupsNotificationsChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'group', 'silent'); + handleSettingsChange(e, 'groups', 'mute'); }, [handleSettingsChange]); const handleGroupsPreviewChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'group', 'showPreviews'); + handleSettingsChange(e, 'groups', 'showPreviews'); }, [handleSettingsChange]); const handleChannelsNotificationsChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'broadcast', 'silent'); + handleSettingsChange(e, 'channels', 'mute'); }, [handleSettingsChange]); const handleChannelsPreviewChange = useCallback((e: ChangeEvent) => { - handleSettingsChange(e, 'broadcast', 'showPreviews'); + handleSettingsChange(e, 'channels', 'showPreviews'); }, [handleSettingsChange]); const handleContactNotificationChange = useCallback((e: ChangeEvent) => { @@ -186,17 +169,17 @@ const SettingsNotifications: FC = ({
@@ -206,15 +189,17 @@ const SettingsNotifications: FC = ({
@@ -224,15 +209,17 @@ const SettingsNotifications: FC = ({
@@ -253,12 +240,6 @@ const SettingsNotifications: FC = ({ export default memo(withGlobal( (global): StateProps => { return { - hasPrivateChatsNotifications: Boolean(global.settings.byKey.hasPrivateChatsNotifications), - hasPrivateChatsMessagePreview: Boolean(global.settings.byKey.hasPrivateChatsMessagePreview), - hasGroupNotifications: Boolean(global.settings.byKey.hasGroupNotifications), - hasGroupMessagePreview: Boolean(global.settings.byKey.hasGroupMessagePreview), - hasBroadcastNotifications: Boolean(global.settings.byKey.hasBroadcastNotifications), - hasBroadcastMessagePreview: Boolean(global.settings.byKey.hasBroadcastMessagePreview), hasContactJoinedNotifications: Boolean(global.settings.byKey.hasContactJoinedNotifications), hasWebNotifications: global.settings.byKey.hasWebNotifications, hasPushNotifications: global.settings.byKey.hasPushNotifications, diff --git a/src/components/middle/FloatingActionButtons.tsx b/src/components/middle/FloatingActionButtons.tsx index d27eb1e52..6c90482b8 100644 --- a/src/components/middle/FloatingActionButtons.tsx +++ b/src/components/middle/FloatingActionButtons.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { MessageListType } from '../../types'; +import type { MessageListType, ThreadId } from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors'; @@ -24,6 +24,7 @@ type OwnProps = { type StateProps = { chatId?: string; messageListType?: MessageListType; + threadId?: ThreadId; unreadCount?: number; unreadReactions?: number[]; unreadMentions?: number[]; @@ -38,6 +39,7 @@ const FloatingActionButtons: FC = ({ canPost, messageListType, chatId, + threadId, unreadCount, unreadReactions, unreadMentions, @@ -56,6 +58,16 @@ const FloatingActionButtons: FC = ({ const hasUnreadReactions = Boolean(reactionsCount); const hasUnreadMentions = Boolean(mentionsCount); + const handleReadAllReactions = useLastCallback(() => { + if (!chatId) return; + readAllReactions({ chatId, threadId }); + }); + + const handleReadAllMentions = useLastCallback(() => { + if (!chatId) return; + readAllMentions({ chatId, threadId }); + }); + useEffect(() => { if (hasUnreadReactions && chatId && !unreadReactions?.length) { fetchUnreadReactions({ chatId }); @@ -120,7 +132,7 @@ const FloatingActionButtons: FC = ({ icon="heart-outline" ariaLabelLang="AccDescrReactionMentionDown" onClick={focusNextReaction} - onReadAll={readAllReactions} + onReadAll={handleReadAllReactions} unreadCount={reactionsCount} className={buildClassName( styles.reactions, @@ -133,7 +145,7 @@ const FloatingActionButtons: FC = ({ icon="mention" ariaLabelLang="AccDescrMentionDown" onClick={focusNextMention} - onReadAll={readAllMentions} + onReadAll={handleReadAllMentions} unreadCount={mentionsCount} className={!hasUnreadMentions && styles.hidden} /> @@ -166,6 +178,7 @@ export default memo(withGlobal( return { messageListType, chatId, + threadId, reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined, unreadReactions: shouldShowCount ? chat.unreadReactions : undefined, unreadMentions: shouldShowCount ? chat.unreadMentions : undefined, diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 2403502ab..d068c5f0a 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -20,8 +20,8 @@ import { isSystemBot, isUserId, isUserRightBanned, - selectIsChatMuted, } from '../../global/helpers'; +import { getIsChatMuted } from '../../global/helpers/notifications'; import { selectBot, selectCanGift, @@ -32,8 +32,8 @@ import { selectCurrentMessageList, selectIsChatWithSelf, selectIsRightColumnShown, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, + selectNotifyException, selectTabState, selectTopic, selectUser, @@ -759,7 +759,7 @@ export default memo(withGlobal( return { chat, - isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), + isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)), isPrivate, isTopic: chat?.isForum && !isMainThread, isForum: chat?.isForum, diff --git a/src/components/middle/hooks/useMessageObservers.ts b/src/components/middle/hooks/useMessageObservers.ts index 815fe5358..58fb3a11f 100644 --- a/src/components/middle/hooks/useMessageObservers.ts +++ b/src/components/middle/hooks/useMessageObservers.ts @@ -86,7 +86,7 @@ export default function useMessageObservers( } if (mentionIds.length) { - markMentionsRead({ messageIds: mentionIds }); + markMentionsRead({ chatId, messageIds: mentionIds }); } if (reactionIds.length) { diff --git a/src/components/middle/message/ActionMessage.tsx b/src/components/middle/message/ActionMessage.tsx index 472d50bf0..cfaabe4c3 100644 --- a/src/components/middle/message/ActionMessage.tsx +++ b/src/components/middle/message/ActionMessage.tsx @@ -227,9 +227,9 @@ const ActionMessage = ({ } if (message.hasUnreadMention) { - markMentionsRead({ messageIds: [id] }); + markMentionsRead({ chatId, messageIds: [id] }); } - }, [hasUnreadReaction, id, animateUnreadReaction, message.hasUnreadMention]); + }, [hasUnreadReaction, chatId, id, animateUnreadReaction, message.hasUnreadMention]); useEffect(() => { if (action.type !== 'giftPremium') return; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 5ad2ac089..ab4c0b407 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -869,9 +869,9 @@ const Message: FC = ({ } if (unreadMentionIds.length) { - markMentionsRead({ messageIds: unreadMentionIds }); + markMentionsRead({ chatId, messageIds: unreadMentionIds }); } - }, [hasUnreadReaction, album, messageId, animateUnreadReaction, message.hasUnreadMention]); + }, [hasUnreadReaction, album, chatId, messageId, animateUnreadReaction, message.hasUnreadMention]); const albumLayout = useMemo(() => { return isAlbum diff --git a/src/components/right/management/ManageUser.tsx b/src/components/right/management/ManageUser.tsx index 3329289ca..694d8e5d3 100644 --- a/src/components/right/management/ManageUser.tsx +++ b/src/components/right/management/ManageUser.tsx @@ -9,11 +9,12 @@ import type { ApiPhoto, ApiUser } from '../../../api/types'; import { ManagementProgress } from '../../../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; -import { isUserBot, selectIsChatMuted } from '../../../global/helpers'; +import { isUserBot } from '../../../global/helpers'; +import { getIsChatMuted } from '../../../global/helpers/notifications'; import { selectChat, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, + selectNotifyException, selectTabState, selectUser, selectUserFullInfo, @@ -296,7 +297,7 @@ export default memo(withGlobal( const chat = selectChat(global, userId); const userFullInfo = selectUserFullInfo(global, userId); const { progress } = selectTabState(global).management; - const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); + const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)); const personalPhoto = userFullInfo?.personalPhoto; const notPersonalPhoto = userFullInfo?.profilePhoto || userFullInfo?.fallbackPhoto; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index cdd485229..26bc51cd2 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1,6 +1,7 @@ import type { ApiChat, ApiChatFolder, ApiChatlistExportedInvite, ApiChatMember, ApiError, ApiMissingInvitedUser, + ApiTopic, } from '../../../api/types'; import type { RequiredGlobalActions } from '../../index'; import type { @@ -60,6 +61,7 @@ import { addChatMembers, addChats, addMessages, + addNotifyExceptions, addSimilarBots, addUsers, addUserStatuses, @@ -221,7 +223,7 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => { const chat = selectChat(global, id); if (chat?.hasUnreadMark) { - actions.toggleChatUnread({ id }); + actions.markChatRead({ id }); } const isChatOnlySummary = !selectChatLastMessageId(global, id); @@ -623,32 +625,26 @@ addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): P }); addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => { - const { chatId, muteUntil = 0 } = payload; + const { chatId, isMuted, mutedUntil } = payload; const chat = selectChat(global, chatId); if (!chat) { return; } - const isMuted = payload.isMuted ?? muteUntil > 0; - - global = updateChat(global, chatId, { isMuted }); - setGlobal(global); - void callApi('updateChatMutedState', { chat, isMuted, muteUntil }); + void callApi('updateChatMutedState', { chat, isMuted, mutedUntil }); }); addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => { - const { chatId, topicId, muteUntil = 0 } = payload; + const { + chatId, topicId, isMuted, mutedUntil, + } = payload; const chat = selectChat(global, chatId); if (!chat) { return; } - const isMuted = payload.isMuted ?? muteUntil > 0; - - global = updateTopic(global, chatId, topicId, { isMuted }); - setGlobal(global); void callApi('updateTopicMutedState', { - chat, topicId, isMuted, muteUntil, + chat, topicId, isMuted, mutedUntil, }); }); @@ -1155,17 +1151,47 @@ addActionHandler('deleteChatFolder', async (global, actions, payload): Promise { +addActionHandler('markChatUnread', (global, actions, payload): ActionReturnType => { const { id } = payload; const chat = selectChat(global, id); - if (chat) { - if (chat.unreadCount) { - void callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID }); - } else { - void callApi('toggleDialogUnread', { - chat, - hasUnreadMark: !chat.hasUnreadMark, - }); + if (!chat) return; + void callApi('toggleDialogUnread', { + chat, + hasUnreadMark: !chat.hasUnreadMark, + }); +}); + +addActionHandler('markChatRead', async (global, actions, payload): Promise => { + const { id } = payload; + const chat = selectChat(global, id); + if (!chat) return; + if (!chat.isForum) { + await callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID }); + actions.readAllMentions({ chatId: id }); + actions.readAllReactions({ chatId: id }); + return; + } + + let hasMoreTopics = true; + let lastTopic: ApiTopic | undefined; + let processedCount = 0; + + while (hasMoreTopics) { + const result = await callApi('fetchTopics', { + chat, offsetDate: lastTopic?.date, offsetTopicId: lastTopic?.id, offsetId: lastTopic?.lastMessageId, limit: 100, + }); + + if (!result?.topics?.length) return; + + result.topics.forEach((topic) => { + if (!topic.unreadCount && !topic.unreadMentionsCount && !topic.unreadReactionsCount) return; + actions.markTopicRead({ chatId: id, topicId: topic.id }); + }); + + lastTopic = result.topics[result.topics.length - 1]; + processedCount += result.topics.length; + if (result.count <= processedCount) { + hasMoreTopics = false; } } }); @@ -1185,6 +1211,8 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType = threadId: topicId, maxId: lastTopicMessageId, }); + actions.readAllMentions({ chatId, threadId: topicId }); + actions.readAllReactions({ chatId, threadId: topicId }); global = getGlobal(); global = updateTopic(global, chatId, topicId, { @@ -2910,6 +2938,7 @@ async function loadChats( global = updateChatListSecondaryInfo(global, listType, result); global = replaceMessages(global, result.messages); global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType); + global = addNotifyExceptions(global, result.notifyExceptionById); if (!shouldIgnorePagination) { global = replaceChatListLoadingParameters( diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 9096adbc4..b382076de 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -1818,12 +1818,11 @@ async function fetchUnreadMentions(global: T, chatId: str } addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => { - const { messageIds, tabId = getCurrentTabId() } = payload; - - const chat = selectCurrentChat(global, tabId); + const { chatId, messageIds, tabId = getCurrentTabId() } = payload; + const chat = selectChat(global, chatId); if (!chat) return; - global = removeUnreadMentions(global, chat.id, chat, messageIds, true); + global = removeUnreadMentions(global, chatId, chat, messageIds, true); setGlobal(global); actions.markMessagesRead({ messageIds, tabId }); @@ -1848,17 +1847,22 @@ addActionHandler('focusNextMention', async (global, actions, payload): Promise { - const { tabId = getCurrentTabId() } = payload || {}; + const { chatId, threadId = MAIN_THREAD_ID } = payload; - const chat = selectCurrentChat(global, tabId); + const chat = selectChat(global, chatId); if (!chat) return undefined; - callApi('readAllMentions', { chat }); + callApi('readAllMentions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId }); - return updateChat(global, chat.id, { - unreadMentionsCount: undefined, - unreadMentions: undefined, - }); + if (threadId === MAIN_THREAD_ID) { + return updateChat(global, chat.id, { + unreadMentionsCount: undefined, + unreadMentions: undefined, + }); + } + + // TODO[Forums]: Support mentions in threads + return undefined; }); addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 6d8ff13ba..e10d0f3d8 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -1,6 +1,6 @@ import type { ApiError, ApiReaction, ApiReactionEmoji } from '../../../api/types'; import type { ActionReturnType } from '../../types'; -import { ApiMediaFormat } from '../../../api/types'; +import { ApiMediaFormat, MAIN_THREAD_ID } from '../../../api/types'; import { GENERAL_REFETCH_INTERVAL } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; @@ -557,16 +557,21 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy }); addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => { - const { tabId = getCurrentTabId() } = payload || {}; - const chat = selectCurrentChat(global, tabId); + const { chatId, threadId = MAIN_THREAD_ID } = payload; + const chat = selectChat(global, chatId); if (!chat) return undefined; - callApi('readAllReactions', { chat }); + callApi('readAllReactions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId }); - return updateUnreadReactions(global, chat.id, { - unreadReactionsCount: undefined, - unreadReactions: undefined, - }); + if (threadId === MAIN_THREAD_ID) { + return updateUnreadReactions(global, chat.id, { + unreadReactionsCount: undefined, + unreadReactions: undefined, + }); + } + + // TODO[Forums]: Support unread reactions in threads + return undefined; }); addActionHandler('loadTopReactions', async (global): Promise => { diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 0c96e7b3e..6f9591a65 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -5,7 +5,7 @@ import { UPLOADING_WALLPAPER_SLUG, } from '../../../types'; -import { APP_CONFIG_REFETCH_INTERVAL, COUNTRIES_WITH_12H_TIME_FORMAT } from '../../../config'; +import { APP_CONFIG_REFETCH_INTERVAL, COUNTRIES_WITH_12H_TIME_FORMAT, MAX_INT_32 } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey } from '../../../util/iteratees'; import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications'; @@ -16,9 +16,9 @@ import { callApi } from '../../../api/gramjs'; import { buildApiInputPrivacyRules } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { - addBlockedUser, addNotifyExceptions, deletePeerPhoto, + addBlockedUser, addNotifyException, addNotifyExceptions, deletePeerPhoto, removeBlockedUser, replaceSettings, updateChat, - updateNotifySettings, updateUser, updateUserFullInfo, + updateUser, updateUserFullInfo, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { @@ -306,26 +306,40 @@ addActionHandler('loadNotificationExceptions', async (global): Promise => }); addActionHandler('loadNotificationSettings', async (global): Promise => { - const result = await callApi('fetchNotificationSettings'); - if (!result) { - return; - } + const [signUpNotification, notifyDefaults] = await Promise.all([ + callApi('fetchContactSignUpSetting'), + callApi('fetchNotifyDefaultSettings'), + ]); + + if (!notifyDefaults) return; global = getGlobal(); - global = replaceSettings(global, result); + global = replaceSettings(global, { + hasContactJoinedNotifications: signUpNotification, + }); + global = { + ...global, + settings: { + ...global.settings, + notifyDefaults, + }, + }; setGlobal(global); }); addActionHandler('updateNotificationSettings', async (global, actions, payload): Promise => { - const { peerType, isSilent, shouldShowPreviews } = payload!; + const { peerType, isMuted, shouldShowPreviews } = payload!; - const result = await callApi('updateNotificationSettings', peerType, { isSilent, shouldShowPreviews }); + const result = await callApi('updateNotificationSettings', peerType, { isMuted, shouldShowPreviews }); if (!result) { return; } global = getGlobal(); - global = updateNotifySettings(global, peerType, isSilent, shouldShowPreviews); + global = addNotifyException(global, peerType, { + mutedUntil: isMuted ? MAX_INT_32 : undefined, + shouldShowPreviews, + }); setGlobal(global); }); @@ -582,12 +596,11 @@ addActionHandler('loadCountryList', async (global, actions, payload): Promise => { - const { tabId = getCurrentTabId() } = payload || {}; +addActionHandler('ensureTimeFormat', async (global, actions): Promise => { if (global.authNearestCountry) { const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT .has(global.authNearestCountry.toUpperCase()) ? '12h' : '24h'; - actions.setSettingOption({ timeFormat, tabId }); + actions.setSettingOption({ timeFormat }); setTimeFormat(timeFormat); } @@ -598,7 +611,7 @@ addActionHandler('ensureTimeFormat', async (global, actions, payload): Promise { switch (update['@type']) { - case 'updateNotifySettings': { - return updateNotifySettings(global, update.peerType, update.isSilent, update.shouldShowPreviews); + case 'updateDefaultNotifySettings': { + return updateNotifyDefaults(global, update.peerType, update.settings); } - case 'updateNotifyExceptions': { + case 'updateChatNotifySettings': { const { - chatId, isMuted, isSilent, shouldShowPreviews, + chatId, settings, } = update; - const chat = global.chats.byId[chatId]; - if (chat) { - global = updateChat(global, chatId, { isMuted }); - } - - global = addNotifyException(global, chatId, { isMuted, isSilent, shouldShowPreviews }); + global = addNotifyException(global, chatId, settings); setGlobal(global); break; } - case 'updateTopicNotifyExceptions': { + case 'updateTopicNotifySettings': { const { - chatId, topicId, isMuted, + chatId, topicId, settings, } = update; - global = updateTopic(global, chatId, topicId, { isMuted }); + global = updateTopic(global, chatId, topicId, { notifySettings: settings }); setGlobal(global); break; diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index 8f035d43a..69ff1143a 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -24,7 +24,10 @@ import { replaceSettings } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { selectCanAnimateInterface, - selectNotifySettings, selectPerformanceSettings, selectTabState, selectTheme, + selectPerformanceSettings, + selectSettingsKeys, + selectTabState, + selectTheme, } from '../../selectors'; const HISTORY_ANIMATION_DURATION = 450; @@ -100,7 +103,7 @@ addActionHandler('initShared', (): ActionReturnType => { }); addActionHandler('initMain', (global): ActionReturnType => { - const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global); + const { hasWebNotifications, hasPushNotifications } = selectSettingsKeys(global); if (hasWebNotifications && hasPushNotifications) { // Most of the browsers only show the notifications permission prompt after the first user gesture. const events = ['click', 'keypress']; diff --git a/src/global/cache.ts b/src/global/cache.ts index 030730288..6acdaba68 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -300,6 +300,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cached.cacheVersion = 2; } + + if (!cached.chats.notifyExceptionById) { + cached.chats.notifyExceptionById = initialState.chats.notifyExceptionById; + } } function updateCache(force?: boolean) { @@ -494,6 +498,7 @@ function reduceChats(global: T): GlobalState['chats'] { similarChannelsById: {}, similarBotsById: {}, isFullyLoaded: {}, + notifyExceptionById: {}, loadingParameters: INITIAL_GLOBAL_STATE.chats.loadingParameters, byId: pickTruthy(global.chats.byId, idsToSave), fullInfoById: pickTruthy(global.chats.fullInfoById, idsToSave), @@ -662,7 +667,6 @@ function reduceSettings(global: T): GlobalState['settings themes, performance, privacy: {}, - notifyExceptions: {}, botVerificationShownPeerIds, miniAppsCachedPosition, miniAppsCachedSize, diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 2042919a8..397605e91 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -13,7 +13,7 @@ import type { } from '../../api/types'; import type { OldLangFn } from '../../hooks/useOldLang'; import type { - CustomPeer, NotifyException, NotifySettings, ThreadId, + CustomPeer, ThreadId, } from '../../types'; import type { LangFn } from '../../util/localization'; import { MAIN_THREAD_ID } from '../../api/types'; @@ -306,40 +306,6 @@ export function isChatArchived(chat: ApiChat) { return chat.folderId === ARCHIVED_FOLDER_ID; } -export function selectIsChatMuted( - chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record = {}, -) { - // If this chat is in exceptions they take precedence - if (notifyExceptions[chat.id] && notifyExceptions[chat.id].isMuted !== undefined) { - return notifyExceptions[chat.id].isMuted; - } - - return ( - chat.isMuted - || (isUserId(chat.id) && !notifySettings.hasPrivateChatsNotifications) - || (isChatChannel(chat) && !notifySettings.hasBroadcastNotifications) - || (isChatGroup(chat) && !notifySettings.hasGroupNotifications) - ); -} - -export function selectShouldShowMessagePreview( - chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record = {}, -) { - const { - hasPrivateChatsMessagePreview = true, - hasBroadcastMessagePreview = true, - hasGroupMessagePreview = true, - } = notifySettings; - // If this chat is in exceptions they take precedence - if (notifyExceptions[chat.id] && notifyExceptions[chat.id].shouldShowPreviews !== undefined) { - return notifyExceptions[chat.id].shouldShowPreviews; - } - - return (isUserId(chat.id) && hasPrivateChatsMessagePreview) - || (isChatChannel(chat) && hasBroadcastMessagePreview) - || (isChatGroup(chat) && hasGroupMessagePreview); -} - export function getCanDeleteChat(chat: ApiChat) { return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); } diff --git a/src/global/helpers/misc.ts b/src/global/helpers/misc.ts index b1195f135..a5f493405 100644 --- a/src/global/helpers/misc.ts +++ b/src/global/helpers/misc.ts @@ -1,4 +1,8 @@ -import type { ApiInputPrivacyRules, BotsPrivacyType, PrivacyVisibility } from '../../api/types'; +import type { + ApiInputPrivacyRules, + BotsPrivacyType, + PrivacyVisibility, +} from '../../api/types'; import type { GlobalState } from '../types'; import { partition } from '../../util/iteratees'; diff --git a/src/global/helpers/notifications.ts b/src/global/helpers/notifications.ts new file mode 100644 index 000000000..630614dea --- /dev/null +++ b/src/global/helpers/notifications.ts @@ -0,0 +1,65 @@ +import type { + ApiChat, + ApiNotifyPeerType, + ApiPeer, + ApiPeerNotifySettings, +} from '../../api/types'; + +import { omitUndefined } from '../../util/iteratees'; +import { getServerTime } from '../../util/serverTime'; +import { isChatChannel, isUserId } from './chats'; + +export function getIsChatMuted( + chat: ApiChat, + notifyDefaults?: Record, + notifyException?: ApiPeerNotifySettings, +) { + const settings = getChatNotifySettings(chat, notifyDefaults, notifyException); + if (!settings?.mutedUntil) return false; + return getServerTime() < settings.mutedUntil; +} + +export function getIsChatSilent( + chat: ApiChat, + notifyDefaults?: Record, + notifyException?: ApiPeerNotifySettings, +) { + const settings = getChatNotifySettings(chat, notifyDefaults, notifyException); + if (!settings) return false; + return !settings.hasSound; +} + +export function getShouldShowMessagePreview( + chat: ApiChat, + notifyDefaults?: Record, + notifyException?: ApiPeerNotifySettings, +) { + const settings = getChatNotifySettings(chat, notifyDefaults, notifyException); + return Boolean(settings?.shouldShowPreviews); +} + +export function getChatNotifySettings( + chat: ApiChat, + notifyDefaults?: Record, + notifyException?: ApiPeerNotifySettings, +): ApiPeerNotifySettings | undefined { + const defaults = notifyDefaults?.[getNotificationPeerType(chat)]; + + if (!notifyException && !defaults) { + return undefined; + } + + return { + ...defaults, + ...(notifyException && omitUndefined(notifyException)), + }; +} + +export function getNotificationPeerType(peer: ApiPeer): ApiNotifyPeerType { + if (isUserId(peer.id)) { + return 'users'; + } + + const chat = peer as ApiChat; + return isChatChannel(chat) ? 'channels' : 'groups'; +} diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 6f6c0fac8..e825aaff6 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -122,6 +122,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { similarChannelsById: {}, similarBotsById: {}, topicsInfoById: {}, + notifyExceptionById: {}, loadingParameters: { active: {}, archived: {}, @@ -297,7 +298,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { }, performance: INITIAL_PERFORMANCE_STATE_MAX, privacy: {}, - notifyExceptions: {}, botVerificationShownPeerIds: [], }, diff --git a/src/global/reducers/settings.ts b/src/global/reducers/settings.ts index 0aa432f7b..c872d9cd5 100644 --- a/src/global/reducers/settings.ts +++ b/src/global/reducers/settings.ts @@ -1,6 +1,6 @@ -import type { ApiNotifyException } from '../../api/types'; +import type { ApiNotifyPeerType, ApiPeerNotifySettings } from '../../api/types'; import type { - ISettings, IThemeSettings, NotifyException, + ISettings, IThemeSettings, ThemeKey, } from '../../types'; import type { GlobalState } from '../types'; @@ -39,52 +39,51 @@ export function replaceThemeSettings( } export function addNotifyExceptions( - global: T, notifyExceptions: ApiNotifyException[], -): T { - notifyExceptions.forEach((notifyException) => { - const { chatId, ...exceptionData } = notifyException; - global = addNotifyException(global, chatId, exceptionData); - }); - - return global; -} - -export function addNotifyException( - global: T, id: string, notifyException: NotifyException, + global: T, notifyExceptionById: Record, ): T { return { ...global, - settings: { - ...global.settings, - notifyExceptions: { - ...global.settings.notifyExceptions, + chats: { + ...global.chats, + notifyExceptionById: { + ...global.chats.notifyExceptionById, + ...notifyExceptionById, + }, + }, + }; +} + +export function addNotifyException( + global: T, id: string, notifyException: ApiPeerNotifySettings, +): T { + return { + ...global, + chats: { + ...global.chats, + notifyExceptionById: { + ...global.chats.notifyExceptionById, [id]: notifyException, }, }, }; } -// eslint-disable-next-line consistent-return -export function updateNotifySettings( - global: T, peerType: 'contact' | 'group' | 'broadcast', isSilent?: boolean, shouldShowPreviews?: boolean, +export function updateNotifyDefaults( + global: T, peerType: ApiNotifyPeerType, settings: Partial, ): T { - switch (peerType) { - case 'contact': - return replaceSettings(global, { - ...(typeof isSilent !== 'undefined' && { hasPrivateChatsNotifications: !isSilent }), - ...(typeof shouldShowPreviews !== 'undefined' && { hasPrivateChatsMessagePreview: shouldShowPreviews }), - }); - case 'group': - return replaceSettings(global, { - ...(typeof isSilent !== 'undefined' && { hasGroupNotifications: !isSilent }), - ...(typeof shouldShowPreviews !== 'undefined' && { hasGroupMessagePreview: shouldShowPreviews }), - }); - case 'broadcast': - return replaceSettings(global, { - ...(typeof isSilent !== 'undefined' && { hasBroadcastNotifications: !isSilent }), - ...(typeof shouldShowPreviews !== 'undefined' && { hasBroadcastMessagePreview: shouldShowPreviews }), - }); - } + return { + ...global, + settings: { + ...global.settings, + notifyDefaults: { + ...global.settings.notifyDefaults, + [peerType]: { + ...global.settings.notifyDefaults?.[peerType], + ...settings, + }, + }, + }, + }; } export function addBlockedUser(global: T, contactId: string): T { diff --git a/src/global/selectors/settings.ts b/src/global/selectors/settings.ts index db4578ad2..3cdefab5a 100644 --- a/src/global/selectors/settings.ts +++ b/src/global/selectors/settings.ts @@ -1,11 +1,11 @@ import type { GlobalState } from '../types'; -export function selectNotifySettings(global: T) { - return global.settings.byKey; +export function selectNotifyDefaults(global: T) { + return global.settings.notifyDefaults; } -export function selectNotifyExceptions(global: T) { - return global.settings.notifyExceptions; +export function selectNotifyException(global: T, chatId: string) { + return global.chats.notifyExceptionById?.[chatId]; } export function selectLanguageCode(global: T) { @@ -13,7 +13,7 @@ export function selectLanguageCode(global: T) { } export function selectCanSetPasscode(global: T) { - return global.authRememberMe && global.isCacheApiSupported; + return global.authRememberMe; } export function selectTranslationLanguage(global: T) { @@ -27,3 +27,7 @@ export function selectNewNoncontactPeersRequirePremium(gl export function selectShouldHideReadMarks(global: T) { return global.settings.byKey.shouldHideReadMarks; } + +export function selectSettingsKeys(global: T) { + return global.settings.byKey; +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index ef36b17a4..612dd1457 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -25,6 +25,7 @@ import type { ApiMessageSearchContext, ApiNewPoll, ApiNotification, + ApiNotifyPeerType, ApiPaymentStatus, ApiPhoto, ApiPremiumSection, @@ -220,8 +221,8 @@ export interface ActionPayloads { isSilent: boolean; }; updateNotificationSettings: { - peerType: 'contact' | 'group' | 'broadcast'; - isSilent?: boolean; + peerType: ApiNotifyPeerType; + isMuted?: boolean; shouldShowPreviews?: boolean; }; @@ -255,7 +256,7 @@ export interface ActionPayloads { loadCountryList: { langCode?: string; }; - ensureTimeFormat: WithTabId | undefined; + ensureTimeFormat: undefined; // misc loadWebPagePreview: { @@ -365,7 +366,8 @@ export interface ActionPayloads { toggleChatArchived: { id: string; }; - toggleChatUnread: { id: string }; + markChatUnread: { id: string }; + markChatRead: { id: string }; loadChatFolders: undefined; loadRecommendedChatFolders: undefined; editChatFolder: { @@ -1047,7 +1049,7 @@ export interface ActionPayloads { updateChatMutedState: { chatId: string; isMuted?: boolean; - muteUntil?: number; + mutedUntil?: number; }; updateChat: { @@ -1313,9 +1315,16 @@ export interface ActionPayloads { } & WithTabId; focusNextReaction: WithTabId | undefined; focusNextMention: WithTabId | undefined; - readAllReactions: WithTabId | undefined; - readAllMentions: WithTabId | undefined; + readAllReactions: { + chatId: string; + threadId?: ThreadId; + }; + readAllMentions: { + chatId: string; + threadId?: ThreadId; + }; markMentionsRead: { + chatId: string; messageIds: number[]; } & WithTabId; copyMessageLink: { @@ -2505,7 +2514,7 @@ export interface ActionPayloads { chatId: string; topicId: number; isMuted?: boolean; - muteUntil?: number; + mutedUntil?: number; }; setViewForumAsMessages: { chatId: string; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 1ec5dfc60..934cf5e54 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -15,8 +15,10 @@ import type { ApiGroupCall, ApiLanguage, ApiMessage, + ApiNotifyPeerType, ApiPaidReactionPrivacyType, ApiPeerColors, + ApiPeerNotifySettings, ApiPeerPhotos, ApiPeerStories, ApiPhoneCall, @@ -54,7 +56,6 @@ import type { EmojiKeywords, ISettings, IThemeSettings, - NotifyException, PerformanceType, Point, ServiceNotification, @@ -221,6 +222,7 @@ export type GlobalState = { similarChannelIds?: string[]; count?: number; }>>; + notifyExceptionById: Record; similarBotsById: Record; }; @@ -411,7 +413,7 @@ export type GlobalState = { loadedWallpapers?: ApiWallpaper[]; themes: Partial>; privacy: Partial>; - notifyExceptions?: Record; + notifyDefaults?: Record; lastPremiumBandwithNotificationDate?: number; paidReactionPrivacy?: ApiPaidReactionPrivacyType; languages?: ApiLanguage[]; diff --git a/src/hooks/useChatContextActions.ts b/src/hooks/useChatContextActions.ts index 8d49f9d4e..fdffd6386 100644 --- a/src/hooks/useChatContextActions.ts +++ b/src/hooks/useChatContextActions.ts @@ -79,7 +79,8 @@ const useChatContextActions = ({ toggleSavedDialogPinned, updateChatMutedState, toggleChatArchived, - toggleChatUnread, + markChatRead, + markChatUnread, openChatInNewTab, } = getActions(); @@ -149,10 +150,10 @@ const useChatContextActions = ({ } const actionMaskAsRead = (chat.unreadCount || chat.hasUnreadMark) - ? { title: lang('MarkAsRead'), icon: 'readchats', handler: () => toggleChatUnread({ id: chat.id }) } + ? { title: lang('MarkAsRead'), icon: 'readchats', handler: () => markChatRead({ id: chat.id }) } : undefined; const actionMarkAsUnread = !(chat.unreadCount || chat.hasUnreadMark) && !chat.isForum - ? { title: lang('MarkAsUnread'), icon: 'unread', handler: () => toggleChatUnread({ id: chat.id }) } + ? { title: lang('MarkAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) } : undefined; const actionArchive = isChatArchived(chat) diff --git a/src/types/index.ts b/src/types/index.ts index bb7b3fd26..b23efa72b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -76,19 +76,6 @@ export interface IThemeSettings { isBlurred?: boolean; } -export type NotifySettings = { - hasPrivateChatsNotifications?: boolean; - hasPrivateChatsMessagePreview?: boolean; - hasGroupNotifications?: boolean; - hasGroupMessagePreview?: boolean; - hasBroadcastNotifications?: boolean; - hasBroadcastMessagePreview?: boolean; - hasContactJoinedNotifications?: boolean; - hasWebNotifications: boolean; - hasPushNotifications: boolean; - notificationSoundVolume: number; -}; - export type LangCode = ( 'en' | 'ar' | 'be' | 'ca' | 'nl' | 'fr' | 'de' | 'id' | 'it' | 'ko' | 'ms' | 'fa' | 'pl' | 'pt-br' | 'ru' | 'es' | 'tr' | 'uk' | 'uz' @@ -96,7 +83,7 @@ export type LangCode = ( export type TimeFormat = '24h' | '12h'; -export interface ISettings extends NotifySettings, Record { +export interface ISettings { theme: ThemeKey; shouldUseSystemTheme: boolean; messageTextSize: number; @@ -139,6 +126,10 @@ export interface ISettings extends NotifySettings, Record { shouldDebugExportedSenders?: boolean; shouldWarnAboutSvg?: boolean; shouldSkipWebAppCloseConfirmation: boolean; + hasContactJoinedNotifications?: boolean; + hasWebNotifications: boolean; + hasPushNotifications: boolean; + notificationSoundVolume: number; } export type IAnchorPosition = { @@ -461,12 +452,6 @@ export enum ManagementScreens { export type ManagementType = 'user' | 'group' | 'channel' | 'bot'; -export type NotifyException = { - isMuted: boolean; - isSilent?: boolean; - shouldShowPreviews?: boolean; -}; - export type EmojiKeywords = { isLoading?: boolean; version?: number; diff --git a/src/util/folderManager.ts b/src/util/folderManager.ts index 366b3c338..6951d9340 100644 --- a/src/util/folderManager.ts +++ b/src/util/folderManager.ts @@ -3,20 +3,18 @@ import { addCallback } from '../lib/teact/teactn'; import { addActionHandler, getGlobal } from '../global'; import type { - ApiChat, ApiChatFolder, ApiUser, + ApiChat, ApiChatFolder, ApiNotifyPeerType, ApiPeerNotifySettings, ApiUser, } from '../api/types'; import type { GlobalState } from '../global/types'; -import type { NotifyException, NotifySettings } from '../types'; import type { CallbackManager } from './callbacks'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, DEBUG, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID, } from '../config'; -import { selectIsChatMuted } from '../global/helpers'; +import { getIsChatMuted } from '../global/helpers/notifications'; import { selectChatLastMessage, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, selectTabState, selectTopics, } from '../global/selectors'; @@ -79,8 +77,8 @@ let prevGlobal: { chatsById: Record; foldersById: Record; usersById: Record; - notifySettings: NotifySettings; - notifyExceptions?: Record; + notifyDefaults?: Record; + notifyExceptions?: Record; } = initials.prevGlobal; let prepared: { @@ -219,8 +217,8 @@ function updateFolderManager(global: GlobalState) { const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds; const areTopicsChanged = global.chats.topicsInfoById !== prevGlobal.topicsInfoById; const areUsersChanged = global.users.byId !== prevGlobal.usersById; - const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings; - const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions; + const areNotifyDefaultsChanged = selectNotifyDefaults(global) !== prevGlobal.notifyDefaults; + const areNotifyExceptionsChanged = global.chats.notifyExceptionById !== prevGlobal.notifyExceptions; let affectedFolderIds: number[] = []; @@ -232,7 +230,7 @@ function updateFolderManager(global: GlobalState) { if (!( isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged - || areChatsChanged || areUsersChanged || areTopicsChanged || areNotifySettingsChanged || areNotifyExceptionsChanged + || areChatsChanged || areUsersChanged || areTopicsChanged || areNotifyDefaultsChanged || areNotifyExceptionsChanged || areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged ) ) { @@ -252,7 +250,7 @@ function updateFolderManager(global: GlobalState) { affectedFolderIds = affectedFolderIds.concat(updateChats( global, areFoldersChanged || isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged, - areNotifySettingsChanged, + areNotifyDefaultsChanged, areNotifyExceptionsChanged, prevAllFolderListIds, prevArchivedFolderListIds, @@ -411,7 +409,7 @@ function buildFolderSummary(folder: ApiChatFolder): FolderSummary { function updateChats( global: GlobalState, areFoldersChanged: boolean, - areNotifySettingsChanged: boolean, + areNotifyDefaultsChanged: boolean, areNotifyExceptionsChanged: boolean, prevAllFolderListIds?: string[], prevArchivedFolderListIds?: string[], @@ -421,8 +419,8 @@ function updateChats( const newUsersById = global.users.byId; const newAllLastMessageIds = global.chats.lastMessageIds.all; const newSavedLastMessageIds = global.chats.lastMessageIds.saved; - const newNotifySettings = selectNotifySettings(global); - const newNotifyExceptions = selectNotifyExceptions(global); + const newNotifyDefaults = selectNotifyDefaults(global); + const newNotifyExceptions = global.chats.notifyExceptionById; const folderSummaries = Object.values(prepared.folderSummariesById); const affectedFolderIds = new Set(); @@ -445,7 +443,7 @@ function updateChats( if ( !areFoldersChanged - && !areNotifySettingsChanged + && !areNotifyDefaultsChanged && !areNotifyExceptionsChanged && chat === prevGlobal.chatsById[chatId] && newUsersById[chatId] === prevGlobal.usersById[chatId] @@ -463,7 +461,7 @@ function updateChats( const newSummary = buildChatSummary( global, chat, - newNotifySettings, + newNotifyDefaults, newNotifyExceptions, newUsersById[chatId], isRemovedFromAll, @@ -500,7 +498,7 @@ function updateChats( prevGlobal.usersById = newUsersById; prevGlobal.lastAllMessageIds = newAllLastMessageIds; prevGlobal.lastSavedMessageIds = newSavedLastMessageIds; - prevGlobal.notifySettings = newNotifySettings; + prevGlobal.notifyDefaults = newNotifyDefaults; prevGlobal.notifyExceptions = newNotifyExceptions; return Array.from(affectedFolderIds); @@ -509,8 +507,8 @@ function updateChats( function buildChatSummary( global: T, chat: ApiChat, - notifySettings: NotifySettings, - notifyExceptions?: Record, + notifyDefaults?: Record, + notifyExceptions?: Record, user?: ApiUser, isRemovedFromAll?: boolean, isRemovedFromSaved?: boolean, @@ -548,7 +546,7 @@ function buildChatSummary( isListedInAll: Boolean(!isRestricted && !isNotJoined && !migratedTo && !shouldHideServiceChat && !isRemovedFromAll), isListedInSaved: !isRemovedFromSaved, isArchived: folderId === ARCHIVED_FOLDER_ID, - isMuted: selectIsChatMuted(chat, notifySettings, notifyExceptions), + isMuted: getIsChatMuted(chat, notifyDefaults, notifyExceptions?.[chat.id]), isUnread: Boolean(unreadCount || unreadMentionsCount || hasUnreadMark), unreadCount, unreadMentionsCount, @@ -810,8 +808,6 @@ function buildInitials() { chatsById: {}, usersById: {}, topicsInfoById: {}, - notifySettings: {} as NotifySettings, - notifyExceptions: {}, }, prepared: { diff --git a/src/util/notifications.tsx b/src/util/notifications.tsx index a1b10c843..e3e85ef12 100644 --- a/src/util/notifications.tsx +++ b/src/util/notifications.tsx @@ -16,16 +16,16 @@ import { getPrivateChatUserId, getUserFullName, isChatChannel, - selectIsChatMuted, - selectShouldShowMessagePreview, } from '../global/helpers'; -import { addNotifyExceptions, replaceSettings } from '../global/reducers'; +import { getIsChatMuted, getIsChatSilent, getShouldShowMessagePreview } from '../global/helpers/notifications'; import { selectChat, selectCurrentMessageList, selectIsChatWithSelf, - selectNotifyExceptions, - selectNotifySettings, + selectNotifyDefaults, + selectNotifyException, + selectSettingsKeys, + selectTopicFromMessage, selectUser, } from '../global/selectors'; import { callApi } from '../api/gramjs'; @@ -34,6 +34,7 @@ import { buildCollectionByKey } from './iteratees'; import * as mediaLoader from './mediaLoader'; import { oldTranslate } from './oldLangProvider'; import { debounce } from './schedulers'; +import { getServerTime } from './serverTime'; import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './windowEnvironment'; import MessageSummary from '../components/common/MessageSummary'; @@ -107,7 +108,7 @@ notificationSound.setAttribute('mozaudiochannel', 'notification'); export async function playNotifySound(id?: string, volume?: number) { if (id !== undefined && soundPlayedIds.has(id)) return; - const { notificationSoundVolume } = selectNotifySettings(getGlobal()); + const { notificationSoundVolume } = selectSettingsKeys(getGlobal()); const currentVolume = volume ? volume / 10 : notificationSoundVolume / 10; if (currentVolume === 0) return; notificationSound.volume = currentVolume; @@ -181,27 +182,6 @@ export async function unsubscribe() { await unsubscribeFromPush(subscription); } -// Indicates if notification settings are loaded from the api -let areSettingsLoaded = false; - -// Load notification settings from the api -async function loadNotificationSettings() { - if (areSettingsLoaded) return selectNotifySettings(getGlobal()); - const [resultSettings, resultExceptions] = await Promise.all([ - callApi('fetchNotificationSettings'), - callApi('fetchNotificationExceptions'), - ]); - if (!resultSettings) return selectNotifySettings(getGlobal()); - - let global = replaceSettings(getGlobal(), resultSettings); - if (resultExceptions) { - global = addNotifyExceptions(global, resultExceptions); - } - setGlobal(global); - areSettingsLoaded = true; - return selectNotifySettings(global); -} - // Load custom emoji from the api if it's not cached already async function loadCustomEmoji(id: string) { let global = getGlobal(); @@ -293,9 +273,12 @@ export async function subscribe() { } function checkIfShouldNotify(chat: ApiChat, message: Partial) { - if (!areSettingsLoaded) return false; const global = getGlobal(); - const isMuted = selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); + const isChatMuted = getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)); + const topic = selectTopicFromMessage(global, message as ApiMessage); + const topicMutedUntil = topic?.notifySettings.mutedUntil; + const isMuted = topicMutedUntil === undefined ? isChatMuted : topicMutedUntil > getServerTime(); + const shouldNotifyAboutMessage = message.content?.action?.type !== 'phoneCall'; if (isMuted || !shouldNotifyAboutMessage || chat.isNotJoined || !chat.isListed || selectIsChatWithSelf(global, chat.id)) { @@ -330,7 +313,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A let body: string; if ( !isScreenLocked - && selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global)) + && getShouldShowMessagePreview(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)) ) { const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId); @@ -386,7 +369,7 @@ export async function notifyAboutCall({ }: { call: ApiPhoneCall; user: ApiUser; }) { - const { hasWebNotifications } = await loadNotificationSettings(); + const { hasWebNotifications } = selectSettingsKeys(getGlobal()); if (document.hasFocus() || !hasWebNotifications) return; const areNotificationsSupported = checkIfNotificationsSupported(); if (!areNotificationsSupported) return; @@ -420,11 +403,18 @@ export async function notifyAboutMessage({ message, isReaction = false, }: { chat: ApiChat; message: Partial; isReaction?: boolean }) { - const { hasWebNotifications } = await loadNotificationSettings(); + const global = getGlobal(); + const { hasWebNotifications } = selectSettingsKeys(global); if (!checkIfShouldNotify(chat, message)) return; + const isChatSilent = getIsChatSilent( + chat, selectNotifyDefaults(getGlobal()), selectNotifyException(getGlobal(), chat.id), + ); + const topic = selectTopicFromMessage(global, message as ApiMessage); + const isSilent = topic?.notifySettings.hasSound === undefined ? isChatSilent : !topic.notifySettings.hasSound; + const areNotificationsSupported = checkIfNotificationsSupported(); if (!hasWebNotifications || !areNotificationsSupported) { - if (!message.isSilent && !isReaction && !IS_ELECTRON) { + if (!isSilent && !message.isSilent && !isReaction && !IS_ELECTRON) { // Only play sound if web notifications are disabled playNotifySoundDebounced(String(message.id) || chat.id); } @@ -463,7 +453,7 @@ export async function notifyAboutMessage({ chatId: chat.id, messageId: message.id, shouldReplaceHistory: true, - isSilent: message.isSilent, + isSilent: isSilent || message.isSilent, reaction: activeReaction?.reaction, }, });