Notification: Fix sound playback conditions (#5734)

This commit is contained in:
zubiden 2025-03-21 14:02:16 +04:00 committed by Alexander Zinchuk
parent 37eb65344c
commit a65d6e0697
44 changed files with 620 additions and 537 deletions

View File

@ -24,13 +24,14 @@ import type {
} from '../../types'; } from '../../types';
import { pick, pickTruthy } from '../../../util/iteratees'; import { pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { getServerTimeOffset } from '../../../util/serverTime';
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc'; import { serializeBytes } from '../helpers/misc';
import { import {
buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId,
} from './common'; } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiPeerNotifySettings } from './misc';
import { import {
buildApiEmojiStatus, buildApiEmojiStatus,
buildApiPeerColor, buildApiPeerColor,
@ -119,10 +120,8 @@ export function buildApiChatFromDialog(
): ApiChat { ): ApiChat {
const { const {
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount, peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount,
notifySettings: { silent, muteUntil },
readOutboxMaxId, readInboxMaxId, draft, viewForumAsMessages, readOutboxMaxId, readInboxMaxId, draft, viewForumAsMessages,
} = dialog; } = dialog;
const isMuted = silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil);
return { return {
id: getApiChatIdFromMtpPeer(peer), id: getApiChatIdFromMtpPeer(peer),
@ -134,8 +133,6 @@ export function buildApiChatFromDialog(
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
unreadReactionsCount, unreadReactionsCount,
isMuted,
muteUntil,
...(unreadMark && { hasUnreadMark: true }), ...(unreadMark && { hasUnreadMark: true }),
...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }), ...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }),
...(viewForumAsMessages && { isForumAsMessages: true }), ...(viewForumAsMessages && { isForumAsMessages: true }),
@ -579,9 +576,7 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
unreadMentionsCount, unreadMentionsCount,
unreadReactionsCount, unreadReactionsCount,
fromId, fromId,
notifySettings: { notifySettings,
silent, muteUntil,
},
} = forumTopic; } = forumTopic;
return { return {
@ -600,8 +595,7 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
unreadMentionsCount, unreadMentionsCount,
unreadReactionsCount, unreadReactionsCount,
fromId: getApiChatIdFromMtpPeer(fromId), fromId: getApiChatIdFromMtpPeer(fromId),
isMuted: silent || (typeof muteUntil === 'number' ? getServerTime() < muteUntil : undefined), notifySettings: buildApiPeerNotifySettings(notifySettings),
muteUntil,
}; };
} }

View File

@ -163,6 +163,7 @@ export function buildApiMessageFromNotification(
chatId: SERVICE_NOTIFICATIONS_USER_ID, chatId: SERVICE_NOTIFICATIONS_USER_ID,
date: notification.inboxDate || currentDate, date: notification.inboxDate || currentDate,
content, content,
isInvertedMedia: notification.invertMedia,
isOutgoing: false, isOutgoing: false,
}; };
} }

View File

@ -8,6 +8,7 @@ import type {
ApiLanguage, ApiLanguage,
ApiOldLangString, ApiOldLangString,
ApiPeerColors, ApiPeerColors,
ApiPeerNotifySettings,
ApiPrivacyKey, ApiPrivacyKey,
ApiSession, ApiSession,
ApiTimezone, ApiTimezone,
@ -21,7 +22,6 @@ import { numberToHexColor } from '../../../util/colors';
import { import {
buildCollectionByCallback, omit, omitUndefined, pick, buildCollectionByCallback, omit, omitUndefined, pick,
} from '../../../util/iteratees'; } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { addUserToLocalDb } from '../helpers/localDb'; import { addUserToLocalDb } from '../helpers/localDb';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildMessageTextContent } from './messageContent'; import { buildApiDocument, buildMessageTextContent } from './messageContent';
@ -106,40 +106,20 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und
return undefined; return undefined;
} }
export function buildApiNotifyException( export function buildApiPeerNotifySettings(
notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer, notifySettings: GramJs.TypePeerNotifySettings,
) { ): ApiPeerNotifySettings {
const { const {
silent, muteUntil, showPreviews, otherSound, silent, muteUntil, showPreviews, otherSound,
} = notifySettings; } = notifySettings;
const hasSound = Boolean(otherSound && !(otherSound instanceof GramJs.NotificationSoundNone)); const hasSound = !(otherSound instanceof GramJs.NotificationSoundNone);
return { return {
chatId: getApiChatIdFromMtpPeer(peer), hasSound,
isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), isSilentPosting: silent,
...(!hasSound && { isSilent: true }), mutedUntil: muteUntil,
...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), shouldShowPreviews: 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,
}; };
} }

View File

@ -14,6 +14,7 @@ import type {
ApiMessage, ApiMessage,
ApiMissingInvitedUser, ApiMissingInvitedUser,
ApiPeer, ApiPeer,
ApiPeerNotifySettings,
ApiPhoto, ApiPhoto,
ApiTopic, ApiTopic,
ApiUser, ApiUser,
@ -31,7 +32,7 @@ import {
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
TOPICS_SLICE, TOPICS_SLICE,
} from '../../../config'; } from '../../../config';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey, omitUndefined } from '../../../util/iteratees';
import { import {
buildApiChatBotCommands, buildApiChatBotCommands,
buildApiChatFolder, buildApiChatFolder,
@ -52,6 +53,7 @@ import {
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common'; import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common';
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiPeerNotifySettings } from '../apiBuilders/misc';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildStickerSet } from '../apiBuilders/symbols'; import { buildStickerSet } from '../apiBuilders/symbols';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users'; import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
@ -96,6 +98,7 @@ type ChatListData = {
orderedPinnedIds: string[] | undefined; orderedPinnedIds: string[] | undefined;
totalChatCount: number; totalChatCount: number;
messages: ApiMessage[]; messages: ApiMessage[];
notifyExceptionById: Record<string, ApiPeerNotifySettings>;
lastMessageByChatId: Record<string, number>; lastMessageByChatId: Record<string, number>;
nextOffsetId?: number; nextOffsetId?: number;
nextOffsetPeerId?: string; nextOffsetPeerId?: string;
@ -150,6 +153,7 @@ export async function fetchChats({
const chats: ApiChat[] = []; const chats: ApiChat[] = [];
const draftsById: Record<string, ApiDraft> = {}; const draftsById: Record<string, ApiDraft> = {};
const notifyExceptionById: Record<string, ApiPeerNotifySettings> = {};
const dialogs = (resultPinned?.dialogs || []).concat(result.dialogs); const dialogs = (resultPinned?.dialogs || []).concat(result.dialogs);
@ -186,7 +190,12 @@ export async function fetchChats({
chats.push(chat); 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) { if (withPinned && dialog.pinned) {
orderedPinnedIds.push(chat.id); orderedPinnedIds.push(chat.id);
@ -229,6 +238,7 @@ export async function fetchChats({
totalChatCount, totalChatCount,
lastMessageByChatId, lastMessageByChatId,
messages, messages,
notifyExceptionById,
nextOffsetId, nextOffsetId,
nextOffsetPeerId, nextOffsetPeerId,
nextOffsetDate, nextOffsetDate,
@ -327,6 +337,7 @@ export async function fetchSavedChats({
lastMessageByChatId, lastMessageByChatId,
messages, messages,
draftsById: {}, draftsById: {},
notifyExceptionById: {},
nextOffsetId, nextOffsetId,
nextOffsetPeerId, nextOffsetPeerId,
nextOffsetDate, nextOffsetDate,
@ -474,7 +485,9 @@ export async function requestChatUpdate({
applyState(result.state); applyState(result.state);
scheduleMutedChatUpdate(chatUpdate.id, chatUpdate.muteUntil, sendApiUpdate); const notifySettings = buildApiPeerNotifySettings(dialog.notifySettings);
scheduleMutedChatUpdate(chatUpdate.id, notifySettings.mutedUntil, sendApiUpdate);
} }
export function saveDraft({ export function saveDraft({
@ -722,25 +735,27 @@ async function getFullChannelInfo(
}; };
} }
export async function updateChatMutedState({ export function updateChatMutedState({
chat, isMuted, muteUntil = 0, chat, isMuted, mutedUntil = 0,
}: { }: {
chat: ApiChat; isMuted: boolean; muteUntil?: number; chat: ApiChat; isMuted?: boolean; mutedUntil?: number;
}) { }) {
if (isMuted && !muteUntil) { if (isMuted && !mutedUntil) {
muteUntil = MAX_INT_32; mutedUntil = MAX_INT_32;
} }
await invokeRequest(new GramJs.account.UpdateNotifySettings({ invokeRequest(new GramJs.account.UpdateNotifySettings({
peer: new GramJs.InputNotifyPeer({ peer: new GramJs.InputNotifyPeer({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
}), }),
settings: new GramJs.InputPeerNotifySettings({ muteUntil }), settings: new GramJs.InputPeerNotifySettings({ muteUntil: mutedUntil }),
})); }));
sendApiUpdate({ sendApiUpdate({
'@type': 'updateNotifyExceptions', '@type': 'updateChatNotifySettings',
chatId: chat.id, chatId: chat.id,
isMuted, settings: {
mutedUntil,
},
}); });
void requestChatUpdate({ void requestChatUpdate({
@ -749,27 +764,29 @@ export async function updateChatMutedState({
}); });
} }
export async function updateTopicMutedState({ export function updateTopicMutedState({
chat, topicId, isMuted, muteUntil = 0, chat, topicId, isMuted, mutedUntil = 0,
}: { }: {
chat: ApiChat; topicId: number; isMuted: boolean; muteUntil?: number; chat: ApiChat; topicId: number; isMuted?: boolean; mutedUntil?: number;
}) { }) {
if (isMuted && !muteUntil) { if (isMuted && !mutedUntil) {
muteUntil = MAX_INT_32; mutedUntil = MAX_INT_32;
} }
await invokeRequest(new GramJs.account.UpdateNotifySettings({ invokeRequest(new GramJs.account.UpdateNotifySettings({
peer: new GramJs.InputNotifyForumTopic({ peer: new GramJs.InputNotifyForumTopic({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: topicId, topMsgId: topicId,
}), }),
settings: new GramJs.InputPeerNotifySettings({ muteUntil }), settings: new GramJs.InputPeerNotifySettings({ muteUntil: mutedUntil }),
})); }));
sendApiUpdate({ sendApiUpdate({
'@type': 'updateTopicNotifyExceptions', '@type': 'updateTopicNotifySettings',
chatId: chat.id, chatId: chat.id,
topicId, topicId,
isMuted, settings: {
mutedUntil,
},
}); });
// TODO[forums] Request forum topic thread update // TODO[forums] Request forum topic thread update

View File

@ -1834,11 +1834,14 @@ export async function reportSponsoredMessage({
export async function readAllMentions({ export async function readAllMentions({
chat, chat,
threadId,
}: { }: {
chat: ApiChat; chat: ApiChat;
threadId?: ThreadId;
}) { }) {
const result = await invokeRequest(new GramJs.messages.ReadMentions({ const result = await invokeRequest(new GramJs.messages.ReadMentions({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
})); }));
if (!result) return; if (!result) return;
@ -1846,17 +1849,20 @@ export async function readAllMentions({
processAffectedHistory(chat, result); processAffectedHistory(chat, result);
if (result.offset) { if (result.offset) {
await readAllMentions({ chat }); await readAllMentions({ chat, threadId });
} }
} }
export async function readAllReactions({ export async function readAllReactions({
chat, chat,
threadId,
}: { }: {
chat: ApiChat; chat: ApiChat;
threadId?: ThreadId;
}) { }) {
const result = await invokeRequest(new GramJs.messages.ReadReactions({ const result = await invokeRequest(new GramJs.messages.ReadReactions({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
})); }));
if (!result) return; if (!result) return;
@ -1864,14 +1870,15 @@ export async function readAllReactions({
processAffectedHistory(chat, result); processAffectedHistory(chat, result);
if (result.offset) { if (result.offset) {
await readAllReactions({ chat }); await readAllReactions({ chat, threadId });
} }
} }
export async function fetchUnreadMentions({ export async function fetchUnreadMentions({
chat, ...pagination chat, threadId, ...pagination
}: { }: {
chat: ApiChat; chat: ApiChat;
threadId?: ThreadId;
offsetId?: number; offsetId?: number;
addOffset?: number; addOffset?: number;
maxId?: number; maxId?: number;
@ -1879,6 +1886,7 @@ export async function fetchUnreadMentions({
}) { }) {
const result = await invokeRequest(new GramJs.messages.GetUnreadMentions({ const result = await invokeRequest(new GramJs.messages.GetUnreadMentions({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
limit: MENTION_UNREAD_SLICE, limit: MENTION_UNREAD_SLICE,
...pagination, ...pagination,
})); }));
@ -1899,9 +1907,10 @@ export async function fetchUnreadMentions({
} }
export async function fetchUnreadReactions({ export async function fetchUnreadReactions({
chat, ...pagination chat, threadId, ...pagination
}: { }: {
chat: ApiChat; chat: ApiChat;
threadId?: ThreadId;
offsetId?: number; offsetId?: number;
addOffset?: number; addOffset?: number;
maxId?: number; maxId?: number;
@ -1909,6 +1918,7 @@ export async function fetchUnreadReactions({
}) { }) {
const result = await invokeRequest(new GramJs.messages.GetUnreadReactions({ const result = await invokeRequest(new GramJs.messages.GetUnreadReactions({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
limit: REACTION_UNREAD_SLICE, limit: REACTION_UNREAD_SLICE,
...pagination, ...pagination,
})); }));

View File

@ -8,7 +8,8 @@ import type {
ApiConfig, ApiConfig,
ApiInputPrivacyRules, ApiInputPrivacyRules,
ApiLanguage, ApiLanguage,
ApiNotifyException, ApiNotifyPeerType,
ApiPeerNotifySettings,
ApiPhoto, ApiPhoto,
ApiPrivacyKey, ApiPrivacyKey,
ApiUser, ApiUser,
@ -21,15 +22,14 @@ import {
MAX_INT_32, MAX_INT_32,
} from '../../../config'; } from '../../../config';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { buildAppConfig } from '../apiBuilders/appConfig'; import { buildAppConfig } from '../apiBuilders/appConfig';
import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common'; import { buildApiPhoto, buildPrivacyRules } from '../apiBuilders/common';
import { import {
buildApiConfig, buildApiConfig,
buildApiCountryList, buildApiCountryList,
buildApiLanguage, buildApiLanguage,
buildApiNotifyException,
buildApiPeerColors, buildApiPeerColors,
buildApiPeerNotifySettings,
buildApiSession, buildApiSession,
buildApiTimezone, buildApiTimezone,
buildApiWallpaper, buildApiWallpaper,
@ -323,20 +323,26 @@ export async function fetchNotificationExceptions() {
return acc; return acc;
} }
acc.push(buildApiNotifyException(update.notifySettings, update.peer.peer)); const peerId = getApiChatIdFromMtpPeer(update.peer.peer);
acc[peerId] = buildApiPeerNotifySettings(update.notifySettings);
return acc; return acc;
}, [] as ApiNotifyException[]); }, {} as Record<string, ApiPeerNotifySettings>);
} }
export async function fetchNotificationSettings() { export async function fetchContactSignUpSetting() {
const hasContactJoinedNotifications = await invokeRequest(new GramJs.account.GetContactSignUpNotification());
return hasContactJoinedNotifications;
}
export async function fetchNotifyDefaultSettings() {
const [ const [
isMutedContactSignUpNotification, usersSettings,
privateContactNotificationsSettings, groupsSettings,
groupNotificationsSettings, channelsSettings,
broadcastNotificationsSettings,
] = await Promise.all([ ] = await Promise.all([
invokeRequest(new GramJs.account.GetContactSignUpNotification()),
invokeRequest(new GramJs.account.GetNotifySettings({ invokeRequest(new GramJs.account.GetNotifySettings({
peer: new GramJs.InputNotifyUsers(), peer: new GramJs.InputNotifyUsers(),
})), })),
@ -348,37 +354,14 @@ export async function fetchNotificationSettings() {
})), })),
]); ]);
if (!privateContactNotificationsSettings || !groupNotificationsSettings || !broadcastNotificationsSettings) { if (!usersSettings || !groupsSettings || !channelsSettings) {
return false; 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 { return {
hasContactJoinedNotifications: !isMutedContactSignUpNotification, users: buildApiPeerNotifySettings(usersSettings),
hasPrivateChatsNotifications: !( groups: buildApiPeerNotifySettings(groupsSettings),
privateSilent channels: buildApiPeerNotifySettings(channelsSettings),
|| (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,
}; };
} }
@ -386,17 +369,17 @@ export function updateContactSignUpNotification(isSilent: boolean) {
return invokeRequest(new GramJs.account.SetContactSignUpNotification({ silent: isSilent })); return invokeRequest(new GramJs.account.SetContactSignUpNotification({ silent: isSilent }));
} }
export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broadcast', { export function updateNotificationSettings(peerType: ApiNotifyPeerType, {
isSilent, isMuted,
shouldShowPreviews, shouldShowPreviews,
}: { }: {
isSilent?: boolean; isMuted?: boolean;
shouldShowPreviews?: boolean; shouldShowPreviews?: boolean;
}) { }) {
let peer: GramJs.TypeInputNotifyPeer; let peer: GramJs.TypeInputNotifyPeer;
if (peerType === 'contact') { if (peerType === 'users') {
peer = new GramJs.InputNotifyUsers(); peer = new GramJs.InputNotifyUsers();
} else if (peerType === 'group') { } else if (peerType === 'groups') {
peer = new GramJs.InputNotifyChats(); peer = new GramJs.InputNotifyChats();
} else { } else {
peer = new GramJs.InputNotifyBroadcasts(); peer = new GramJs.InputNotifyBroadcasts();
@ -404,8 +387,7 @@ export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broa
const settings = { const settings = {
showPreviews: shouldShowPreviews, showPreviews: shouldShowPreviews,
silent: isSilent, muteUntil: isMuted ? MAX_INT_32 : 0,
muteUntil: isSilent ? MAX_INT_32 : 0,
}; };
return invokeRequest(new GramJs.account.UpdateNotifySettings({ return invokeRequest(new GramJs.account.UpdateNotifySettings({

View File

@ -3,7 +3,7 @@ import type { OnApiUpdate } from '../types';
import { MAX_INT_32 } from '../../config'; import { MAX_INT_32 } from '../../config';
import { getServerTime } from '../../util/serverTime'; 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<string, any>(); const unmuteTimers = new Map<string, any>();
const unmuteQueue: Array<UnmuteQueueItem> = []; const unmuteQueue: Array<UnmuteQueueItem> = [];
const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => { const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => {
@ -12,9 +12,9 @@ const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) =>
clearTimeout(unmuteTimers.get(id)); clearTimeout(unmuteTimers.get(id));
unmuteTimers.delete(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.push(item);
unmuteQueue.sort((a, b) => b.muteUntil - a.muteUntil); unmuteQueue.sort((a, b) => b.mutedUntil - a.mutedUntil);
const next = unmuteQueue.pop(); const next = unmuteQueue.pop();
if (!next) return; if (!next) return;
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -23,30 +23,34 @@ const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) =>
const afterNext = unmuteQueue.pop(); const afterNext = unmuteQueue.pop();
if (afterNext) scheduleUnmute(afterNext, onUpdate); if (afterNext) scheduleUnmute(afterNext, onUpdate);
} }
}, (item.muteUntil - getServerTime()) * 1000); }, (item.mutedUntil - getServerTime()) * 1000);
unmuteTimers.set(id, timer); unmuteTimers.set(id, timer);
}; };
export function scheduleMutedChatUpdate(chatId: string, muteUntil = 0, onUpdate: OnApiUpdate) { export function scheduleMutedChatUpdate(chatId: string, mutedUntil = 0, onUpdate: OnApiUpdate) {
scheduleUnmute({ scheduleUnmute({
chatId, chatId,
muteUntil, mutedUntil,
}, () => onUpdate({ }, () => onUpdate({
'@type': 'updateNotifyExceptions', '@type': 'updateChatNotifySettings',
chatId, 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({ scheduleUnmute({
chatId, chatId,
topicId, topicId,
muteUntil, mutedUntil,
}, () => onUpdate({ }, () => onUpdate({
'@type': 'updateTopicNotifyExceptions', '@type': 'updateTopicNotifySettings',
chatId, chatId,
topicId, topicId,
isMuted: false, settings: {
mutedUntil: 0,
},
})); }));
} }

View File

@ -47,8 +47,7 @@ import {
buildMessageDraft, buildMessageDraft,
} from '../apiBuilders/messages'; } from '../apiBuilders/messages';
import { import {
buildApiNotifyException, buildApiPeerNotifySettings,
buildApiNotifyExceptionTopic,
buildLangStrings, buildLangStrings,
buildPrivacyKey, buildPrivacyKey,
} from '../apiBuilders/misc'; } from '../apiBuilders/misc';
@ -631,28 +630,6 @@ export function updater(update: Update) {
messageIds: update.messages, messageIds: update.messages,
isPinned: update.pinned, 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 ( } else if (
update instanceof GramJs.UpdateUserTyping update instanceof GramJs.UpdateUserTyping
|| update instanceof GramJs.UpdateChatUserTyping || update instanceof GramJs.UpdateChatUserTyping
@ -837,18 +814,41 @@ export function updater(update: Update) {
// Settings // Settings
} else if (update instanceof GramJs.UpdateNotifySettings) { } else if (update instanceof GramJs.UpdateNotifySettings) {
const { const {
notifySettings: { notifySettings,
showPreviews, silent, muteUntil, peer: notifyPeer,
},
peer: { className },
} = update; } = 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' const peerType = className === 'NotifyUsers'
? 'contact' ? 'users'
: (className === 'NotifyChats' : (className === 'NotifyChats'
? 'group' ? 'groups'
: (className === 'NotifyBroadcasts' : (className === 'NotifyBroadcasts'
? 'broadcast' ? 'channels'
: undefined : undefined
) )
); );
@ -858,11 +858,9 @@ export function updater(update: Update) {
} }
sendApiUpdate({ sendApiUpdate({
'@type': 'updateNotifySettings', '@type': 'updateDefaultNotifySettings',
peerType, peerType,
isSilent: Boolean(silent settings,
|| (typeof muteUntil === 'number' && Date.now() + getServerTimeOffset() * 1000 < muteUntil * 1000)),
shouldShowPreviews: Boolean(showPreviews),
}); });
} else if (update instanceof GramJs.UpdatePeerBlocked) { } else if (update instanceof GramJs.UpdatePeerBlocked) {
sendApiUpdate({ sendApiUpdate({

View File

@ -2,7 +2,7 @@ import type { ApiBotCommand } from './bots';
import type { import type {
ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet,
} from './messages'; } from './messages';
import type { ApiBotVerification, ApiChatInviteImporter } from './misc'; import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings } from './misc';
import type { import type {
ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername, ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername,
} from './users'; } from './users';
@ -27,8 +27,6 @@ export interface ApiChat {
unreadMentionsCount?: number; unreadMentionsCount?: number;
unreadReactionsCount?: number; unreadReactionsCount?: number;
isVerified?: true; isVerified?: true;
isMuted?: boolean;
muteUntil?: number;
areSignaturesShown?: boolean; areSignaturesShown?: boolean;
areProfilesShown?: boolean; areProfilesShown?: boolean;
hasPrivateLink?: boolean; hasPrivateLink?: boolean;
@ -262,8 +260,7 @@ export interface ApiTopic {
unreadMentionsCount: number; unreadMentionsCount: number;
unreadReactionsCount: number; unreadReactionsCount: number;
fromId: string; fromId: string;
isMuted?: boolean; notifySettings: ApiPeerNotifySettings;
muteUntil?: number;
} }
export interface ApiChatlistInviteNew { export interface ApiChatlistInviteNew {

View File

@ -106,13 +106,6 @@ export interface ApiSessionData {
isTest?: true; isTest?: true;
} }
export type ApiNotifyException = {
chatId: string;
isMuted: boolean;
isSilent?: boolean;
shouldShowPreviews?: boolean;
};
export type ApiNotification = { export type ApiNotification = {
localId: string; localId: string;
containerSelector?: string; containerSelector?: string;
@ -352,3 +345,12 @@ export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
export type ApiLimitTypeForPromo = Exclude<ApiLimitType, export type ApiLimitTypeForPromo = Exclude<ApiLimitType,
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned' 'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned'
>; >;
export type ApiPeerNotifySettings = {
mutedUntil?: number;
hasSound?: boolean;
isSilentPosting?: boolean;
shouldShowPreviews?: boolean;
};
export type ApiNotifyPeerType = 'users' | 'groups' | 'channels';

View File

@ -35,7 +35,11 @@ import type {
BoughtPaidMedia, BoughtPaidMedia,
} from './messages'; } from './messages';
import type { import type {
ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData, ApiEmojiInteraction,
ApiError,
ApiNotifyPeerType,
ApiPeerNotifySettings,
ApiSessionData,
} from './misc'; } from './misc';
import type { ApiStarsAmount } from './payments'; import type { ApiStarsAmount } from './payments';
import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings';
@ -501,21 +505,24 @@ export type ApiUpdateTwoFaError = {
messageKey: RegularLangFnParameters; messageKey: RegularLangFnParameters;
}; };
export type ApiUpdateNotifySettings = { export type ApiUpdateDefaultNotifySettings = {
'@type': 'updateNotifySettings'; '@type': 'updateDefaultNotifySettings';
peerType: 'contact' | 'group' | 'broadcast'; peerType: ApiNotifyPeerType;
isSilent: boolean; settings: Partial<ApiPeerNotifySettings>;
shouldShowPreviews: boolean;
}; };
export type ApiUpdateNotifyExceptions = { export type ApiUpdatePeerNotifySettings = {
'@type': 'updateNotifyExceptions'; '@type': 'updateChatNotifySettings';
} & ApiNotifyException; chatId: string;
settings: Partial<ApiPeerNotifySettings>;
};
export type ApiUpdateTopicNotifyExceptions = { export type ApiUpdateTopicNotifySettings = {
'@type': 'updateTopicNotifyExceptions'; '@type': 'updateTopicNotifySettings';
chatId: string;
topicId: number; topicId: number;
} & ApiNotifyException; settings: Partial<ApiPeerNotifySettings>;
};
export type ApiUpdateTwoFaStateWaitCode = { export type ApiUpdateTwoFaStateWaitCode = {
'@type': 'updateTwoFaStateWaitCode'; '@type': 'updateTwoFaStateWaitCode';
@ -823,14 +830,14 @@ export type ApiUpdate = (
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifySettings | ApiUpdatePinnedTopic |
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses |
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction |
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |

View File

@ -22,15 +22,15 @@ import {
getHasAdminRight, getHasAdminRight,
isChatChannel, isChatChannel,
isUserRightBanned, isUserRightBanned,
selectIsChatMuted,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import { import {
selectBotAppPermissions, selectBotAppPermissions,
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectCurrentMessageList, selectCurrentMessageList,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings, selectNotifyException,
selectTopicLink, selectTopicLink,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
@ -419,7 +419,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
<Switcher <Switcher
id="group-notifications" id="group-notifications"
label={userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'} label={userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'}
checked={isMuted} checked={!isMuted}
inactive inactive
/> />
</ListItem> </ListItem>
@ -487,7 +487,7 @@ export default memo(withGlobal<OwnProps>(
const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined; const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined;
const botAppPermissions = chatOrUserId ? selectBotAppPermissions(global, chatOrUserId) : undefined; const botAppPermissions = chatOrUserId ? selectBotAppPermissions(global, chatOrUserId) : undefined;
const isForum = chat?.isForum; 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 { threadId } = selectCurrentMessageList(global) || {};
const topicId = isForum && threadId ? Number(threadId) : undefined; const topicId = isForum && threadId ? Number(threadId) : undefined;

View File

@ -51,16 +51,16 @@ const MuteChatModal: FC<OwnProps> = ({
], [lang]); ], [lang]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
let muteUntil: number; let mutedUntil: number;
if (muteUntilOption === MuteDuration.Forever) { if (muteUntilOption === MuteDuration.Forever) {
muteUntil = MAX_INT_32; mutedUntil = MAX_INT_32;
} else { } else {
muteUntil = Math.floor(Date.now() / 1000) + Number(muteUntilOption); mutedUntil = Math.floor(Date.now() / 1000) + Number(muteUntilOption);
} }
if (topicId) { if (topicId) {
updateTopicMutedState({ chatId, topicId, muteUntil }); updateTopicMutedState({ chatId, topicId, mutedUntil });
} else { } else {
updateChatMutedState({ chatId, muteUntil }); updateChatMutedState({ chatId, mutedUntil });
} }
onClose(); onClose();
}, [chatId, muteUntilOption, onClose, topicId]); }, [chatId, muteUntilOption, onClose, topicId]);

View File

@ -23,8 +23,8 @@ import {
groupStatefulContent, groupStatefulContent,
isUserId, isUserId,
isUserOnline, isUserOnline,
selectIsChatMuted,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChat, selectChat,
@ -35,8 +35,8 @@ import {
selectDraft, selectDraft,
selectIsForumPanelClosed, selectIsForumPanelClosed,
selectIsForumPanelOpen, selectIsForumPanelOpen,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings, selectNotifyException,
selectOutgoingStatus, selectOutgoingStatus,
selectPeer, selectPeer,
selectPeerStory, selectPeerStory,
@ -470,7 +470,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)),
lastMessageSender, lastMessageSender,
draft: selectDraft(global, chatId, MAIN_THREAD_ID), draft: selectDraft(global, chatId, MAIN_THREAD_ID),
isSelected, isSelected,

View File

@ -6,6 +6,7 @@ import type { ApiChat, ApiTopic } from '../../../api/types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { getServerTime } from '../../../util/serverTime';
import { isSignal } from '../../../util/signals'; import { isSignal } from '../../../util/signals';
import { formatIntegerCompact } from '../../../util/textFormat'; import { formatIntegerCompact } from '../../../util/textFormat';
import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -62,20 +63,29 @@ const ChatBadge: FC<OwnProps> = ({
isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined
), [topics, isForum]); ), [topics, isForum]);
const unreadCount = useMemo(() => ( const unreadCount = useMemo(() => {
isForum if (!isForum) {
// If we have unmuted topics, display the count of those. Otherwise, display the count of all topics. return (topic || chat).unreadCount;
? ((isMuted && topicsWithUnread?.filter((acc) => acc.isMuted === false).length) }
|| topicsWithUnread?.length)
: (topic || chat).unreadCount
), [chat, topic, topicsWithUnread, isForum, isMuted]);
const shouldBeMuted = useMemo(() => { return topicsWithUnread?.length;
const hasUnmutedUnreadTopics = topics }, [chat, topic, topicsWithUnread, isForum]);
&& Object.values(topics).some((acc) => !acc.isMuted && acc.unreadCount);
return isMuted || (topics && !hasUnmutedUnreadTopics); const shouldBeUnMuted = useMemo(() => {
}, [topics, isMuted]); 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; const hasUnreadMark = topic ? false : chat.hasUnreadMark;
@ -91,7 +101,7 @@ const ChatBadge: FC<OwnProps> = ({
const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog); const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog);
const className = buildClassName( const className = buildClassName(
'ChatBadge', 'ChatBadge',
shouldBeMuted && 'muted', !shouldBeUnMuted && 'muted',
!isUnread && isPinned && 'pinned', !isUnread && isPinned && 'pinned',
isUnread && 'unread', isUnread && 'unread',
); );
@ -109,7 +119,7 @@ const ChatBadge: FC<OwnProps> = ({
function renderContent() { function renderContent() {
const unreadReactionsElement = unreadReactionsCount && ( const unreadReactionsElement = unreadReactionsCount && (
<div className={buildClassName('ChatBadge reaction', shouldBeMuted && 'muted')}> <div className={buildClassName('ChatBadge reaction', !shouldBeUnMuted && 'muted')}>
<Icon name="heart" /> <Icon name="heart" />
</div> </div>
); );
@ -121,7 +131,7 @@ const ChatBadge: FC<OwnProps> = ({
); );
const unopenedTopicElement = isTopicUnopened && ( const unopenedTopicElement = isTopicUnopened && (
<div className={buildClassName('ChatBadge unopened', shouldBeMuted && 'muted')} /> <div className={buildClassName('ChatBadge unopened', !shouldBeUnMuted && 'muted')} />
); );
const unreadCountElement = (hasUnreadMark || unreadCount) ? ( const unreadCountElement = (hasUnreadMark || unreadCount) ? (

View File

@ -10,6 +10,7 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { groupStatefulContent } from '../../../global/helpers'; import { groupStatefulContent } from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectCanDeleteTopic, selectCanDeleteTopic,
@ -17,6 +18,8 @@ import {
selectChatMessage, selectChatMessage,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft, selectDraft,
selectNotifyDefaults,
selectNotifyException,
selectOutgoingStatus, selectOutgoingStatus,
selectPeerStory, selectPeerStory,
selectSender, selectSender,
@ -57,6 +60,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat: ApiChat; chat: ApiChat;
isChatMuted?: boolean;
canDelete?: boolean; canDelete?: boolean;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
lastMessageStory?: ApiTypeStory; lastMessageStory?: ApiTypeStory;
@ -75,6 +79,7 @@ const Topic: FC<OwnProps & StateProps> = ({
isSelected, isSelected,
chatId, chatId,
chat, chat,
isChatMuted,
style, style,
lastMessage, lastMessage,
lastMessageStory, lastMessageStory,
@ -106,9 +111,9 @@ const Topic: FC<OwnProps & StateProps> = ({
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
const { const {
isPinned, isClosed, isPinned, isClosed, notifySettings,
} = topic; } = topic;
const isMuted = topic.isMuted || (topic.isMuted === undefined && chat.isMuted); const isMuted = Boolean(notifySettings.mutedUntil || (notifySettings.mutedUntil === undefined && isChatMuted));
const handleOpenDeleteModal = useLastCallback(() => { const handleOpenDeleteModal = useLastCallback(() => {
markRenderDeleteModal(); markRenderDeleteModal();
@ -154,6 +159,7 @@ const Topic: FC<OwnProps & StateProps> = ({
const contextActions = useTopicContextActions({ const contextActions = useTopicContextActions({
topic, topic,
chat, chat,
isChatMuted,
wasOpened: wasTopicOpened, wasOpened: wasTopicOpened,
canDelete, canDelete,
handleDelete: handleOpenDeleteModal, handleDelete: handleOpenDeleteModal,
@ -181,7 +187,7 @@ const Topic: FC<OwnProps & StateProps> = ({
<TopicIcon topic={topic} className={styles.topicIcon} observeIntersection={observeIntersection} /> <TopicIcon topic={topic} className={styles.topicIcon} observeIntersection={observeIntersection} />
<h3 dir="auto" className="fullName">{renderText(topic.title)}</h3> <h3 dir="auto" className="fullName">{renderText(topic.title)}</h3>
</div> </div>
{topic.isMuted && <Icon name="muted" />} {Boolean(notifySettings.mutedUntil) && <Icon name="muted" />}
<div className="separator" /> <div className="separator" />
{isClosed && ( {isClosed && (
<Icon name="lock-badge" className={styles.closedIcon} /> <Icon name="lock-badge" className={styles.closedIcon} />
@ -247,11 +253,16 @@ export default memo(withGlobal<OwnProps>(
const storyData = lastMessage?.content.storyData; const storyData = lastMessage?.content.storyData;
const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id); const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id);
const isChatMuted = chat && getIsChatMuted(
chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id),
);
return { return {
chat, chat,
lastMessage, lastMessage,
lastMessageSender, lastMessageSender,
typingStatus, typingStatus,
isChatMuted,
canDelete: selectCanDeleteTopic(global, chatId, topic.id), canDelete: selectCanDeleteTopic(global, chatId, topic.id),
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
draft, draft,

View File

@ -13,6 +13,7 @@ import useOldLang from '../../../../hooks/useOldLang';
export default function useTopicContextActions({ export default function useTopicContextActions({
topic, topic,
chat, chat,
isChatMuted,
wasOpened, wasOpened,
canDelete, canDelete,
handleDelete, handleDelete,
@ -20,6 +21,7 @@ export default function useTopicContextActions({
}: { }: {
topic: ApiTopic; topic: ApiTopic;
chat: ApiChat; chat: ApiChat;
isChatMuted?: boolean;
wasOpened?: boolean; wasOpened?: boolean;
canDelete?: boolean; canDelete?: boolean;
handleDelete?: NoneToVoidFunction; handleDelete?: NoneToVoidFunction;
@ -29,7 +31,7 @@ export default function useTopicContextActions({
return useMemo(() => { return useMemo(() => {
const { const {
isPinned, isMuted, isClosed, id: topicId, isPinned, notifySettings, isClosed, id: topicId,
} = topic; } = topic;
const chatId = chat.id; const chatId = chat.id;
@ -75,7 +77,7 @@ export default function useTopicContextActions({
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }), handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }),
}) : undefined; }) : undefined;
const actionMute = ((chat.isMuted && isMuted !== false) || isMuted === true) const actionMute = ((isChatMuted && notifySettings.mutedUntil === undefined) || notifySettings.mutedUntil)
? { ? {
title: lang('ChatList.Unmute'), title: lang('ChatList.Unmute'),
icon: 'unmute', icon: 'unmute',
@ -114,5 +116,5 @@ export default function useTopicContextActions({
actionCloseTopic, actionCloseTopic,
actionDelete, actionDelete,
]) as MenuItemContextAction[]; ]) as MenuItemContextAction[];
}, [topic, chat, wasOpened, lang, canDelete, handleDelete, handleMute]); }, [topic, chat, isChatMuted, wasOpened, lang, canDelete, handleDelete, handleMute]);
} }

View File

@ -5,10 +5,10 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types'; import type { ApiChat, ApiUser } from '../../../api/types';
import { StoryViewerOrigin } from '../../../types'; import { StoryViewerOrigin } from '../../../types';
import { isUserId, selectIsChatMuted } from '../../../global/helpers'; import { isUserId } from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import { import {
selectChat, selectIsChatPinned, selectNotifyExceptions, selectChat, selectIsChatPinned, selectNotifyDefaults, selectNotifyException, selectUser,
selectNotifySettings, selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -156,9 +156,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const user = selectUser(global, chatId); const user = selectUser(global, chatId);
const isPinned = selectIsChatPinned(global, chatId); const isPinned = selectIsChatPinned(global, chatId);
const isMuted = chat const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id));
? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global))
: undefined;
return { return {
chat, chat,

View File

@ -3,6 +3,8 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiNotifyPeerType, ApiPeerNotifySettings } from '../../../api/types';
import { import {
checkIfNotificationsSupported, checkIfNotificationsSupported,
checkIfOfflinePushFailed, checkIfOfflinePushFailed,
@ -22,12 +24,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
hasPrivateChatsNotifications: boolean; notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>;
hasPrivateChatsMessagePreview: boolean;
hasGroupNotifications: boolean;
hasGroupMessagePreview: boolean;
hasBroadcastNotifications: boolean;
hasBroadcastMessagePreview: boolean;
hasContactJoinedNotifications: boolean; hasContactJoinedNotifications: boolean;
hasWebNotifications: boolean; hasWebNotifications: boolean;
hasPushNotifications: boolean; hasPushNotifications: boolean;
@ -37,12 +34,7 @@ type StateProps = {
const SettingsNotifications: FC<OwnProps & StateProps> = ({ const SettingsNotifications: FC<OwnProps & StateProps> = ({
isActive, isActive,
onReset, onReset,
hasPrivateChatsNotifications, notifyDefaults,
hasPrivateChatsMessagePreview,
hasGroupNotifications,
hasGroupMessagePreview,
hasBroadcastNotifications,
hasBroadcastMessagePreview,
hasContactJoinedNotifications, hasContactJoinedNotifications,
hasPushNotifications, hasPushNotifications,
hasWebNotifications, hasWebNotifications,
@ -66,27 +58,18 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
const handleSettingsChange = useCallback(( const handleSettingsChange = useCallback((
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,
peerType: 'contact' | 'group' | 'broadcast', peerType: ApiNotifyPeerType,
setting: 'silent' | 'showPreviews', setting: 'mute' | 'showPreviews',
) => { ) => {
const currentIsSilent = peerType === 'contact' const currentIsMuted = Boolean(notifyDefaults?.[peerType]?.mutedUntil);
? !hasPrivateChatsNotifications const currentShouldShowPreviews = Boolean(notifyDefaults?.[peerType]?.shouldShowPreviews);
: !(peerType === 'group' ? hasGroupNotifications : hasBroadcastNotifications);
const currentShouldShowPreviews = peerType === 'contact'
? hasPrivateChatsMessagePreview
: (peerType === 'group' ? hasGroupMessagePreview : hasBroadcastMessagePreview);
updateNotificationSettings({ updateNotificationSettings({
peerType, peerType,
...(setting === 'silent' && { isSilent: !e.target.checked, shouldShowPreviews: currentShouldShowPreviews }), isMuted: setting === 'mute' ? !e.target.checked : currentIsMuted,
...(setting === 'showPreviews' && { shouldShowPreviews: e.target.checked, isSilent: currentIsSilent }), shouldShowPreviews: setting === 'showPreviews' ? e.target.checked : currentShouldShowPreviews,
}); });
}, [ }, [notifyDefaults]);
hasBroadcastMessagePreview, hasBroadcastNotifications,
hasGroupMessagePreview, hasGroupNotifications,
hasPrivateChatsMessagePreview, hasPrivateChatsNotifications,
updateNotificationSettings,
]);
const handleWebNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleWebNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const isEnabled = e.target.checked; const isEnabled = e.target.checked;
@ -103,27 +86,27 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
}, [updateWebNotificationSettings]); }, [updateWebNotificationSettings]);
const handlePrivateChatsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handlePrivateChatsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'contact', 'silent'); handleSettingsChange(e, 'users', 'mute');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handlePrivateChatsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handlePrivateChatsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'contact', 'showPreviews'); handleSettingsChange(e, 'users', 'showPreviews');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handleGroupsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleGroupsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'group', 'silent'); handleSettingsChange(e, 'groups', 'mute');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handleGroupsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleGroupsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'group', 'showPreviews'); handleSettingsChange(e, 'groups', 'showPreviews');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handleChannelsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleChannelsNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'broadcast', 'silent'); handleSettingsChange(e, 'channels', 'mute');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handleChannelsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleChannelsPreviewChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
handleSettingsChange(e, 'broadcast', 'showPreviews'); handleSettingsChange(e, 'channels', 'showPreviews');
}, [handleSettingsChange]); }, [handleSettingsChange]);
const handleContactNotificationChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleContactNotificationChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@ -186,17 +169,17 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
<Checkbox <Checkbox
label={lang('NotificationsForPrivateChats')} label={lang('NotificationsForPrivateChats')}
subLabel={lang(hasPrivateChatsNotifications subLabel={lang(notifyDefaults?.users?.mutedUntil
? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={hasPrivateChatsNotifications} checked={Boolean(notifyDefaults?.users?.mutedUntil)}
onChange={handlePrivateChatsNotificationsChange} onChange={handlePrivateChatsNotificationsChange}
/> />
<Checkbox <Checkbox
label={lang('MessagePreview')} label={lang('MessagePreview')}
disabled={!hasPrivateChatsNotifications} disabled={!notifyDefaults?.users?.mutedUntil}
subLabel={lang(hasPrivateChatsMessagePreview subLabel={lang(notifyDefaults?.users?.shouldShowPreviews
? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={hasPrivateChatsMessagePreview} checked={Boolean(notifyDefaults?.users?.shouldShowPreviews)}
onChange={handlePrivateChatsPreviewChange} onChange={handlePrivateChatsPreviewChange}
/> />
</div> </div>
@ -206,15 +189,17 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
<Checkbox <Checkbox
label={lang('NotificationsForGroups')} label={lang('NotificationsForGroups')}
subLabel={lang(hasGroupNotifications ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} subLabel={lang(notifyDefaults?.groups?.mutedUntil
checked={hasGroupNotifications} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={Boolean(notifyDefaults?.groups?.mutedUntil)}
onChange={handleGroupsNotificationsChange} onChange={handleGroupsNotificationsChange}
/> />
<Checkbox <Checkbox
label={lang('MessagePreview')} label={lang('MessagePreview')}
disabled={!hasGroupNotifications} disabled={!notifyDefaults?.groups?.mutedUntil}
subLabel={lang(hasGroupMessagePreview ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} subLabel={lang(notifyDefaults?.groups?.shouldShowPreviews
checked={hasGroupMessagePreview} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={Boolean(notifyDefaults?.groups?.shouldShowPreviews)}
onChange={handleGroupsPreviewChange} onChange={handleGroupsPreviewChange}
/> />
</div> </div>
@ -224,15 +209,17 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
<Checkbox <Checkbox
label={lang('NotificationsForChannels')} label={lang('NotificationsForChannels')}
subLabel={lang(hasBroadcastNotifications ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} subLabel={lang(notifyDefaults?.channels?.mutedUntil
checked={hasBroadcastNotifications} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={Boolean(notifyDefaults?.channels?.mutedUntil)}
onChange={handleChannelsNotificationsChange} onChange={handleChannelsNotificationsChange}
/> />
<Checkbox <Checkbox
label={lang('MessagePreview')} label={lang('MessagePreview')}
disabled={!hasBroadcastNotifications} disabled={!notifyDefaults?.channels?.mutedUntil}
subLabel={lang(hasBroadcastMessagePreview ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')} subLabel={lang(notifyDefaults?.channels?.shouldShowPreviews
checked={hasBroadcastMessagePreview} ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
checked={Boolean(notifyDefaults?.channels?.shouldShowPreviews)}
onChange={handleChannelsPreviewChange} onChange={handleChannelsPreviewChange}
/> />
</div> </div>
@ -253,12 +240,6 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
return { 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), hasContactJoinedNotifications: Boolean(global.settings.byKey.hasContactJoinedNotifications),
hasWebNotifications: global.settings.byKey.hasWebNotifications, hasWebNotifications: global.settings.byKey.hasWebNotifications,
hasPushNotifications: global.settings.byKey.hasPushNotifications, hasPushNotifications: global.settings.byKey.hasPushNotifications,

View File

@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { MessageListType } from '../../types'; import type { MessageListType, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors'; import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors';
@ -24,6 +24,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
messageListType?: MessageListType; messageListType?: MessageListType;
threadId?: ThreadId;
unreadCount?: number; unreadCount?: number;
unreadReactions?: number[]; unreadReactions?: number[];
unreadMentions?: number[]; unreadMentions?: number[];
@ -38,6 +39,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
canPost, canPost,
messageListType, messageListType,
chatId, chatId,
threadId,
unreadCount, unreadCount,
unreadReactions, unreadReactions,
unreadMentions, unreadMentions,
@ -56,6 +58,16 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
const hasUnreadReactions = Boolean(reactionsCount); const hasUnreadReactions = Boolean(reactionsCount);
const hasUnreadMentions = Boolean(mentionsCount); const hasUnreadMentions = Boolean(mentionsCount);
const handleReadAllReactions = useLastCallback(() => {
if (!chatId) return;
readAllReactions({ chatId, threadId });
});
const handleReadAllMentions = useLastCallback(() => {
if (!chatId) return;
readAllMentions({ chatId, threadId });
});
useEffect(() => { useEffect(() => {
if (hasUnreadReactions && chatId && !unreadReactions?.length) { if (hasUnreadReactions && chatId && !unreadReactions?.length) {
fetchUnreadReactions({ chatId }); fetchUnreadReactions({ chatId });
@ -120,7 +132,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
icon="heart-outline" icon="heart-outline"
ariaLabelLang="AccDescrReactionMentionDown" ariaLabelLang="AccDescrReactionMentionDown"
onClick={focusNextReaction} onClick={focusNextReaction}
onReadAll={readAllReactions} onReadAll={handleReadAllReactions}
unreadCount={reactionsCount} unreadCount={reactionsCount}
className={buildClassName( className={buildClassName(
styles.reactions, styles.reactions,
@ -133,7 +145,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
icon="mention" icon="mention"
ariaLabelLang="AccDescrMentionDown" ariaLabelLang="AccDescrMentionDown"
onClick={focusNextMention} onClick={focusNextMention}
onReadAll={readAllMentions} onReadAll={handleReadAllMentions}
unreadCount={mentionsCount} unreadCount={mentionsCount}
className={!hasUnreadMentions && styles.hidden} className={!hasUnreadMentions && styles.hidden}
/> />
@ -166,6 +178,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
messageListType, messageListType,
chatId, chatId,
threadId,
reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined, reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined,
unreadReactions: shouldShowCount ? chat.unreadReactions : undefined, unreadReactions: shouldShowCount ? chat.unreadReactions : undefined,
unreadMentions: shouldShowCount ? chat.unreadMentions : undefined, unreadMentions: shouldShowCount ? chat.unreadMentions : undefined,

View File

@ -20,8 +20,8 @@ import {
isSystemBot, isSystemBot,
isUserId, isUserId,
isUserRightBanned, isUserRightBanned,
selectIsChatMuted,
} from '../../global/helpers'; } from '../../global/helpers';
import { getIsChatMuted } from '../../global/helpers/notifications';
import { import {
selectBot, selectBot,
selectCanGift, selectCanGift,
@ -32,8 +32,8 @@ import {
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatWithSelf, selectIsChatWithSelf,
selectIsRightColumnShown, selectIsRightColumnShown,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings, selectNotifyException,
selectTabState, selectTabState,
selectTopic, selectTopic,
selectUser, selectUser,
@ -759,7 +759,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), isMuted: getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)),
isPrivate, isPrivate,
isTopic: chat?.isForum && !isMainThread, isTopic: chat?.isForum && !isMainThread,
isForum: chat?.isForum, isForum: chat?.isForum,

View File

@ -86,7 +86,7 @@ export default function useMessageObservers(
} }
if (mentionIds.length) { if (mentionIds.length) {
markMentionsRead({ messageIds: mentionIds }); markMentionsRead({ chatId, messageIds: mentionIds });
} }
if (reactionIds.length) { if (reactionIds.length) {

View File

@ -227,9 +227,9 @@ const ActionMessage = ({
} }
if (message.hasUnreadMention) { if (message.hasUnreadMention) {
markMentionsRead({ messageIds: [id] }); markMentionsRead({ chatId, messageIds: [id] });
} }
}, [hasUnreadReaction, id, animateUnreadReaction, message.hasUnreadMention]); }, [hasUnreadReaction, chatId, id, animateUnreadReaction, message.hasUnreadMention]);
useEffect(() => { useEffect(() => {
if (action.type !== 'giftPremium') return; if (action.type !== 'giftPremium') return;

View File

@ -869,9 +869,9 @@ const Message: FC<OwnProps & StateProps> = ({
} }
if (unreadMentionIds.length) { 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(() => { const albumLayout = useMemo(() => {
return isAlbum return isAlbum

View File

@ -9,11 +9,12 @@ import type { ApiPhoto, ApiUser } from '../../../api/types';
import { ManagementProgress } from '../../../types'; import { ManagementProgress } from '../../../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; 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 { import {
selectChat, selectChat,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings, selectNotifyException,
selectTabState, selectTabState,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
@ -296,7 +297,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const userFullInfo = selectUserFullInfo(global, userId); const userFullInfo = selectUserFullInfo(global, userId);
const { progress } = selectTabState(global).management; 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 personalPhoto = userFullInfo?.personalPhoto;
const notPersonalPhoto = userFullInfo?.profilePhoto || userFullInfo?.fallbackPhoto; const notPersonalPhoto = userFullInfo?.profilePhoto || userFullInfo?.fallbackPhoto;

View File

@ -1,6 +1,7 @@
import type { import type {
ApiChat, ApiChatFolder, ApiChatlistExportedInvite, ApiChat, ApiChatFolder, ApiChatlistExportedInvite,
ApiChatMember, ApiError, ApiMissingInvitedUser, ApiChatMember, ApiError, ApiMissingInvitedUser,
ApiTopic,
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
@ -60,6 +61,7 @@ import {
addChatMembers, addChatMembers,
addChats, addChats,
addMessages, addMessages,
addNotifyExceptions,
addSimilarBots, addSimilarBots,
addUsers, addUsers,
addUserStatuses, addUserStatuses,
@ -221,7 +223,7 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (chat?.hasUnreadMark) { if (chat?.hasUnreadMark) {
actions.toggleChatUnread({ id }); actions.markChatRead({ id });
} }
const isChatOnlySummary = !selectChatLastMessageId(global, id); const isChatOnlySummary = !selectChatLastMessageId(global, id);
@ -623,32 +625,26 @@ addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): P
}); });
addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => { addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, muteUntil = 0 } = payload; const { chatId, isMuted, mutedUntil } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
} }
const isMuted = payload.isMuted ?? muteUntil > 0; void callApi('updateChatMutedState', { chat, isMuted, mutedUntil });
global = updateChat(global, chatId, { isMuted });
setGlobal(global);
void callApi('updateChatMutedState', { chat, isMuted, muteUntil });
}); });
addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => { addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, topicId, muteUntil = 0 } = payload; const {
chatId, topicId, isMuted, mutedUntil,
} = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
} }
const isMuted = payload.isMuted ?? muteUntil > 0;
global = updateTopic(global, chatId, topicId, { isMuted });
setGlobal(global);
void callApi('updateTopicMutedState', { void callApi('updateTopicMutedState', {
chat, topicId, isMuted, muteUntil, chat, topicId, isMuted, mutedUntil,
}); });
}); });
@ -1155,17 +1151,47 @@ addActionHandler('deleteChatFolder', async (global, actions, payload): Promise<v
} }
}); });
addActionHandler('toggleChatUnread', (global, actions, payload): ActionReturnType => { addActionHandler('markChatUnread', (global, actions, payload): ActionReturnType => {
const { id } = payload; const { id } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (chat) { if (!chat) return;
if (chat.unreadCount) { void callApi('toggleDialogUnread', {
void callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID }); chat,
} else { hasUnreadMark: !chat.hasUnreadMark,
void callApi('toggleDialogUnread', { });
chat, });
hasUnreadMark: !chat.hasUnreadMark,
}); addActionHandler('markChatRead', async (global, actions, payload): Promise<void> => {
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, threadId: topicId,
maxId: lastTopicMessageId, maxId: lastTopicMessageId,
}); });
actions.readAllMentions({ chatId, threadId: topicId });
actions.readAllReactions({ chatId, threadId: topicId });
global = getGlobal(); global = getGlobal();
global = updateTopic(global, chatId, topicId, { global = updateTopic(global, chatId, topicId, {
@ -2910,6 +2938,7 @@ async function loadChats(
global = updateChatListSecondaryInfo(global, listType, result); global = updateChatListSecondaryInfo(global, listType, result);
global = replaceMessages(global, result.messages); global = replaceMessages(global, result.messages);
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType); global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
global = addNotifyExceptions(global, result.notifyExceptionById);
if (!shouldIgnorePagination) { if (!shouldIgnorePagination) {
global = replaceChatListLoadingParameters( global = replaceChatListLoadingParameters(

View File

@ -1818,12 +1818,11 @@ async function fetchUnreadMentions<T extends GlobalState>(global: T, chatId: str
} }
addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => { addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => {
const { messageIds, tabId = getCurrentTabId() } = payload; const { chatId, messageIds, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
const chat = selectCurrentChat(global, tabId);
if (!chat) return; if (!chat) return;
global = removeUnreadMentions(global, chat.id, chat, messageIds, true); global = removeUnreadMentions(global, chatId, chat, messageIds, true);
setGlobal(global); setGlobal(global);
actions.markMessagesRead({ messageIds, tabId }); actions.markMessagesRead({ messageIds, tabId });
@ -1848,17 +1847,22 @@ addActionHandler('focusNextMention', async (global, actions, payload): Promise<v
}); });
addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType => { addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType => {
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; if (!chat) return undefined;
callApi('readAllMentions', { chat }); callApi('readAllMentions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId });
return updateChat(global, chat.id, { if (threadId === MAIN_THREAD_ID) {
unreadMentionsCount: undefined, return updateChat(global, chat.id, {
unreadMentions: undefined, unreadMentionsCount: undefined,
}); unreadMentions: undefined,
});
}
// TODO[Forums]: Support mentions in threads
return undefined;
}); });
addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {

View File

@ -1,6 +1,6 @@
import type { ApiError, ApiReaction, ApiReactionEmoji } from '../../../api/types'; import type { ApiError, ApiReaction, ApiReactionEmoji } from '../../../api/types';
import type { ActionReturnType } from '../../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 { GENERAL_REFETCH_INTERVAL } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -557,16 +557,21 @@ addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnTy
}); });
addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => { addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => {
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; if (!chat) return undefined;
callApi('readAllReactions', { chat }); callApi('readAllReactions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId });
return updateUnreadReactions(global, chat.id, { if (threadId === MAIN_THREAD_ID) {
unreadReactionsCount: undefined, return updateUnreadReactions(global, chat.id, {
unreadReactions: undefined, unreadReactionsCount: undefined,
}); unreadReactions: undefined,
});
}
// TODO[Forums]: Support unread reactions in threads
return undefined;
}); });
addActionHandler('loadTopReactions', async (global): Promise<void> => { addActionHandler('loadTopReactions', async (global): Promise<void> => {

View File

@ -5,7 +5,7 @@ import {
UPLOADING_WALLPAPER_SLUG, UPLOADING_WALLPAPER_SLUG,
} from '../../../types'; } 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 { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications'; import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications';
@ -16,9 +16,9 @@ import { callApi } from '../../../api/gramjs';
import { buildApiInputPrivacyRules } from '../../helpers'; import { buildApiInputPrivacyRules } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
addBlockedUser, addNotifyExceptions, deletePeerPhoto, addBlockedUser, addNotifyException, addNotifyExceptions, deletePeerPhoto,
removeBlockedUser, replaceSettings, updateChat, removeBlockedUser, replaceSettings, updateChat,
updateNotifySettings, updateUser, updateUserFullInfo, updateUser, updateUserFullInfo,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
@ -306,26 +306,40 @@ addActionHandler('loadNotificationExceptions', async (global): Promise<void> =>
}); });
addActionHandler('loadNotificationSettings', async (global): Promise<void> => { addActionHandler('loadNotificationSettings', async (global): Promise<void> => {
const result = await callApi('fetchNotificationSettings'); const [signUpNotification, notifyDefaults] = await Promise.all([
if (!result) { callApi('fetchContactSignUpSetting'),
return; callApi('fetchNotifyDefaultSettings'),
} ]);
if (!notifyDefaults) return;
global = getGlobal(); global = getGlobal();
global = replaceSettings(global, result); global = replaceSettings(global, {
hasContactJoinedNotifications: signUpNotification,
});
global = {
...global,
settings: {
...global.settings,
notifyDefaults,
},
};
setGlobal(global); setGlobal(global);
}); });
addActionHandler('updateNotificationSettings', async (global, actions, payload): Promise<void> => { addActionHandler('updateNotificationSettings', async (global, actions, payload): Promise<void> => {
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) { if (!result) {
return; return;
} }
global = getGlobal(); global = getGlobal();
global = updateNotifySettings(global, peerType, isSilent, shouldShowPreviews); global = addNotifyException(global, peerType, {
mutedUntil: isMuted ? MAX_INT_32 : undefined,
shouldShowPreviews,
});
setGlobal(global); setGlobal(global);
}); });
@ -582,12 +596,11 @@ addActionHandler('loadCountryList', async (global, actions, payload): Promise<vo
setGlobal(global); setGlobal(global);
}); });
addActionHandler('ensureTimeFormat', async (global, actions, payload): Promise<void> => { addActionHandler('ensureTimeFormat', async (global, actions): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
if (global.authNearestCountry) { if (global.authNearestCountry) {
const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT
.has(global.authNearestCountry.toUpperCase()) ? '12h' : '24h'; .has(global.authNearestCountry.toUpperCase()) ? '12h' : '24h';
actions.setSettingOption({ timeFormat, tabId }); actions.setSettingOption({ timeFormat });
setTimeFormat(timeFormat); setTimeFormat(timeFormat);
} }
@ -598,7 +611,7 @@ addActionHandler('ensureTimeFormat', async (global, actions, payload): Promise<v
const nearestCountryCode = await callApi('fetchNearestCountry'); const nearestCountryCode = await callApi('fetchNearestCountry');
if (nearestCountryCode) { if (nearestCountryCode) {
const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT.has(nearestCountryCode.toUpperCase()) ? '12h' : '24h'; const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT.has(nearestCountryCode.toUpperCase()) ? '12h' : '24h';
actions.setSettingOption({ timeFormat, tabId }); actions.setSettingOption({ timeFormat });
setTimeFormat(timeFormat); setTimeFormat(timeFormat);
} }
}); });

View File

@ -2,37 +2,33 @@ import type { ActionReturnType } from '../../types';
import { addActionHandler, setGlobal } from '../../index'; import { addActionHandler, setGlobal } from '../../index';
import { import {
addNotifyException, updateChat, updateNotifySettings, addNotifyException,
updateNotifyDefaults,
updateTopic, updateTopic,
} from '../../reducers'; } from '../../reducers';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'updateNotifySettings': { case 'updateDefaultNotifySettings': {
return updateNotifySettings(global, update.peerType, update.isSilent, update.shouldShowPreviews); return updateNotifyDefaults(global, update.peerType, update.settings);
} }
case 'updateNotifyExceptions': { case 'updateChatNotifySettings': {
const { const {
chatId, isMuted, isSilent, shouldShowPreviews, chatId, settings,
} = update; } = update;
const chat = global.chats.byId[chatId];
if (chat) { global = addNotifyException(global, chatId, settings);
global = updateChat(global, chatId, { isMuted });
}
global = addNotifyException(global, chatId, { isMuted, isSilent, shouldShowPreviews });
setGlobal(global); setGlobal(global);
break; break;
} }
case 'updateTopicNotifyExceptions': { case 'updateTopicNotifySettings': {
const { const {
chatId, topicId, isMuted, chatId, topicId, settings,
} = update; } = update;
global = updateTopic(global, chatId, topicId, { isMuted }); global = updateTopic(global, chatId, topicId, { notifySettings: settings });
setGlobal(global); setGlobal(global);
break; break;

View File

@ -24,7 +24,10 @@ import { replaceSettings } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectNotifySettings, selectPerformanceSettings, selectTabState, selectTheme, selectPerformanceSettings,
selectSettingsKeys,
selectTabState,
selectTheme,
} from '../../selectors'; } from '../../selectors';
const HISTORY_ANIMATION_DURATION = 450; const HISTORY_ANIMATION_DURATION = 450;
@ -100,7 +103,7 @@ addActionHandler('initShared', (): ActionReturnType => {
}); });
addActionHandler('initMain', (global): ActionReturnType => { addActionHandler('initMain', (global): ActionReturnType => {
const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global); const { hasWebNotifications, hasPushNotifications } = selectSettingsKeys(global);
if (hasWebNotifications && hasPushNotifications) { if (hasWebNotifications && hasPushNotifications) {
// Most of the browsers only show the notifications permission prompt after the first user gesture. // Most of the browsers only show the notifications permission prompt after the first user gesture.
const events = ['click', 'keypress']; const events = ['click', 'keypress'];

View File

@ -300,6 +300,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.cacheVersion = 2; cached.cacheVersion = 2;
} }
if (!cached.chats.notifyExceptionById) {
cached.chats.notifyExceptionById = initialState.chats.notifyExceptionById;
}
} }
function updateCache(force?: boolean) { function updateCache(force?: boolean) {
@ -494,6 +498,7 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
similarChannelsById: {}, similarChannelsById: {},
similarBotsById: {}, similarBotsById: {},
isFullyLoaded: {}, isFullyLoaded: {},
notifyExceptionById: {},
loadingParameters: INITIAL_GLOBAL_STATE.chats.loadingParameters, loadingParameters: INITIAL_GLOBAL_STATE.chats.loadingParameters,
byId: pickTruthy(global.chats.byId, idsToSave), byId: pickTruthy(global.chats.byId, idsToSave),
fullInfoById: pickTruthy(global.chats.fullInfoById, idsToSave), fullInfoById: pickTruthy(global.chats.fullInfoById, idsToSave),
@ -662,7 +667,6 @@ function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings
themes, themes,
performance, performance,
privacy: {}, privacy: {},
notifyExceptions: {},
botVerificationShownPeerIds, botVerificationShownPeerIds,
miniAppsCachedPosition, miniAppsCachedPosition,
miniAppsCachedSize, miniAppsCachedSize,

View File

@ -13,7 +13,7 @@ import type {
} from '../../api/types'; } from '../../api/types';
import type { OldLangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { import type {
CustomPeer, NotifyException, NotifySettings, ThreadId, CustomPeer, ThreadId,
} from '../../types'; } from '../../types';
import type { LangFn } from '../../util/localization'; import type { LangFn } from '../../util/localization';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
@ -306,40 +306,6 @@ export function isChatArchived(chat: ApiChat) {
return chat.folderId === ARCHIVED_FOLDER_ID; return chat.folderId === ARCHIVED_FOLDER_ID;
} }
export function selectIsChatMuted(
chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record<string, NotifyException> = {},
) {
// 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<string, NotifyException> = {},
) {
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) { export function getCanDeleteChat(chat: ApiChat) {
return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator);
} }

View File

@ -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 type { GlobalState } from '../types';
import { partition } from '../../util/iteratees'; import { partition } from '../../util/iteratees';

View File

@ -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<ApiNotifyPeerType, ApiPeerNotifySettings>,
notifyException?: ApiPeerNotifySettings,
) {
const settings = getChatNotifySettings(chat, notifyDefaults, notifyException);
if (!settings?.mutedUntil) return false;
return getServerTime() < settings.mutedUntil;
}
export function getIsChatSilent(
chat: ApiChat,
notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>,
notifyException?: ApiPeerNotifySettings,
) {
const settings = getChatNotifySettings(chat, notifyDefaults, notifyException);
if (!settings) return false;
return !settings.hasSound;
}
export function getShouldShowMessagePreview(
chat: ApiChat,
notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>,
notifyException?: ApiPeerNotifySettings,
) {
const settings = getChatNotifySettings(chat, notifyDefaults, notifyException);
return Boolean(settings?.shouldShowPreviews);
}
export function getChatNotifySettings(
chat: ApiChat,
notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>,
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';
}

View File

@ -122,6 +122,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
similarChannelsById: {}, similarChannelsById: {},
similarBotsById: {}, similarBotsById: {},
topicsInfoById: {}, topicsInfoById: {},
notifyExceptionById: {},
loadingParameters: { loadingParameters: {
active: {}, active: {},
archived: {}, archived: {},
@ -297,7 +298,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
}, },
performance: INITIAL_PERFORMANCE_STATE_MAX, performance: INITIAL_PERFORMANCE_STATE_MAX,
privacy: {}, privacy: {},
notifyExceptions: {},
botVerificationShownPeerIds: [], botVerificationShownPeerIds: [],
}, },

View File

@ -1,6 +1,6 @@
import type { ApiNotifyException } from '../../api/types'; import type { ApiNotifyPeerType, ApiPeerNotifySettings } from '../../api/types';
import type { import type {
ISettings, IThemeSettings, NotifyException, ISettings, IThemeSettings,
ThemeKey, ThemeKey,
} from '../../types'; } from '../../types';
import type { GlobalState } from '../types'; import type { GlobalState } from '../types';
@ -39,52 +39,51 @@ export function replaceThemeSettings<T extends GlobalState>(
} }
export function addNotifyExceptions<T extends GlobalState>( export function addNotifyExceptions<T extends GlobalState>(
global: T, notifyExceptions: ApiNotifyException[], global: T, notifyExceptionById: Record<string, ApiPeerNotifySettings>,
): T {
notifyExceptions.forEach((notifyException) => {
const { chatId, ...exceptionData } = notifyException;
global = addNotifyException(global, chatId, exceptionData);
});
return global;
}
export function addNotifyException<T extends GlobalState>(
global: T, id: string, notifyException: NotifyException,
): T { ): T {
return { return {
...global, ...global,
settings: { chats: {
...global.settings, ...global.chats,
notifyExceptions: { notifyExceptionById: {
...global.settings.notifyExceptions, ...global.chats.notifyExceptionById,
...notifyExceptionById,
},
},
};
}
export function addNotifyException<T extends GlobalState>(
global: T, id: string, notifyException: ApiPeerNotifySettings,
): T {
return {
...global,
chats: {
...global.chats,
notifyExceptionById: {
...global.chats.notifyExceptionById,
[id]: notifyException, [id]: notifyException,
}, },
}, },
}; };
} }
// eslint-disable-next-line consistent-return export function updateNotifyDefaults<T extends GlobalState>(
export function updateNotifySettings<T extends GlobalState>( global: T, peerType: ApiNotifyPeerType, settings: Partial<ApiPeerNotifySettings>,
global: T, peerType: 'contact' | 'group' | 'broadcast', isSilent?: boolean, shouldShowPreviews?: boolean,
): T { ): T {
switch (peerType) { return {
case 'contact': ...global,
return replaceSettings(global, { settings: {
...(typeof isSilent !== 'undefined' && { hasPrivateChatsNotifications: !isSilent }), ...global.settings,
...(typeof shouldShowPreviews !== 'undefined' && { hasPrivateChatsMessagePreview: shouldShowPreviews }), notifyDefaults: {
}); ...global.settings.notifyDefaults,
case 'group': [peerType]: {
return replaceSettings(global, { ...global.settings.notifyDefaults?.[peerType],
...(typeof isSilent !== 'undefined' && { hasGroupNotifications: !isSilent }), ...settings,
...(typeof shouldShowPreviews !== 'undefined' && { hasGroupMessagePreview: shouldShowPreviews }), },
}); },
case 'broadcast': },
return replaceSettings(global, { };
...(typeof isSilent !== 'undefined' && { hasBroadcastNotifications: !isSilent }),
...(typeof shouldShowPreviews !== 'undefined' && { hasBroadcastMessagePreview: shouldShowPreviews }),
});
}
} }
export function addBlockedUser<T extends GlobalState>(global: T, contactId: string): T { export function addBlockedUser<T extends GlobalState>(global: T, contactId: string): T {

View File

@ -1,11 +1,11 @@
import type { GlobalState } from '../types'; import type { GlobalState } from '../types';
export function selectNotifySettings<T extends GlobalState>(global: T) { export function selectNotifyDefaults<T extends GlobalState>(global: T) {
return global.settings.byKey; return global.settings.notifyDefaults;
} }
export function selectNotifyExceptions<T extends GlobalState>(global: T) { export function selectNotifyException<T extends GlobalState>(global: T, chatId: string) {
return global.settings.notifyExceptions; return global.chats.notifyExceptionById?.[chatId];
} }
export function selectLanguageCode<T extends GlobalState>(global: T) { export function selectLanguageCode<T extends GlobalState>(global: T) {
@ -13,7 +13,7 @@ export function selectLanguageCode<T extends GlobalState>(global: T) {
} }
export function selectCanSetPasscode<T extends GlobalState>(global: T) { export function selectCanSetPasscode<T extends GlobalState>(global: T) {
return global.authRememberMe && global.isCacheApiSupported; return global.authRememberMe;
} }
export function selectTranslationLanguage<T extends GlobalState>(global: T) { export function selectTranslationLanguage<T extends GlobalState>(global: T) {
@ -27,3 +27,7 @@ export function selectNewNoncontactPeersRequirePremium<T extends GlobalState>(gl
export function selectShouldHideReadMarks<T extends GlobalState>(global: T) { export function selectShouldHideReadMarks<T extends GlobalState>(global: T) {
return global.settings.byKey.shouldHideReadMarks; return global.settings.byKey.shouldHideReadMarks;
} }
export function selectSettingsKeys<T extends GlobalState>(global: T) {
return global.settings.byKey;
}

View File

@ -25,6 +25,7 @@ import type {
ApiMessageSearchContext, ApiMessageSearchContext,
ApiNewPoll, ApiNewPoll,
ApiNotification, ApiNotification,
ApiNotifyPeerType,
ApiPaymentStatus, ApiPaymentStatus,
ApiPhoto, ApiPhoto,
ApiPremiumSection, ApiPremiumSection,
@ -220,8 +221,8 @@ export interface ActionPayloads {
isSilent: boolean; isSilent: boolean;
}; };
updateNotificationSettings: { updateNotificationSettings: {
peerType: 'contact' | 'group' | 'broadcast'; peerType: ApiNotifyPeerType;
isSilent?: boolean; isMuted?: boolean;
shouldShowPreviews?: boolean; shouldShowPreviews?: boolean;
}; };
@ -255,7 +256,7 @@ export interface ActionPayloads {
loadCountryList: { loadCountryList: {
langCode?: string; langCode?: string;
}; };
ensureTimeFormat: WithTabId | undefined; ensureTimeFormat: undefined;
// misc // misc
loadWebPagePreview: { loadWebPagePreview: {
@ -365,7 +366,8 @@ export interface ActionPayloads {
toggleChatArchived: { toggleChatArchived: {
id: string; id: string;
}; };
toggleChatUnread: { id: string }; markChatUnread: { id: string };
markChatRead: { id: string };
loadChatFolders: undefined; loadChatFolders: undefined;
loadRecommendedChatFolders: undefined; loadRecommendedChatFolders: undefined;
editChatFolder: { editChatFolder: {
@ -1047,7 +1049,7 @@ export interface ActionPayloads {
updateChatMutedState: { updateChatMutedState: {
chatId: string; chatId: string;
isMuted?: boolean; isMuted?: boolean;
muteUntil?: number; mutedUntil?: number;
}; };
updateChat: { updateChat: {
@ -1313,9 +1315,16 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
focusNextReaction: WithTabId | undefined; focusNextReaction: WithTabId | undefined;
focusNextMention: WithTabId | undefined; focusNextMention: WithTabId | undefined;
readAllReactions: WithTabId | undefined; readAllReactions: {
readAllMentions: WithTabId | undefined; chatId: string;
threadId?: ThreadId;
};
readAllMentions: {
chatId: string;
threadId?: ThreadId;
};
markMentionsRead: { markMentionsRead: {
chatId: string;
messageIds: number[]; messageIds: number[];
} & WithTabId; } & WithTabId;
copyMessageLink: { copyMessageLink: {
@ -2505,7 +2514,7 @@ export interface ActionPayloads {
chatId: string; chatId: string;
topicId: number; topicId: number;
isMuted?: boolean; isMuted?: boolean;
muteUntil?: number; mutedUntil?: number;
}; };
setViewForumAsMessages: { setViewForumAsMessages: {
chatId: string; chatId: string;

View File

@ -15,8 +15,10 @@ import type {
ApiGroupCall, ApiGroupCall,
ApiLanguage, ApiLanguage,
ApiMessage, ApiMessage,
ApiNotifyPeerType,
ApiPaidReactionPrivacyType, ApiPaidReactionPrivacyType,
ApiPeerColors, ApiPeerColors,
ApiPeerNotifySettings,
ApiPeerPhotos, ApiPeerPhotos,
ApiPeerStories, ApiPeerStories,
ApiPhoneCall, ApiPhoneCall,
@ -54,7 +56,6 @@ import type {
EmojiKeywords, EmojiKeywords,
ISettings, ISettings,
IThemeSettings, IThemeSettings,
NotifyException,
PerformanceType, PerformanceType,
Point, Point,
ServiceNotification, ServiceNotification,
@ -221,6 +222,7 @@ export type GlobalState = {
similarChannelIds?: string[]; similarChannelIds?: string[];
count?: number; count?: number;
}>>; }>>;
notifyExceptionById: Record<string, ApiPeerNotifySettings>;
similarBotsById: Record<string, SimilarBotsInfo>; similarBotsById: Record<string, SimilarBotsInfo>;
}; };
@ -411,7 +413,7 @@ export type GlobalState = {
loadedWallpapers?: ApiWallpaper[]; loadedWallpapers?: ApiWallpaper[];
themes: Partial<Record<ThemeKey, IThemeSettings>>; themes: Partial<Record<ThemeKey, IThemeSettings>>;
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>; privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
notifyExceptions?: Record<number, NotifyException>; notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>;
lastPremiumBandwithNotificationDate?: number; lastPremiumBandwithNotificationDate?: number;
paidReactionPrivacy?: ApiPaidReactionPrivacyType; paidReactionPrivacy?: ApiPaidReactionPrivacyType;
languages?: ApiLanguage[]; languages?: ApiLanguage[];

View File

@ -79,7 +79,8 @@ const useChatContextActions = ({
toggleSavedDialogPinned, toggleSavedDialogPinned,
updateChatMutedState, updateChatMutedState,
toggleChatArchived, toggleChatArchived,
toggleChatUnread, markChatRead,
markChatUnread,
openChatInNewTab, openChatInNewTab,
} = getActions(); } = getActions();
@ -149,10 +150,10 @@ const useChatContextActions = ({
} }
const actionMaskAsRead = (chat.unreadCount || chat.hasUnreadMark) 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; : undefined;
const actionMarkAsUnread = !(chat.unreadCount || chat.hasUnreadMark) && !chat.isForum 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; : undefined;
const actionArchive = isChatArchived(chat) const actionArchive = isChatArchived(chat)

View File

@ -76,19 +76,6 @@ export interface IThemeSettings {
isBlurred?: boolean; 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 = ( export type LangCode = (
'en' | 'ar' | 'be' | 'ca' | 'nl' | 'fr' | 'de' | 'id' | 'it' | 'ko' | 'ms' | 'fa' | 'pl' | 'pt-br' | 'ru' | 'es' 'en' | 'ar' | 'be' | 'ca' | 'nl' | 'fr' | 'de' | 'id' | 'it' | 'ko' | 'ms' | 'fa' | 'pl' | 'pt-br' | 'ru' | 'es'
| 'tr' | 'uk' | 'uz' | 'tr' | 'uk' | 'uz'
@ -96,7 +83,7 @@ export type LangCode = (
export type TimeFormat = '24h' | '12h'; export type TimeFormat = '24h' | '12h';
export interface ISettings extends NotifySettings, Record<string, any> { export interface ISettings {
theme: ThemeKey; theme: ThemeKey;
shouldUseSystemTheme: boolean; shouldUseSystemTheme: boolean;
messageTextSize: number; messageTextSize: number;
@ -139,6 +126,10 @@ export interface ISettings extends NotifySettings, Record<string, any> {
shouldDebugExportedSenders?: boolean; shouldDebugExportedSenders?: boolean;
shouldWarnAboutSvg?: boolean; shouldWarnAboutSvg?: boolean;
shouldSkipWebAppCloseConfirmation: boolean; shouldSkipWebAppCloseConfirmation: boolean;
hasContactJoinedNotifications?: boolean;
hasWebNotifications: boolean;
hasPushNotifications: boolean;
notificationSoundVolume: number;
} }
export type IAnchorPosition = { export type IAnchorPosition = {
@ -461,12 +452,6 @@ export enum ManagementScreens {
export type ManagementType = 'user' | 'group' | 'channel' | 'bot'; export type ManagementType = 'user' | 'group' | 'channel' | 'bot';
export type NotifyException = {
isMuted: boolean;
isSilent?: boolean;
shouldShowPreviews?: boolean;
};
export type EmojiKeywords = { export type EmojiKeywords = {
isLoading?: boolean; isLoading?: boolean;
version?: number; version?: number;

View File

@ -3,20 +3,18 @@ import { addCallback } from '../lib/teact/teactn';
import { addActionHandler, getGlobal } from '../global'; import { addActionHandler, getGlobal } from '../global';
import type { import type {
ApiChat, ApiChatFolder, ApiUser, ApiChat, ApiChatFolder, ApiNotifyPeerType, ApiPeerNotifySettings, ApiUser,
} from '../api/types'; } from '../api/types';
import type { GlobalState } from '../global/types'; import type { GlobalState } from '../global/types';
import type { NotifyException, NotifySettings } from '../types';
import type { CallbackManager } from './callbacks'; import type { CallbackManager } from './callbacks';
import { import {
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, DEBUG, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID, ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, DEBUG, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID,
} from '../config'; } from '../config';
import { selectIsChatMuted } from '../global/helpers'; import { getIsChatMuted } from '../global/helpers/notifications';
import { import {
selectChatLastMessage, selectChatLastMessage,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings,
selectTabState, selectTabState,
selectTopics, selectTopics,
} from '../global/selectors'; } from '../global/selectors';
@ -79,8 +77,8 @@ let prevGlobal: {
chatsById: Record<string, ApiChat>; chatsById: Record<string, ApiChat>;
foldersById: Record<string, ApiChatFolder>; foldersById: Record<string, ApiChatFolder>;
usersById: Record<string, ApiUser>; usersById: Record<string, ApiUser>;
notifySettings: NotifySettings; notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>;
notifyExceptions?: Record<number, NotifyException>; notifyExceptions?: Record<number, ApiPeerNotifySettings>;
} = initials.prevGlobal; } = initials.prevGlobal;
let prepared: { let prepared: {
@ -219,8 +217,8 @@ function updateFolderManager(global: GlobalState) {
const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds; const areAllLastMessageIdsChanged = global.chats.lastMessageIds.all !== prevGlobal.lastAllMessageIds;
const areTopicsChanged = global.chats.topicsInfoById !== prevGlobal.topicsInfoById; const areTopicsChanged = global.chats.topicsInfoById !== prevGlobal.topicsInfoById;
const areUsersChanged = global.users.byId !== prevGlobal.usersById; const areUsersChanged = global.users.byId !== prevGlobal.usersById;
const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings; const areNotifyDefaultsChanged = selectNotifyDefaults(global) !== prevGlobal.notifyDefaults;
const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions; const areNotifyExceptionsChanged = global.chats.notifyExceptionById !== prevGlobal.notifyExceptions;
let affectedFolderIds: number[] = []; let affectedFolderIds: number[] = [];
@ -232,7 +230,7 @@ function updateFolderManager(global: GlobalState) {
if (!( if (!(
isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged || areFoldersChanged
|| areChatsChanged || areUsersChanged || areTopicsChanged || areNotifySettingsChanged || areNotifyExceptionsChanged || areChatsChanged || areUsersChanged || areTopicsChanged || areNotifyDefaultsChanged || areNotifyExceptionsChanged
|| areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged || areSavedLastMessageIdsChanged || areAllLastMessageIdsChanged
) )
) { ) {
@ -252,7 +250,7 @@ function updateFolderManager(global: GlobalState) {
affectedFolderIds = affectedFolderIds.concat(updateChats( affectedFolderIds = affectedFolderIds.concat(updateChats(
global, global,
areFoldersChanged || isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged, areFoldersChanged || isAllFolderChanged || isArchivedFolderChanged || isSavedFolderChanged,
areNotifySettingsChanged, areNotifyDefaultsChanged,
areNotifyExceptionsChanged, areNotifyExceptionsChanged,
prevAllFolderListIds, prevAllFolderListIds,
prevArchivedFolderListIds, prevArchivedFolderListIds,
@ -411,7 +409,7 @@ function buildFolderSummary(folder: ApiChatFolder): FolderSummary {
function updateChats( function updateChats(
global: GlobalState, global: GlobalState,
areFoldersChanged: boolean, areFoldersChanged: boolean,
areNotifySettingsChanged: boolean, areNotifyDefaultsChanged: boolean,
areNotifyExceptionsChanged: boolean, areNotifyExceptionsChanged: boolean,
prevAllFolderListIds?: string[], prevAllFolderListIds?: string[],
prevArchivedFolderListIds?: string[], prevArchivedFolderListIds?: string[],
@ -421,8 +419,8 @@ function updateChats(
const newUsersById = global.users.byId; const newUsersById = global.users.byId;
const newAllLastMessageIds = global.chats.lastMessageIds.all; const newAllLastMessageIds = global.chats.lastMessageIds.all;
const newSavedLastMessageIds = global.chats.lastMessageIds.saved; const newSavedLastMessageIds = global.chats.lastMessageIds.saved;
const newNotifySettings = selectNotifySettings(global); const newNotifyDefaults = selectNotifyDefaults(global);
const newNotifyExceptions = selectNotifyExceptions(global); const newNotifyExceptions = global.chats.notifyExceptionById;
const folderSummaries = Object.values(prepared.folderSummariesById); const folderSummaries = Object.values(prepared.folderSummariesById);
const affectedFolderIds = new Set<number>(); const affectedFolderIds = new Set<number>();
@ -445,7 +443,7 @@ function updateChats(
if ( if (
!areFoldersChanged !areFoldersChanged
&& !areNotifySettingsChanged && !areNotifyDefaultsChanged
&& !areNotifyExceptionsChanged && !areNotifyExceptionsChanged
&& chat === prevGlobal.chatsById[chatId] && chat === prevGlobal.chatsById[chatId]
&& newUsersById[chatId] === prevGlobal.usersById[chatId] && newUsersById[chatId] === prevGlobal.usersById[chatId]
@ -463,7 +461,7 @@ function updateChats(
const newSummary = buildChatSummary( const newSummary = buildChatSummary(
global, global,
chat, chat,
newNotifySettings, newNotifyDefaults,
newNotifyExceptions, newNotifyExceptions,
newUsersById[chatId], newUsersById[chatId],
isRemovedFromAll, isRemovedFromAll,
@ -500,7 +498,7 @@ function updateChats(
prevGlobal.usersById = newUsersById; prevGlobal.usersById = newUsersById;
prevGlobal.lastAllMessageIds = newAllLastMessageIds; prevGlobal.lastAllMessageIds = newAllLastMessageIds;
prevGlobal.lastSavedMessageIds = newSavedLastMessageIds; prevGlobal.lastSavedMessageIds = newSavedLastMessageIds;
prevGlobal.notifySettings = newNotifySettings; prevGlobal.notifyDefaults = newNotifyDefaults;
prevGlobal.notifyExceptions = newNotifyExceptions; prevGlobal.notifyExceptions = newNotifyExceptions;
return Array.from(affectedFolderIds); return Array.from(affectedFolderIds);
@ -509,8 +507,8 @@ function updateChats(
function buildChatSummary<T extends GlobalState>( function buildChatSummary<T extends GlobalState>(
global: T, global: T,
chat: ApiChat, chat: ApiChat,
notifySettings: NotifySettings, notifyDefaults?: Record<ApiNotifyPeerType, ApiPeerNotifySettings>,
notifyExceptions?: Record<number, NotifyException>, notifyExceptions?: Record<string, ApiPeerNotifySettings>,
user?: ApiUser, user?: ApiUser,
isRemovedFromAll?: boolean, isRemovedFromAll?: boolean,
isRemovedFromSaved?: boolean, isRemovedFromSaved?: boolean,
@ -548,7 +546,7 @@ function buildChatSummary<T extends GlobalState>(
isListedInAll: Boolean(!isRestricted && !isNotJoined && !migratedTo && !shouldHideServiceChat && !isRemovedFromAll), isListedInAll: Boolean(!isRestricted && !isNotJoined && !migratedTo && !shouldHideServiceChat && !isRemovedFromAll),
isListedInSaved: !isRemovedFromSaved, isListedInSaved: !isRemovedFromSaved,
isArchived: folderId === ARCHIVED_FOLDER_ID, isArchived: folderId === ARCHIVED_FOLDER_ID,
isMuted: selectIsChatMuted(chat, notifySettings, notifyExceptions), isMuted: getIsChatMuted(chat, notifyDefaults, notifyExceptions?.[chat.id]),
isUnread: Boolean(unreadCount || unreadMentionsCount || hasUnreadMark), isUnread: Boolean(unreadCount || unreadMentionsCount || hasUnreadMark),
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
@ -810,8 +808,6 @@ function buildInitials() {
chatsById: {}, chatsById: {},
usersById: {}, usersById: {},
topicsInfoById: {}, topicsInfoById: {},
notifySettings: {} as NotifySettings,
notifyExceptions: {},
}, },
prepared: { prepared: {

View File

@ -16,16 +16,16 @@ import {
getPrivateChatUserId, getPrivateChatUserId,
getUserFullName, getUserFullName,
isChatChannel, isChatChannel,
selectIsChatMuted,
selectShouldShowMessagePreview,
} from '../global/helpers'; } from '../global/helpers';
import { addNotifyExceptions, replaceSettings } from '../global/reducers'; import { getIsChatMuted, getIsChatSilent, getShouldShowMessagePreview } from '../global/helpers/notifications';
import { import {
selectChat, selectChat,
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatWithSelf, selectIsChatWithSelf,
selectNotifyExceptions, selectNotifyDefaults,
selectNotifySettings, selectNotifyException,
selectSettingsKeys,
selectTopicFromMessage,
selectUser, selectUser,
} from '../global/selectors'; } from '../global/selectors';
import { callApi } from '../api/gramjs'; import { callApi } from '../api/gramjs';
@ -34,6 +34,7 @@ import { buildCollectionByKey } from './iteratees';
import * as mediaLoader from './mediaLoader'; import * as mediaLoader from './mediaLoader';
import { oldTranslate } from './oldLangProvider'; import { oldTranslate } from './oldLangProvider';
import { debounce } from './schedulers'; import { debounce } from './schedulers';
import { getServerTime } from './serverTime';
import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './windowEnvironment'; import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './windowEnvironment';
import MessageSummary from '../components/common/MessageSummary'; import MessageSummary from '../components/common/MessageSummary';
@ -107,7 +108,7 @@ notificationSound.setAttribute('mozaudiochannel', 'notification');
export async function playNotifySound(id?: string, volume?: number) { export async function playNotifySound(id?: string, volume?: number) {
if (id !== undefined && soundPlayedIds.has(id)) return; if (id !== undefined && soundPlayedIds.has(id)) return;
const { notificationSoundVolume } = selectNotifySettings(getGlobal()); const { notificationSoundVolume } = selectSettingsKeys(getGlobal());
const currentVolume = volume ? volume / 10 : notificationSoundVolume / 10; const currentVolume = volume ? volume / 10 : notificationSoundVolume / 10;
if (currentVolume === 0) return; if (currentVolume === 0) return;
notificationSound.volume = currentVolume; notificationSound.volume = currentVolume;
@ -181,27 +182,6 @@ export async function unsubscribe() {
await unsubscribeFromPush(subscription); 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 // Load custom emoji from the api if it's not cached already
async function loadCustomEmoji(id: string) { async function loadCustomEmoji(id: string) {
let global = getGlobal(); let global = getGlobal();
@ -293,9 +273,12 @@ export async function subscribe() {
} }
function checkIfShouldNotify(chat: ApiChat, message: Partial<ApiMessage>) { function checkIfShouldNotify(chat: ApiChat, message: Partial<ApiMessage>) {
if (!areSettingsLoaded) return false;
const global = getGlobal(); 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'; const shouldNotifyAboutMessage = message.content?.action?.type !== 'phoneCall';
if (isMuted || !shouldNotifyAboutMessage if (isMuted || !shouldNotifyAboutMessage
|| chat.isNotJoined || !chat.isListed || selectIsChatWithSelf(global, chat.id)) { || chat.isNotJoined || !chat.isListed || selectIsChatWithSelf(global, chat.id)) {
@ -330,7 +313,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
let body: string; let body: string;
if ( if (
!isScreenLocked !isScreenLocked
&& selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global)) && getShouldShowMessagePreview(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id))
) { ) {
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId); const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
@ -386,7 +369,7 @@ export async function notifyAboutCall({
}: { }: {
call: ApiPhoneCall; user: ApiUser; call: ApiPhoneCall; user: ApiUser;
}) { }) {
const { hasWebNotifications } = await loadNotificationSettings(); const { hasWebNotifications } = selectSettingsKeys(getGlobal());
if (document.hasFocus() || !hasWebNotifications) return; if (document.hasFocus() || !hasWebNotifications) return;
const areNotificationsSupported = checkIfNotificationsSupported(); const areNotificationsSupported = checkIfNotificationsSupported();
if (!areNotificationsSupported) return; if (!areNotificationsSupported) return;
@ -420,11 +403,18 @@ export async function notifyAboutMessage({
message, message,
isReaction = false, isReaction = false,
}: { chat: ApiChat; message: Partial<ApiMessage>; isReaction?: boolean }) { }: { chat: ApiChat; message: Partial<ApiMessage>; isReaction?: boolean }) {
const { hasWebNotifications } = await loadNotificationSettings(); const global = getGlobal();
const { hasWebNotifications } = selectSettingsKeys(global);
if (!checkIfShouldNotify(chat, message)) return; 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(); const areNotificationsSupported = checkIfNotificationsSupported();
if (!hasWebNotifications || !areNotificationsSupported) { if (!hasWebNotifications || !areNotificationsSupported) {
if (!message.isSilent && !isReaction && !IS_ELECTRON) { if (!isSilent && !message.isSilent && !isReaction && !IS_ELECTRON) {
// Only play sound if web notifications are disabled // Only play sound if web notifications are disabled
playNotifySoundDebounced(String(message.id) || chat.id); playNotifySoundDebounced(String(message.id) || chat.id);
} }
@ -463,7 +453,7 @@ export async function notifyAboutMessage({
chatId: chat.id, chatId: chat.id,
messageId: message.id, messageId: message.id,
shouldReplaceHistory: true, shouldReplaceHistory: true,
isSilent: message.isSilent, isSilent: isSilent || message.isSilent,
reaction: activeReaction?.reaction, reaction: activeReaction?.reaction,
}, },
}); });