Notification: Respect global notification settings and exceptions (#1123)

This commit is contained in:
Alexander Zinchuk 2021-05-31 15:08:55 +03:00
parent 839b6b65f2
commit 33aaad6bca
26 changed files with 780 additions and 633 deletions

View File

@ -3,9 +3,8 @@ import { ApiUser, ApiUserStatus, ApiUserType } from '../../types';
export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser { export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
const { const {
about, commonChatsCount, pinnedMsgId, botInfo, notifySettings: { silent, muteUntil }, about, commonChatsCount, pinnedMsgId, botInfo,
} = mtpUserFull; } = mtpUserFull;
const isMuted = silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000);
return { return {
...(buildApiUser(mtpUserFull.user) as ApiUser), ...(buildApiUser(mtpUserFull.user) as ApiUser),
@ -13,7 +12,6 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
bio: about, bio: about,
commonChatsCount, commonChatsCount,
pinnedMessageId: pinnedMsgId, pinnedMessageId: pinnedMsgId,
isMuted,
...(botInfo && { botDescription: botInfo.description }), ...(botInfo && { botDescription: botInfo.description }),
}, },
}; };

View File

@ -45,7 +45,7 @@ export {
updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact, updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact,
updateProfilePhoto, uploadProfilePhoto, fetchWallpapers, uploadWallpaper, updateProfilePhoto, uploadProfilePhoto, fetchWallpapers, uploadWallpaper,
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
loadNotificationsSettings, updateContactSignUpNotification, updateNotificationSettings, fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings,
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice, fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
} from './settings'; } from './settings';

View File

@ -156,7 +156,15 @@ export function terminateAllAuthorizations() {
return invokeRequest(new GramJs.auth.ResetAuthorizations()); return invokeRequest(new GramJs.auth.ResetAuthorizations());
} }
export async function loadNotificationsSettings() { export async function fetchNotificationExceptions() {
const result = await invokeRequest(new GramJs.account.GetNotifyExceptions({ compareSound: true }), true);
if (result instanceof GramJs.Updates || result instanceof GramJs.UpdatesCombined) {
updateLocalDb(result);
}
}
export async function fetchNotificationSettings() {
const [ const [
isMutedContactSignUpNotification, isMutedContactSignUpNotification,
privateContactNotificationsSettings, privateContactNotificationsSettings,
@ -212,10 +220,10 @@ export function updateContactSignUpNotification(isSilent: boolean) {
export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broadcast', { export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broadcast', {
isSilent, isSilent,
isShowPreviews, shouldShowPreviews,
}: { }: {
isSilent: boolean; isSilent: boolean;
isShowPreviews: boolean; shouldShowPreviews: boolean;
}) { }) {
let peer: GramJs.TypeInputNotifyPeer; let peer: GramJs.TypeInputNotifyPeer;
if (peerType === 'contact') { if (peerType === 'contact') {
@ -227,7 +235,7 @@ export function updateNotificationSettings(peerType: 'contact' | 'group' | 'broa
} }
const settings = { const settings = {
showPreviews: isShowPreviews, showPreviews: shouldShowPreviews,
silent: isSilent, silent: isSilent,
muteUntil: isSilent ? MAX_INT_32 : undefined, muteUntil: isSilent ? MAX_INT_32 : undefined,
}; };
@ -358,7 +366,10 @@ export async function setPrivacySettings(
return buildPrivacyRules(result.rules); return buildPrivacyRules(result.rules);
} }
function updateLocalDb(result: GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice) { function updateLocalDb(
result: GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice |
GramJs.Updates | GramJs.UpdatesCombined,
) {
result.users.forEach((user) => { result.users.forEach((user) => {
if (user instanceof GramJs.User) { if (user instanceof GramJs.User) {
localDb.users[user.id] = user; localDb.users[user.id] = user;

View File

@ -544,14 +544,18 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
update instanceof GramJs.UpdateNotifySettings update instanceof GramJs.UpdateNotifySettings
&& update.peer instanceof GramJs.NotifyPeer && update.peer instanceof GramJs.NotifyPeer
) { ) {
const { silent, muteUntil } = update.notifySettings; const {
silent, muteUntil, showPreviews, sound,
} = update.notifySettings;
const isMuted = silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000);
onUpdate({ onUpdate({
'@type': 'updateChat', '@type': 'updateNotifyExceptions',
id: getApiChatIdFromMtpPeer(update.peer.peer), id: getApiChatIdFromMtpPeer(update.peer.peer),
chat: { isMuted,
isMuted: silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000), ...(sound === '' && { isSilent: true }),
}, ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }),
}); });
} else if ( } else if (
update instanceof GramJs.UpdateUserTyping update instanceof GramJs.UpdateUserTyping
@ -741,7 +745,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
'@type': 'updateNotifySettings', '@type': 'updateNotifySettings',
peerType, peerType,
isSilent: Boolean(silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000)), isSilent: Boolean(silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000)),
isShowPreviews: Boolean(showPreviews), shouldShowPreviews: Boolean(showPreviews),
}); });
} else if (update instanceof GramJs.UpdatePeerBlocked) { } else if (update instanceof GramJs.UpdatePeerBlocked) {
onUpdate({ onUpdate({

View File

@ -327,7 +327,15 @@ export type ApiUpdateNotifySettings = {
'@type': 'updateNotifySettings'; '@type': 'updateNotifySettings';
peerType: 'contact' | 'group' | 'broadcast'; peerType: 'contact' | 'group' | 'broadcast';
isSilent: boolean; isSilent: boolean;
isShowPreviews: boolean; shouldShowPreviews: boolean;
};
export type ApiUpdateNotifyExceptions = {
'@type': 'updateNotifyExceptions';
id: number;
isMuted: boolean;
isSilent?: boolean;
shouldShowPreviews?: boolean;
}; };
export type updateTwoFaStateWaitCode = { export type updateTwoFaStateWaitCode = {
@ -369,7 +377,7 @@ export type ApiUpdate = (
ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages |
ApiUpdateTwoFaError | updateTwoFaStateWaitCode | ApiUpdateTwoFaError | updateTwoFaStateWaitCode |
ApiUpdateNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -25,7 +25,6 @@ export interface ApiUserFullInfo {
commonChatsCount?: number; commonChatsCount?: number;
botDescription?: string; botDescription?: string;
pinnedMessageId?: number; pinnedMessageId?: number;
isMuted?: boolean;
} }
export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown'; export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown';

View File

@ -12,13 +12,14 @@ import './Badge.scss';
type OwnProps = { type OwnProps = {
chat: ApiChat; chat: ApiChat;
isPinned?: boolean; isPinned?: boolean;
isMuted?: boolean;
}; };
const Badge: FC<OwnProps> = ({ chat, isPinned }) => { const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
const isShown = Boolean(chat.unreadCount || chat.hasUnreadMark || isPinned); const isShown = Boolean(chat.unreadCount || chat.hasUnreadMark || isPinned);
const className = buildClassName( const className = buildClassName(
'Badge', 'Badge',
chat.isMuted && 'muted', isMuted && 'muted',
isPinned && 'pinned', isPinned && 'pinned',
Boolean(chat.unreadCount || chat.hasUnreadMark) && 'unread', Boolean(chat.unreadCount || chat.hasUnreadMark) && 'unread',
); );

View File

@ -25,9 +25,11 @@ import {
getMessageMediaThumbDataUri, getMessageMediaThumbDataUri,
getMessageVideo, getMessageVideo,
getMessageSticker, getMessageSticker,
selectIsChatMuted,
} from '../../../modules/helpers'; } from '../../../modules/helpers';
import { import {
selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList, selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList,
selectNotifySettings, selectNotifyExceptions,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
import { renderActionMessageText } from '../../common/helpers/renderActionMessageText'; import { renderActionMessageText } from '../../common/helpers/renderActionMessageText';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -62,6 +64,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
isMuted?: boolean;
privateChatUser?: ApiUser; privateChatUser?: ApiUser;
actionTargetUser?: ApiUser; actionTargetUser?: ApiUser;
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
@ -87,6 +90,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
isSelected, isSelected,
isPinned, isPinned,
chat, chat,
isMuted,
privateChatUser, privateChatUser,
actionTargetUser, actionTargetUser,
lastMessageSender, lastMessageSender,
@ -255,14 +259,14 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="title"> <div className="title">
<h3>{renderText(getChatTitle(chat, privateChatUser))}</h3> <h3>{renderText(getChatTitle(chat, privateChatUser))}</h3>
{chat.isVerified && <VerifiedIcon />} {chat.isVerified && <VerifiedIcon />}
{chat.isMuted && <i className="icon-muted-chat" />} {isMuted && <i className="icon-muted-chat" />}
{chat.lastMessage && ( {chat.lastMessage && (
<LastMessageMeta message={chat.lastMessage} outgoingStatus={lastMessageOutgoingStatus} /> <LastMessageMeta message={chat.lastMessage} outgoingStatus={lastMessageOutgoingStatus} />
)} )}
</div> </div>
<div className="subtitle"> <div className="subtitle">
{renderLastMessageOrTyping()} {renderLastMessageOrTyping()}
<Badge chat={chat} isPinned={isPinned} /> <Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
</div> </div>
</div> </div>
<DeleteChatModal <DeleteChatModal
@ -307,6 +311,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
lastMessageSender, lastMessageSender,
...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }), ...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }),
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }), ...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }),

View File

@ -5,11 +5,13 @@ import { withGlobal } from '../../../lib/teact/teactn';
import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types'; import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types';
import { GlobalActions } from '../../../global/types'; import { GlobalActions } from '../../../global/types';
import { NotifyException, NotifySettings } from '../../../types';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import { buildCollectionByKey, pick } from '../../../util/iteratees'; import { buildCollectionByKey, pick } from '../../../util/iteratees';
import { captureEvents, SwipeDirection } from '../../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import { getFolderUnreadDialogs } from '../../../modules/helpers'; import { getFolderUnreadDialogs } from '../../../modules/helpers';
import { selectNotifyExceptions, selectNotifySettings } from '../../../modules/selectors';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useThrottledMemo from '../../../hooks/useThrottledMemo'; import useThrottledMemo from '../../../hooks/useThrottledMemo';
@ -24,6 +26,8 @@ type StateProps = {
chatsById: Record<number, ApiChat>; chatsById: Record<number, ApiChat>;
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
chatFoldersById: Record<number, ApiChatFolder>; chatFoldersById: Record<number, ApiChatFolder>;
notifySettings: NotifySettings;
notifyExceptions: Record<number, NotifyException>;
orderedFolderIds?: number[]; orderedFolderIds?: number[];
lastSyncTime?: number; lastSyncTime?: number;
}; };
@ -36,6 +40,8 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
chatsById, chatsById,
usersById, usersById,
chatFoldersById, chatFoldersById,
notifySettings,
notifyExceptions,
orderedFolderIds, orderedFolderIds,
lastSyncTime, lastSyncTime,
loadChatFolders, loadChatFolders,
@ -68,7 +74,7 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
const counters = displayedFolders.map((folder) => { const counters = displayedFolders.map((folder) => {
const { const {
unreadDialogsCount, hasActiveDialogs, unreadDialogsCount, hasActiveDialogs,
} = getFolderUnreadDialogs(chatsById, usersById, folder, chatIds) || {}; } = getFolderUnreadDialogs(chatsById, usersById, folder, notifySettings, notifyExceptions, chatIds) || {};
return { return {
id: folder.id, id: folder.id,
@ -78,7 +84,7 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
}); });
return buildCollectionByKey(counters, 'id'); return buildCollectionByKey(counters, 'id');
}, INFO_THROTTLE, [displayedFolders, chatsById, usersById]); }, INFO_THROTTLE, [displayedFolders, chatsById, usersById, notifySettings, notifyExceptions]);
const folderTabs = useMemo(() => { const folderTabs = useMemo(() => {
if (!displayedFolders || !displayedFolders.length) { if (!displayedFolders || !displayedFolders.length) {
@ -185,6 +191,8 @@ export default memo(withGlobal(
chatFoldersById, chatFoldersById,
orderedFolderIds, orderedFolderIds,
lastSyncTime, lastSyncTime,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadChatFolders']), (setGlobal, actions): DispatchProps => pick(actions, ['loadChatFolders']),

View File

@ -7,13 +7,16 @@ import { GlobalActions } from '../../../global/types';
import { import {
ApiChat, ApiChatFolder, ApiUser, MAIN_THREAD_ID, ApiChat, ApiChatFolder, ApiUser, MAIN_THREAD_ID,
} from '../../../api/types'; } from '../../../api/types';
import { NotifyException, NotifySettings } from '../../../types';
import { ALL_CHATS_PRELOAD_DISABLED, CHAT_HEIGHT_PX, CHAT_LIST_SLICE } from '../../../config'; import { ALL_CHATS_PRELOAD_DISABLED, CHAT_HEIGHT_PX, CHAT_LIST_SLICE } from '../../../config';
import { IS_ANDROID } from '../../../util/environment'; import { IS_ANDROID } from '../../../util/environment';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import { mapValues, pick } from '../../../util/iteratees'; import { mapValues, pick } from '../../../util/iteratees';
import { getChatOrder, prepareChatList, prepareFolderListIds } from '../../../modules/helpers'; import { getChatOrder, prepareChatList, prepareFolderListIds } from '../../../modules/helpers';
import { selectChatFolder, selectCurrentMessageList } from '../../../modules/selectors'; import {
selectChatFolder, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings,
} from '../../../modules/selectors';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { useChatAnimationType } from './hooks'; import { useChatAnimationType } from './hooks';
@ -36,6 +39,8 @@ type StateProps = {
orderedPinnedIds?: number[]; orderedPinnedIds?: number[];
lastSyncTime?: number; lastSyncTime?: number;
isInDiscussionThread?: boolean; isInDiscussionThread?: boolean;
notifySettings: NotifySettings;
notifyExceptions: Record<number, NotifyException>;
}; };
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages'>; type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages'>;
@ -57,14 +62,16 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
orderedPinnedIds, orderedPinnedIds,
lastSyncTime, lastSyncTime,
isInDiscussionThread, isInDiscussionThread,
notifySettings,
notifyExceptions,
loadMoreChats, loadMoreChats,
preloadTopChatMessages, preloadTopChatMessages,
}) => { }) => {
const [currentListIds, currentPinnedIds] = useMemo(() => { const [currentListIds, currentPinnedIds] = useMemo(() => {
return folderType === 'folder' && chatFolder return folderType === 'folder' && chatFolder
? prepareFolderListIds(chatsById, usersById, chatFolder) ? prepareFolderListIds(chatsById, usersById, chatFolder, notifySettings, notifyExceptions)
: [listIds, orderedPinnedIds]; : [listIds, orderedPinnedIds];
}, [folderType, chatsById, usersById, chatFolder, listIds, orderedPinnedIds]); }, [folderType, chatFolder, chatsById, usersById, notifySettings, notifyExceptions, listIds, orderedPinnedIds]);
const [orderById, orderedIds] = useMemo(() => { const [orderById, orderedIds] = useMemo(() => {
if (!currentListIds || (folderType === 'folder' && !chatFolder)) { if (!currentListIds || (folderType === 'folder' && !chatFolder)) {
@ -202,6 +209,8 @@ export default memo(withGlobal<OwnProps>(
chatFolder, chatFolder,
}), }),
isInDiscussionThread: currentThreadId !== MAIN_THREAD_ID, isInDiscussionThread: currentThreadId !== MAIN_THREAD_ID,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadMoreChats', 'preloadTopChatMessages']), (setGlobal, actions): DispatchProps => pick(actions, ['loadMoreChats', 'preloadTopChatMessages']),

View File

@ -22,7 +22,7 @@ type StateProps = {
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
'loadNotificationsSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' 'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings'
)>; )>;
const SettingsNotifications: FC<StateProps & DispatchProps> = ({ const SettingsNotifications: FC<StateProps & DispatchProps> = ({
@ -33,13 +33,13 @@ const SettingsNotifications: FC<StateProps & DispatchProps> = ({
hasBroadcastNotifications, hasBroadcastNotifications,
hasBroadcastMessagePreview, hasBroadcastMessagePreview,
hasContactJoinedNotifications, hasContactJoinedNotifications,
loadNotificationsSettings, loadNotificationSettings,
updateContactSignUpNotification, updateContactSignUpNotification,
updateNotificationSettings, updateNotificationSettings,
}) => { }) => {
useEffect(() => { useEffect(() => {
loadNotificationsSettings(); loadNotificationSettings();
}, [loadNotificationsSettings]); }, [loadNotificationSettings]);
const handleSettingsChange = useCallback(( const handleSettingsChange = useCallback((
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,
@ -49,14 +49,14 @@ const SettingsNotifications: FC<StateProps & DispatchProps> = ({
const currentIsSilent = peerType === 'contact' const currentIsSilent = peerType === 'contact'
? !hasPrivateChatsNotifications ? !hasPrivateChatsNotifications
: !(peerType === 'group' ? hasGroupNotifications : hasBroadcastNotifications); : !(peerType === 'group' ? hasGroupNotifications : hasBroadcastNotifications);
const currentIsShowPreviews = peerType === 'contact' const currentShouldShowPreviews = peerType === 'contact'
? hasPrivateChatsMessagePreview ? hasPrivateChatsMessagePreview
: (peerType === 'group' ? hasGroupMessagePreview : hasBroadcastMessagePreview); : (peerType === 'group' ? hasGroupMessagePreview : hasBroadcastMessagePreview);
updateNotificationSettings({ updateNotificationSettings({
peerType, peerType,
...(setting === 'silent' && { isSilent: !e.target.checked, isShowPreviews: currentIsShowPreviews }), ...(setting === 'silent' && { isSilent: !e.target.checked, shouldShowPreviews: currentShouldShowPreviews }),
...(setting === 'showPreviews' && { isShowPreviews: e.target.checked, isSilent: currentIsSilent }), ...(setting === 'showPreviews' && { shouldShowPreviews: e.target.checked, isSilent: currentIsSilent }),
}); });
}, [ }, [
hasBroadcastMessagePreview, hasBroadcastNotifications, hasBroadcastMessagePreview, hasBroadcastNotifications,
@ -151,7 +151,7 @@ export default memo(withGlobal((global): StateProps => {
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
'loadNotificationsSettings', 'loadNotificationSettings',
'updateContactSignUpNotification', 'updateContactSignUpNotification',
'updateNotificationSettings', 'updateNotificationSettings',
]))(SettingsNotifications)); ]))(SettingsNotifications));

View File

@ -5,9 +5,11 @@ import { withGlobal } from '../../../../lib/teact/teactn';
import { GlobalActions } from '../../../../global/types'; import { GlobalActions } from '../../../../global/types';
import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types'; import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types';
import { NotifyException, NotifySettings } from '../../../../types';
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
import { pick } from '../../../../util/iteratees'; import { pick } from '../../../../util/iteratees';
import { selectNotifyExceptions, selectNotifySettings } from '../../../../modules/selectors';
import { throttle } from '../../../../util/schedulers'; import { throttle } from '../../../../util/schedulers';
import getAnimationData from '../../../common/helpers/animatedAssets'; import getAnimationData from '../../../common/helpers/animatedAssets';
import { getFolderDescriptionText } from '../../../../modules/helpers'; import { getFolderDescriptionText } from '../../../../modules/helpers';
@ -29,6 +31,8 @@ type StateProps = {
orderedFolderIds?: number[]; orderedFolderIds?: number[];
foldersById: Record<number, ApiChatFolder>; foldersById: Record<number, ApiChatFolder>;
recommendedChatFolders?: ApiChatFolder[]; recommendedChatFolders?: ApiChatFolder[];
notifySettings: NotifySettings;
notifyExceptions: Record<number, NotifyException>;
}; };
type DispatchProps = Pick<GlobalActions, 'loadRecommendedChatFolders' | 'addChatFolder' | 'showError'>; type DispatchProps = Pick<GlobalActions, 'loadRecommendedChatFolders' | 'addChatFolder' | 'showError'>;
@ -45,6 +49,8 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
orderedFolderIds, orderedFolderIds,
foldersById, foldersById,
recommendedChatFolders, recommendedChatFolders,
notifySettings,
notifyExceptions,
loadRecommendedChatFolders, loadRecommendedChatFolders,
addChatFolder, addChatFolder,
showError, showError,
@ -96,10 +102,12 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
return { return {
id: folder.id, id: folder.id,
title: folder.title, title: folder.title,
subtitle: getFolderDescriptionText(chatsById, usersById, folder, chatIds, lang), subtitle: getFolderDescriptionText(
chatsById, usersById, folder, notifySettings, notifyExceptions, chatIds, lang,
),
}; };
}); });
}, [orderedFolderIds, chatsById, foldersById, usersById, lang]); }, [orderedFolderIds, chatsById, foldersById, usersById, notifySettings, notifyExceptions, lang]);
const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => { const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => {
if (Object.keys(foldersById).length >= MAX_ALLOWED_FOLDERS) { if (Object.keys(foldersById).length >= MAX_ALLOWED_FOLDERS) {
@ -222,6 +230,8 @@ export default memo(withGlobal<OwnProps>(
orderedFolderIds, orderedFolderIds,
foldersById, foldersById,
recommendedChatFolders, recommendedChatFolders,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadRecommendedChatFolders', 'addChatFolder', 'showError']), (setGlobal, actions): DispatchProps => pick(actions, ['loadRecommendedChatFolders', 'addChatFolder', 'showError']),

View File

@ -209,5 +209,6 @@ function reduceSettings(global: GlobalState): GlobalState['settings'] {
return { return {
byKey, byKey,
privacy: {}, privacy: {},
notifyExceptions: {},
}; };
} }

View File

@ -116,6 +116,7 @@ export const INITIAL_STATE: GlobalState = {
language: 'en', language: 'en',
}, },
privacy: {}, privacy: {},
notifyExceptions: {},
}, },
twoFaSettings: {}, twoFaSettings: {},

View File

@ -34,6 +34,7 @@ import {
Receipt, Receipt,
ApiPrivacyKey, ApiPrivacyKey,
ApiPrivacySettings, ApiPrivacySettings,
NotifyException,
} from '../types'; } from '../types';
export type MessageListType = 'thread' | 'pinned' | 'scheduled'; export type MessageListType = 'thread' | 'pinned' | 'scheduled';
@ -365,6 +366,7 @@ export type GlobalState = {
byKey: ISettings; byKey: ISettings;
loadedWallpapers?: ApiWallpaper[]; loadedWallpapers?: ApiWallpaper[];
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>; privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
notifyExceptions: Record<number, NotifyException>;
}; };
twoFaSettings: { twoFaSettings: {
@ -434,8 +436,9 @@ export type ActionTypes = (
'updatePassword' | 'updateRecoveryEmail' | 'clearPassword' | 'provideTwoFaEmailCode' | 'checkPassword' | 'updatePassword' | 'updateRecoveryEmail' | 'clearPassword' | 'provideTwoFaEmailCode' | 'checkPassword' |
'loadBlockedContacts' | 'blockContact' | 'unblockContact' | 'loadBlockedContacts' | 'blockContact' | 'unblockContact' |
'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' | 'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' |
'loadNotificationsSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' | 'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' |
'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | 'setPrivacySettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | 'setPrivacySettings' |
'loadNotificationExceptions' |
// Stickers & GIFs // Stickers & GIFs
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' | 'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' | 'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |

File diff suppressed because it is too large Load Diff

View File

@ -939,6 +939,7 @@ account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings =
account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.confirmPasswordEmail#8fdf1920 code:string = Bool;
account.getContactSignUpNotification#9f07c728 = Bool; account.getContactSignUpNotification#9f07c728 = Bool;
account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; account.setContactSignUpNotification#cff43f61 silent:Bool = Bool;
account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true peer:flags.0?InputNotifyPeer = Updates;
account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPaperSettings = WallPaper; account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPaperSettings = WallPaper;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull; users.getFullUser#ca30a5b1 id:InputUser = UserFull;

View File

@ -939,6 +939,7 @@ account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings =
account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.confirmPasswordEmail#8fdf1920 code:string = Bool;
account.getContactSignUpNotification#9f07c728 = Bool; account.getContactSignUpNotification#9f07c728 = Bool;
account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; account.setContactSignUpNotification#cff43f61 silent:Bool = Bool;
account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true peer:flags.0?InputNotifyPeer = Updates;
account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPaperSettings = WallPaper; account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPaperSettings = WallPaper;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull; users.getFullUser#ca30a5b1 id:InputUser = UserFull;

View File

@ -304,9 +304,13 @@ addReducer('terminateAllAuthorizations', () => {
})(); })();
}); });
addReducer('loadNotificationsSettings', () => { addReducer('loadNotificationExceptions', () => {
callApi('fetchNotificationExceptions');
});
addReducer('loadNotificationSettings', () => {
(async () => { (async () => {
const result = await callApi('loadNotificationsSettings'); const result = await callApi('fetchNotificationSettings');
if (!result) { if (!result) {
return; return;
} }
@ -316,16 +320,16 @@ addReducer('loadNotificationsSettings', () => {
}); });
addReducer('updateNotificationSettings', (global, actions, payload) => { addReducer('updateNotificationSettings', (global, actions, payload) => {
const { peerType, isSilent, isShowPreviews } = payload!; const { peerType, isSilent, shouldShowPreviews } = payload!;
(async () => { (async () => {
const result = await callApi('updateNotificationSettings', peerType, { isSilent, isShowPreviews }); const result = await callApi('updateNotificationSettings', peerType, { isSilent, shouldShowPreviews });
if (!result) { if (!result) {
return; return;
} }
setGlobal(updateNotifySettings(getGlobal(), peerType, isSilent, isShowPreviews)); setGlobal(updateNotifySettings(getGlobal(), peerType, isSilent, shouldShowPreviews));
})(); })();
}); });

View File

@ -1,13 +1,27 @@
import { addReducer } from '../../../lib/teact/teactn'; import { addReducer, setGlobal } from '../../../lib/teact/teactn';
import { ApiUpdate } from '../../../api/types'; import { ApiUpdate } from '../../../api/types';
import { GlobalState } from '../../../global/types'; import { GlobalState } from '../../../global/types';
import { updateNotifySettings } from '../../reducers'; import { addNotifyException, updateChat, updateNotifySettings } from '../../reducers';
addReducer('apiUpdate', (global, actions, update: ApiUpdate): GlobalState | undefined => { addReducer('apiUpdate', (global, actions, update: ApiUpdate): GlobalState | undefined => {
switch (update['@type']) { switch (update['@type']) {
case 'updateNotifySettings': { case 'updateNotifySettings': {
return updateNotifySettings(global, update.peerType, update.isSilent, update.isShowPreviews); return updateNotifySettings(global, update.peerType, update.isSilent, update.shouldShowPreviews);
}
case 'updateNotifyExceptions': {
const {
id, isMuted, isSilent, shouldShowPreviews,
} = update;
const chat = global.chats.byId[id];
if (chat) {
global = updateChat(global, id, { isMuted });
}
setGlobal(addNotifyException(global, id, { isMuted, isSilent, shouldShowPreviews }));
break;
} }
} }

View File

@ -6,6 +6,7 @@ import {
ApiChatFolder, ApiChatFolder,
MAIN_THREAD_ID, MAIN_THREAD_ID,
} from '../../api/types'; } from '../../api/types';
import { NotifyException, NotifySettings } from '../../types';
import { ARCHIVED_FOLDER_ID } from '../../config'; import { ARCHIVED_FOLDER_ID } from '../../config';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
@ -200,6 +201,17 @@ 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<number, NotifyException>,
) {
return !(notifyExceptions[chat.id] && !notifyExceptions[chat.id].isMuted) && (
chat.isMuted
|| (isChatPrivate(chat.id) && !notifySettings.hasPrivateChatsNotifications)
|| (isChatChannel(chat) && !notifySettings.hasBroadcastNotifications)
|| (isChatGroup(chat) && !notifySettings.hasGroupNotifications)
);
}
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);
} }
@ -208,6 +220,8 @@ export function prepareFolderListIds(
chatsById: Record<number, ApiChat>, chatsById: Record<number, ApiChat>,
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
folder: ApiChatFolder, folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions: Record<number, NotifyException>,
chatIdsCache?: number[], chatIdsCache?: number[],
) { ) {
const excludedChatIds = folder.excludedChatIds ? new Set(folder.excludedChatIds) : undefined; const excludedChatIds = folder.excludedChatIds ? new Set(folder.excludedChatIds) : undefined;
@ -216,7 +230,14 @@ export function prepareFolderListIds(
const listIds = (chatIdsCache || Object.keys(chatsById).map(Number)) const listIds = (chatIdsCache || Object.keys(chatsById).map(Number))
.filter((id) => { .filter((id) => {
return filterChatFolder( return filterChatFolder(
chatsById[id], folder, usersById, excludedChatIds, includedChatIds, pinnedChatIds, chatsById[id],
folder,
usersById,
notifySettings,
notifyExceptions,
excludedChatIds,
includedChatIds,
pinnedChatIds,
); );
}); });
@ -227,6 +248,8 @@ function filterChatFolder(
chat: ApiChat, chat: ApiChat,
folder: ApiChatFolder, folder: ApiChatFolder,
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
notifySettings: NotifySettings,
notifyExceptions: Record<number, NotifyException>,
excludedChatIds?: Set<number>, excludedChatIds?: Set<number>,
includedChatIds?: Set<number>, includedChatIds?: Set<number>,
pinnedChatIds?: Set<number>, pinnedChatIds?: Set<number>,
@ -247,7 +270,7 @@ function filterChatFolder(
return false; return false;
} }
if (chat.isMuted && folder.excludeMuted) { if (folder.excludeMuted && selectIsChatMuted(chat, notifySettings, notifyExceptions)) {
return false; return false;
} }
@ -340,9 +363,11 @@ export function getFolderUnreadDialogs(
chatsById: Record<number, ApiChat>, chatsById: Record<number, ApiChat>,
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
folder: ApiChatFolder, folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions: Record<number, NotifyException>,
chatIdsCache: number[], chatIdsCache: number[],
) { ) {
const [listIds] = prepareFolderListIds(chatsById, usersById, folder, chatIdsCache); const [listIds] = prepareFolderListIds(chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache);
const listedChats = listIds const listedChats = listIds
.map((id) => chatsById[id]) .map((id) => chatsById[id])
@ -366,6 +391,8 @@ export function getFolderDescriptionText(
chatsById: Record<number, ApiChat>, chatsById: Record<number, ApiChat>,
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
folder: ApiChatFolder, folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions: Record<number, NotifyException>,
chatIdsCache: number[], chatIdsCache: number[],
lang: LangFn, lang: LangFn,
) { ) {
@ -383,7 +410,7 @@ export function getFolderDescriptionText(
|| (excludedChatIds && excludedChatIds.length) || (excludedChatIds && excludedChatIds.length)
|| (includedChatIds && includedChatIds.length) || (includedChatIds && includedChatIds.length)
) { ) {
const length = getFolderChatsCount(chatsById, usersById, folder, chatIdsCache); const length = getFolderChatsCount(chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache);
return lang('Chats', length); return lang('Chats', length);
} }
@ -407,9 +434,13 @@ function getFolderChatsCount(
chatsById: Record<number, ApiChat>, chatsById: Record<number, ApiChat>,
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
folder: ApiChatFolder, folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions: Record<number, NotifyException>,
chatIdsCache: number[], chatIdsCache: number[],
) { ) {
const [listIds, pinnedIds] = prepareFolderListIds(chatsById, usersById, folder, chatIdsCache); const [listIds, pinnedIds] = prepareFolderListIds(
chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache,
);
const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder'); const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder');
return pinnedChats.length + otherChats.length; return pinnedChats.length + otherChats.length;
} }

View File

@ -1,5 +1,5 @@
import { GlobalState } from '../../global/types'; import { GlobalState } from '../../global/types';
import { ISettings } from '../../types'; import { ISettings, NotifyException } from '../../types';
export function replaceSettings(global: GlobalState, newSettings?: Partial<ISettings>): GlobalState { export function replaceSettings(global: GlobalState, newSettings?: Partial<ISettings>): GlobalState {
return { return {
@ -14,24 +14,39 @@ export function replaceSettings(global: GlobalState, newSettings?: Partial<ISett
}; };
} }
export function addNotifyException(
global: GlobalState, id: number, notifyException: NotifyException,
): GlobalState {
return {
...global,
settings: {
...global.settings,
notifyExceptions: {
...global.settings.notifyExceptions,
[id]: notifyException,
},
},
};
}
export function updateNotifySettings( export function updateNotifySettings(
global: GlobalState, peerType: 'contact' | 'group' | 'broadcast', isSilent?: boolean, isShowPreviews?: boolean, global: GlobalState, peerType: 'contact' | 'group' | 'broadcast', isSilent?: boolean, shouldShowPreviews?: boolean,
) { ) {
switch (peerType) { switch (peerType) {
case 'contact': case 'contact':
return replaceSettings(global, { return replaceSettings(global, {
...(typeof isSilent !== 'undefined' && { hasPrivateChatsNotifications: !isSilent }), ...(typeof isSilent !== 'undefined' && { hasPrivateChatsNotifications: !isSilent }),
...(typeof isShowPreviews !== 'undefined' && { hasPrivateChatsMessagePreview: isShowPreviews }), ...(typeof shouldShowPreviews !== 'undefined' && { hasPrivateChatsMessagePreview: shouldShowPreviews }),
}); });
case 'group': case 'group':
return replaceSettings(global, { return replaceSettings(global, {
...(typeof isSilent !== 'undefined' && { hasGroupNotifications: !isSilent }), ...(typeof isSilent !== 'undefined' && { hasGroupNotifications: !isSilent }),
...(typeof isShowPreviews !== 'undefined' && { hasGroupMessagePreview: isShowPreviews }), ...(typeof shouldShowPreviews !== 'undefined' && { hasGroupMessagePreview: shouldShowPreviews }),
}); });
case 'broadcast': case 'broadcast':
return replaceSettings(global, { return replaceSettings(global, {
...(typeof isSilent !== 'undefined' && { hasBroadcastNotifications: !isSilent }), ...(typeof isSilent !== 'undefined' && { hasBroadcastNotifications: !isSilent }),
...(typeof isShowPreviews !== 'undefined' && { hasBroadcastMessagePreview: isShowPreviews }), ...(typeof shouldShowPreviews !== 'undefined' && { hasBroadcastMessagePreview: shouldShowPreviews }),
}); });
default: default:

View File

@ -7,3 +7,4 @@ export * from './localSearch';
export * from './management'; export * from './management';
export * from './symbols'; export * from './symbols';
export * from './payments'; export * from './payments';
export * from './settings';

View File

@ -0,0 +1,9 @@
import { GlobalState } from '../../global/types';
export function selectNotifySettings(global: GlobalState) {
return global.settings.byKey;
}
export function selectNotifyExceptions(global: GlobalState) {
return global.settings.notifyExceptions;
}

View File

@ -20,7 +20,17 @@ export interface IAlbum {
mainMessage: ApiMessage; mainMessage: ApiMessage;
} }
export interface ISettings extends Record<string, any> { export type NotifySettings = {
hasPrivateChatsNotifications?: boolean;
hasPrivateChatsMessagePreview?: boolean;
hasGroupNotifications?: boolean;
hasGroupMessagePreview?: boolean;
hasBroadcastNotifications?: boolean;
hasBroadcastMessagePreview?: boolean;
hasContactJoinedNotifications?: boolean;
};
export interface ISettings extends NotifySettings, Record<string, any> {
messageTextSize: number; messageTextSize: number;
customBackground?: string; customBackground?: string;
patternColor?: string; patternColor?: string;
@ -37,13 +47,6 @@ export interface ISettings extends Record<string, any> {
shouldSuggestStickers: boolean; shouldSuggestStickers: boolean;
shouldLoopStickers: boolean; shouldLoopStickers: boolean;
hasPassword?: boolean; hasPassword?: boolean;
hasPrivateChatsNotifications?: boolean;
hasPrivateChatsMessagePreview?: boolean;
hasGroupNotifications?: boolean;
hasGroupMessagePreview?: boolean;
hasBroadcastNotifications?: boolean;
hasBroadcastMessagePreview?: boolean;
hasContactJoinedNotifications?: boolean;
languages?: ApiLanguage[]; languages?: ApiLanguage[];
language: 'en' | 'fr' | 'de' | 'it' | 'pt' | 'ru' | 'es' | 'uk'; language: 'en' | 'fr' | 'de' | 'it' | 'pt' | 'ru' | 'es' | 'uk';
} }
@ -269,3 +272,9 @@ export enum ManagementScreens {
} }
export type ManagementType = 'user' | 'group' | 'channel'; export type ManagementType = 'user' | 'group' | 'channel';
export type NotifyException = {
isMuted: boolean;
isSilent?: boolean;
shouldShowPreviews?: boolean;
};

View File

@ -125,14 +125,18 @@ export async function unsubscribe() {
} }
// Load notification settings from the api // Load notification settings from the api
async function loadNotificationsSettings() { async function loadNotificationSettings() {
const result = await callApi('loadNotificationsSettings'); const [result] = await Promise.all([
callApi('fetchNotificationSettings'),
callApi('fetchNotificationExceptions'),
]);
if (!result) return; if (!result) return;
setGlobal(replaceSettings(getGlobal(), result)); setGlobal(replaceSettings(getGlobal(), result));
} }
export async function subscribe() { export async function subscribe() {
loadNotificationsSettings(); loadNotificationSettings();
if (!checkIfPushSupported()) { if (!checkIfPushSupported()) {
// Ask for notification permissions only if service worker notifications are not supported // Ask for notification permissions only if service worker notifications are not supported