From 691c0c4263e4e7bb394d813704cc11c24b4fb7d0 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 22 Oct 2021 13:49:20 +0300 Subject: [PATCH] Support local changelogs; Fix marking read for service notifications --- .eslintrc | 10 ++- src/api/gramjs/methods/chats.ts | 36 +++++++-- src/api/gramjs/methods/messages.ts | 2 +- src/api/gramjs/updater.ts | 20 +++-- src/api/types/updates.ts | 7 +- src/components/main/Main.tsx | 15 +++- src/components/middle/MessageList.tsx | 2 +- src/components/middle/message/Message.tsx | 11 +-- .../middle/message/hooks/useOuterHandlers.ts | 10 +-- src/global/cache.ts | 5 ++ src/global/initial.ts | 2 + src/global/types.ts | 10 ++- src/modules/actions/api/chats.ts | 16 ++-- src/modules/actions/api/messages.ts | 12 ++- src/modules/actions/api/sync.ts | 41 +++++----- src/modules/actions/apiUpdaters/messages.ts | 11 +++ src/modules/actions/ui/messages.ts | 76 ++++++++++++++++++- src/modules/helpers/messages.ts | 2 +- src/modules/reducers/messages.ts | 5 +- src/modules/selectors/chats.ts | 8 +- src/modules/selectors/messages.ts | 33 +++++--- src/versionNotification.txt | 3 + webpack.config.js | 17 +++-- 23 files changed, 267 insertions(+), 87 deletions(-) create mode 100644 src/versionNotification.txt diff --git a/.eslintrc b/.eslintrc index 8b2c8010..06ca86fd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,10 @@ "error", 120 ], - "array-bracket-newline": [2, "consistent"], + "array-bracket-newline": [ + 2, + "consistent" + ], "no-null/no-null": 2, "no-console": "error", "semi": "error", @@ -68,5 +71,8 @@ }, "parserOptions": { "project": "./tsconfig.json" - } + }, + "ignorePatterns": [ + "webpack.config.js" + ] } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index a7810ae5..13ef1d90 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -12,7 +12,9 @@ import { ApiChatAdminRights, } from '../../types'; -import { DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../../config'; +import { + DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID, +} from '../../../config'; import { invokeRequest, uploadFile } from './client'; import { buildApiChatFromDialog, @@ -52,12 +54,14 @@ export async function fetchChats({ archived, withPinned, serverTimeOffset, + lastLocalServiceMessage, }: { limit: number; offsetDate?: number; archived?: boolean; withPinned?: boolean; serverTimeOffset: number; + lastLocalServiceMessage?: ApiMessage; }) { const result = await invokeRequest(new GramJs.messages.GetDialogs({ offsetPeer: new GramJs.InputPeerEmpty(), @@ -111,7 +115,17 @@ export async function fetchChats({ const peerEntity = peersByKey[getPeerKey(dialog.peer)]; const chat = buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset); - chat.lastMessage = lastMessagesByChatId[chat.id]; + + if ( + chat.id === SERVICE_NOTIFICATIONS_USER_ID + && lastLocalServiceMessage + && (!lastMessagesByChatId[chat.id] || lastLocalServiceMessage.date > lastMessagesByChatId[chat.id].date) + ) { + chat.lastMessage = lastLocalServiceMessage; + } else { + chat.lastMessage = lastMessagesByChatId[chat.id]; + } + chat.isListed = true; chats.push(chat); @@ -232,8 +246,10 @@ export async function fetchChat({ export async function requestChatUpdate({ chat, serverTimeOffset, + lastLocalMessage, + noLastMessage, }: { - chat: ApiChat; serverTimeOffset: number; + chat: ApiChat; serverTimeOffset: number; lastLocalMessage?: ApiMessage; noLastMessage?: boolean; }) { const { id, accessHash } = chat; @@ -260,14 +276,23 @@ export async function requestChatUpdate({ updateLocalDb(result); - const lastMessage = buildApiMessage(result.messages[0]); + let lastMessage: ApiMessage | undefined; + if (!noLastMessage) { + const lastRemoteMessage = buildApiMessage(result.messages[0]); + + if (lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date))) { + lastMessage = lastLocalMessage; + } else { + lastMessage = lastRemoteMessage; + } + } onUpdate({ '@type': 'updateChat', id, chat: { ...buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset), - lastMessage, + ...(lastMessage && { lastMessage }), }, }); } @@ -442,6 +467,7 @@ export async function updateChatMutedState({ void requestChatUpdate({ chat, serverTimeOffset, + noLastMessage: true, }); } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 07535092..5a92b8d0 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -688,7 +688,7 @@ export async function markMessageListRead({ } if (threadId === MAIN_THREAD_ID) { - void requestChatUpdate({ chat, serverTimeOffset }); + void requestChatUpdate({ chat, serverTimeOffset, noLastMessage: true }); } else { void requestThreadInfoUpdate({ chat, threadId }); } diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 6650a2ad..8485830a 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -82,7 +82,6 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { || update instanceof GramJs.UpdateNewChannelMessage || update instanceof GramJs.UpdateShortChatMessage || update instanceof GramJs.UpdateShortMessage - || update instanceof GramJs.UpdateServiceNotification ) { let message: ApiMessage | undefined; let shouldForceReply: boolean | undefined; @@ -91,13 +90,6 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { message = buildApiMessageFromShortChat(update); } else if (update instanceof GramJs.UpdateShortMessage) { message = buildApiMessageFromShort(update); - } else if (update instanceof GramJs.UpdateServiceNotification) { - const currentDate = Date.now() / 1000 + serverTimeOffset; - message = buildApiMessageFromNotification(update, currentDate); - - if (isMessageWithMedia(update)) { - addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update)); - } } else { // TODO Remove if proven not reproducing if (update.message instanceof GramJs.MessageEmpty) { @@ -319,6 +311,18 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { }); }, DELETE_MISSING_CHANNEL_MESSAGE_DELAY); } + } else if (update instanceof GramJs.UpdateServiceNotification) { + const currentDate = Date.now() / 1000 + serverTimeOffset; + const message = buildApiMessageFromNotification(update, currentDate); + + if (isMessageWithMedia(update)) { + addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update)); + } + + onUpdate({ + '@type': 'updateServiceNotification', + message, + }); } else if (( originRequest instanceof GramJs.messages.SendMessage || originRequest instanceof GramJs.messages.SendMedia diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 8f5ad756..b43037e7 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -239,6 +239,11 @@ export type ApiUpdateMessagePollVote = { options: string[]; }; +export type ApiUpdateServiceNotification = { + '@type': 'updateServiceNotification'; + message: ApiMessage; +}; + export type ApiUpdateDeleteMessages = { '@type': 'deleteMessages'; ids: number[]; @@ -381,7 +386,7 @@ export type ApiUpdate = ( ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders | ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdateChannelMessages | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | - ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | + ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification | ApiDeleteUser | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos | ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateError | ApiUpdateResetContacts | diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index fc85720b..9874bf3b 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -18,6 +18,7 @@ import { selectIsForwardModalOpen, selectIsMediaViewerOpen, selectIsRightColumnShown, + selectIsServiceChatReady, } from '../../modules/selectors'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import buildClassName from '../../util/buildClassName'; @@ -62,11 +63,12 @@ type StateProps = { shouldSkipHistoryAnimations?: boolean; language?: LangCode; openedStickerSetShortName?: string; + isServiceChatReady?: boolean; }; type DispatchProps = Pick; const NOTIFICATION_INTERVAL = 1000; @@ -92,6 +94,7 @@ const Main: FC = ({ shouldSkipHistoryAnimations, language, openedStickerSetShortName, + isServiceChatReady, loadAnimatedEmojis, loadNotificationSettings, loadNotificationExceptions, @@ -100,6 +103,7 @@ const Main: FC = ({ loadEmojiKeywords, loadCountryList, openStickerSetShortName, + checkVersionNotification, }) => { if (DEBUG && !DEBUG_isLogged) { DEBUG_isLogged = true; @@ -128,6 +132,12 @@ const Main: FC = ({ loadTopInlineBots, loadEmojiKeywords, loadCountryList, language, ]); + useEffect(() => { + if (lastSyncTime && isServiceChatReady) { + checkVersionNotification(); + } + }, [lastSyncTime, isServiceChatReady, checkVersionNotification]); + useEffect(() => { if (lastSyncTime && LOCATION_HASH.startsWith('#?tgaddr=')) { processDeepLink(decodeURIComponent(LOCATION_HASH.substr('#?tgaddr='.length))); @@ -302,10 +312,11 @@ export default memo(withGlobal( shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, language: global.settings.byKey.language, openedStickerSetShortName: global.openedStickerSetShortName, + isServiceChatReady: selectIsServiceChatReady(global), }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline', - 'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadCountryList', + 'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadCountryList', 'checkVersionNotification', ]), )(Main)); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 0a38570b..1ca2d906 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -396,7 +396,7 @@ const MessageList: FC = ({ } const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; - const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`); + const anchor = anchorIdRef.current && document.getElementById(anchorIdRef.current); const unreadDivider = ( !anchor && memoUnreadDividerBeforeIdRef.current diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 77f83968..d667d061 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -297,17 +297,13 @@ const Message: FC = ({ const senderPeer = forwardInfo ? originSender : sender; const selectMessage = useCallback((e?: React.MouseEvent, groupedId?: string) => { - if (isLocal) { - return; - } - toggleMessageSelection({ messageId, groupedId, ...(e?.shiftKey && { withShift: true }), ...(isAlbum && { childMessageIds: album!.messages.map(({ id }) => id) }), }); - }, [isLocal, toggleMessageSelection, messageId, isAlbum, album]); + }, [toggleMessageSelection, messageId, isAlbum, album]); const { handleMouseDown, @@ -320,7 +316,6 @@ const Message: FC = ({ selectMessage, ref, messageId, - isLocal, isAlbum, Boolean(isInSelectMode), Boolean(canReply), @@ -705,12 +700,12 @@ const Message: FC = ({ data-last-message-id={album ? album.messages[album.messages.length - 1].id : undefined} data-has-unread-mention={message.hasUnreadMention} /> - {!isLocal && !isInDocumentGroup && ( + {!isInDocumentGroup && (
{isSelected && }
)} - {!isLocal && isLastInDocumentGroup && ( + {isLastInDocumentGroup && (
, groupedId?: string) => void, containerRef: RefObject, messageId: number, - isLocal: boolean, isAlbum: boolean, isInSelectMode: boolean, canReply: boolean, @@ -28,14 +27,11 @@ export default function useOuterHandlers( function handleMouseDown(e: React.MouseEvent) { preventMessageInputBlur(e); - - if (!isLocal) { - handleBeforeContextMenu(e); - } + handleBeforeContextMenu(e); } function handleClick(e: React.MouseEvent) { - if (isInSelectMode && !isLocal) { + if (isInSelectMode) { selectMessage(e); } else if (IS_ANDROID) { const target = e.target as HTMLDivElement; @@ -111,7 +107,7 @@ export default function useOuterHandlers( return { handleMouseDown: !isInSelectMode ? handleMouseDown : undefined, handleClick, - handleContextMenu: !isInSelectMode && !isLocal ? handleContextMenu : undefined, + handleContextMenu: !isInSelectMode ? handleContextMenu : undefined, handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined, handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined, isSwiped, diff --git a/src/global/cache.ts b/src/global/cache.ts index 165976eb..76660a06 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -127,6 +127,10 @@ function readCache(initialState: GlobalState): GlobalState { byChatId: {}, }; } + + if (!cached.serviceNotifications) { + cached.serviceNotifications = []; + } } const newState = { @@ -171,6 +175,7 @@ function updateCache() { 'push', 'shouldShowContextMenuHint', 'leftColumnWidth', + 'serviceNotifications', ]), isChatInfoShown: reduceShowChatInfo(global), users: reduceUsers(global), diff --git a/src/global/initial.ts b/src/global/initial.ts index 25fc20a4..b20885ca 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -167,4 +167,6 @@ export const INITIAL_STATE: GlobalState = { activeDownloads: { byChatId: {}, }, + + serviceNotifications: [], }; diff --git a/src/global/types.ts b/src/global/types.ts index 029fd671..63933544 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -72,6 +72,12 @@ export interface Thread { replyStack?: number[]; } +export interface ServiceNotification { + id: number; + message: ApiMessage; + version?: string; +} + export type GlobalState = { isChatInfoShown: boolean; isLeftColumnShown: boolean; @@ -443,6 +449,8 @@ export type GlobalState = { }; shouldShowContextMenuHint?: boolean; + + serviceNotifications: ServiceNotification[]; }; export type ActionTypes = ( @@ -521,7 +529,7 @@ export type ActionTypes = ( // misc 'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | - 'deleteDeviceToken' | + 'deleteDeviceToken' | 'checkVersionNotification' | 'createServiceNotification' | // payment 'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' | 'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' | diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 540d4827..595834f3 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -15,7 +15,7 @@ import { RE_TME_INVITE_LINK, RE_TME_LINK, TIPS_USERNAME, - LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, + LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, SERVICE_NOTIFICATIONS_USER_ID, } from '../../../config'; import { callApi } from '../../../api/gramjs'; import { @@ -38,7 +38,7 @@ import { selectChatByUsername, selectThreadTopMessageId, selectCurrentMessageList, - selectThreadInfo, selectCurrentChat, + selectThreadInfo, selectCurrentChat, selectLastServiceNotification, } from '../../selectors'; import { buildCollectionByKey } from '../../../util/iteratees'; import { debounce, pause, throttle } from '../../../util/schedulers'; @@ -255,6 +255,9 @@ addReducer('requestChatUpdate', (global, actions, payload) => { void callApi('requestChatUpdate', { chat, serverTimeOffset, + ...(chatId === SERVICE_NOTIFICATIONS_USER_ID && { + lastLocalMessage: selectLastServiceNotification(global)?.message, + }), }); }); @@ -964,12 +967,15 @@ addReducer('deleteChatMember', (global, actions, payload) => { }); async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) { + let global = getGlobal(); + const result = await callApi('fetchChats', { limit: CHAT_LIST_LOAD_SLICE, offsetDate, archived: listType === 'archived', - withPinned: getGlobal().chats.orderedPinnedIds[listType] === undefined, - serverTimeOffset: getGlobal().serverTimeOffset, + withPinned: global.chats.orderedPinnedIds[listType] === undefined, + serverTimeOffset: global.serverTimeOffset, + lastLocalServiceMessage: selectLastServiceNotification(global)?.message, }); if (!result) { @@ -982,7 +988,7 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: number, off chatIds.shift(); } - let global = getGlobal(); + global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = updateChats(global, buildCollectionByKey(result.chats, 'id')); diff --git a/src/modules/actions/api/messages.ts b/src/modules/actions/api/messages.ts index a2d5064e..f09aa2af 100644 --- a/src/modules/actions/api/messages.ts +++ b/src/modules/actions/api/messages.ts @@ -15,7 +15,7 @@ import { } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; -import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE } from '../../../config'; +import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { callApi, cancelApiProgress } from '../../../api/gramjs'; import { areSortedArraysIntersecting, buildCollectionByKey, split } from '../../../util/iteratees'; import { @@ -663,11 +663,15 @@ async function loadViewportMessages( messages, users, chats, threadInfos, } = result; - const byId = buildCollectionByKey(messages, 'id'); - const ids = Object.keys(byId).map(Number); - let global = getGlobal(); + const localMessages = chatId === SERVICE_NOTIFICATIONS_USER_ID + ? global.serviceNotifications.map(({ message }) => message) + : []; + const allMessages = ([] as ApiMessage[]).concat(messages, localMessages); + const byId = buildCollectionByKey(allMessages, 'id'); + const ids = Object.keys(byId).map(Number); + global = addChatMessagesById(global, chatId, byId); global = isOutlying ? updateOutlyingIds(global, chatId, threadId, ids) diff --git a/src/modules/actions/api/sync.ts b/src/modules/actions/api/sync.ts index 1d0568fb..291e2c34 100644 --- a/src/modules/actions/api/sync.ts +++ b/src/modules/actions/api/sync.ts @@ -3,12 +3,12 @@ import { } from '../../../lib/teact/teactn'; import { - ApiChat, ApiFormattedText, ApiUser, MAIN_THREAD_ID, + ApiChat, ApiFormattedText, ApiMessage, ApiUser, MAIN_THREAD_ID, } from '../../../api/types'; import { GlobalActions } from '../../../global/types'; import { - CHAT_LIST_LOAD_SLICE, DEBUG, MESSAGE_LIST_SLICE, + CHAT_LIST_LOAD_SLICE, DEBUG, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID, } from '../../../config'; import { callApi } from '../../../api/gramjs'; import { buildCollectionByKey } from '../../../util/iteratees'; @@ -22,6 +22,9 @@ import { updateChatListSecondaryInfo, updateThreadInfos, replaceThreadParam, + updateListedIds, + safeReplaceViewportIds, + addChatMessagesById, } from '../../reducers'; import { selectUser, @@ -31,6 +34,7 @@ import { selectChatMessage, selectThreadInfo, selectCountNotMutedUnread, + selectLastServiceNotification, } from '../../selectors'; import { isChatPrivate } from '../../helpers'; @@ -91,16 +95,20 @@ async function afterSync(actions: GlobalActions) { } async function loadAndReplaceChats() { + let global = getGlobal(); + const result = await callApi('fetchChats', { limit: CHAT_LIST_LOAD_SLICE, withPinned: true, - serverTimeOffset: getGlobal().serverTimeOffset, + serverTimeOffset: global.serverTimeOffset, + lastLocalServiceMessage: selectLastServiceNotification(global)?.message, }); + if (!result) { return undefined; } - let global = getGlobal(); + global = getGlobal(); const { recentlyFoundChatIds } = global.globalSearch; const { userIds: contactIds } = global.contactList || {}; @@ -211,29 +219,25 @@ async function loadAndReplaceMessages(savedUsers?: ApiUser[]) { if (result && newCurrentChatId === currentChatId) { const currentMessageListInfo = global.messages.byChatId[currentChatId]; - const byId = buildCollectionByKey(result.messages, 'id'); + const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID + ? global.serviceNotifications.map(({ message }) => message) + : []; + const allMessages = ([] as ApiMessage[]).concat(result.messages, localMessages); + const byId = buildCollectionByKey(allMessages, 'id'); const listedIds = Object.keys(byId).map(Number); global = { ...global, messages: { ...global.messages, - byChatId: { - [currentChatId]: { - byId, - threadsById: { - [MAIN_THREAD_ID]: { - ...(currentMessageListInfo?.threadsById[MAIN_THREAD_ID]), - listedIds, - viewportIds: listedIds, - outlyingIds: undefined, - }, - }, - }, - }, + byChatId: {}, }, }; + global = addChatMessagesById(global, currentChatId, byId); + global = updateListedIds(global, currentChatId, MAIN_THREAD_ID, listedIds); + global = safeReplaceViewportIds(global, currentChatId, MAIN_THREAD_ID, listedIds); + if (currentThreadId && threadInfo && threadInfo.originChannelId) { const { originChannelId } = threadInfo; const currentMessageListInfoOrigin = global.messages.byChatId[originChannelId]; @@ -275,6 +279,7 @@ async function loadAndReplaceMessages(savedUsers?: ApiUser[]) { }; } } + global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateThreadInfos(global, currentChatId, result.threadInfos); diff --git a/src/modules/actions/apiUpdaters/messages.ts b/src/modules/actions/apiUpdaters/messages.ts index ac7b4b8e..2b2ae4f1 100644 --- a/src/modules/actions/apiUpdaters/messages.ts +++ b/src/modules/actions/apiUpdaters/messages.ts @@ -38,6 +38,7 @@ import { selectFirstUnreadId, selectChat, selectIsChatWithBot, + selectIsServiceChatReady, } from '../../selectors'; import { getMessageContent, isChatPrivate, isMessageLocal } from '../../helpers'; @@ -433,6 +434,16 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { break; } + + case 'updateServiceNotification': { + const { message } = update; + + if (selectIsServiceChatReady(global)) { + actions.createServiceNotification({ message }); + } + + break; + } } }); diff --git a/src/modules/actions/ui/messages.ts b/src/modules/actions/ui/messages.ts index 18eb8b67..61106719 100644 --- a/src/modules/actions/ui/messages.ts +++ b/src/modules/actions/ui/messages.ts @@ -1,9 +1,14 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; -import { MAIN_THREAD_ID } from '../../../api/types'; +import { ApiMessage, MAIN_THREAD_ID } from '../../../api/types'; import { FocusDirection } from '../../../types'; -import { ANIMATION_END_DELAY, FAST_SMOOTH_MAX_DURATION } from '../../../config'; +import { + ANIMATION_END_DELAY, + APP_VERSION, + FAST_SMOOTH_MAX_DURATION, + SERVICE_NOTIFICATIONS_USER_ID, +} from '../../../config'; import { IS_TOUCH_ENV } from '../../../util/environment'; import { enterMessageSelectMode, @@ -29,10 +34,15 @@ import { selectReplyStack, } from '../../selectors'; import { findLast } from '../../../util/iteratees'; +import { getServerTime } from '../../../util/serverTime'; + +// @ts-ignore +import versionNotification from '../../../versionNotification.txt'; const FOCUS_DURATION = 1500; const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY; const POLL_RESULT_OPEN_DELAY_MS = 450; +const SERVICE_NOTIFICATIONS_MAX_AMOUNT = 1e4; let blurTimeout: number | undefined; @@ -506,3 +516,65 @@ addReducer('closePollModal', (global) => { isPollModalOpen: false, }; }); + +addReducer('checkVersionNotification', (global, actions) => { + const currentVersion = APP_VERSION.split('.').slice(0, 2).join('.'); + const { serviceNotifications } = global; + + if (serviceNotifications.find(({ version }) => version === currentVersion)) { + return; + } + + const message: Omit = { + chatId: SERVICE_NOTIFICATIONS_USER_ID, + date: getServerTime(global.serverTimeOffset), + content: { + text: { + text: versionNotification, + }, + }, + isOutgoing: false, + }; + + actions.createServiceNotification({ + message, + version: currentVersion, + }); +}); + +addReducer('createServiceNotification', (global, actions, payload) => { + const { message, version } = payload; + const { serviceNotifications } = global; + const serviceChat = selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)!; + + const maxId = Math.max( + serviceChat.lastMessage?.id || 0, + ...serviceNotifications.map(({ id }) => id), + ); + const fractionalPart = (serviceNotifications.length + 1) / SERVICE_NOTIFICATIONS_MAX_AMOUNT; + // The fractional ID is made of the largest integer ID and an incremented fractional part + const id = Math.floor(maxId) + fractionalPart; + + message.id = id; + + const serviceNotification = { + id, + message, + version, + }; + + setGlobal({ + ...global, + serviceNotifications: [ + ...serviceNotifications, + serviceNotification, + ], + }); + + actions.apiUpdate({ + '@type': 'newMessage', + id: message.id, + chatId: message.chatId, + message, + }); +}); diff --git a/src/modules/helpers/messages.ts b/src/modules/helpers/messages.ts index fef24628..1eb063ab 100644 --- a/src/modules/helpers/messages.ts +++ b/src/modules/helpers/messages.ts @@ -199,7 +199,7 @@ export function isActionMessage(message: ApiMessage) { } export function isServiceNotificationMessage(message: ApiMessage) { - return message.chatId === SERVICE_NOTIFICATIONS_USER_ID && isMessageLocal(message); + return message.chatId === SERVICE_NOTIFICATIONS_USER_ID && Math.round(message.id) !== message.id; } export function isAnonymousOwnMessage(message: ApiMessage) { diff --git a/src/modules/reducers/messages.ts b/src/modules/reducers/messages.ts index d8a868e0..24f64893 100644 --- a/src/modules/reducers/messages.ts +++ b/src/modules/reducers/messages.ts @@ -366,14 +366,15 @@ export function safeReplaceViewportIds( threadId: number, newViewportIds: number[], ): GlobalState { - const viewportIds = selectViewportIds(global, chatId, threadId) || []; + const currentIds = selectViewportIds(global, chatId, threadId) || []; + const newIds = orderHistoryIds(newViewportIds); return replaceThreadParam( global, chatId, threadId, 'viewportIds', - areSortedArraysEqual(viewportIds, newViewportIds) ? viewportIds : newViewportIds, + areSortedArraysEqual(currentIds, newIds) ? currentIds : newIds, ); } diff --git a/src/modules/selectors/chats.ts b/src/modules/selectors/chats.ts index b1e44f91..ec18e6c0 100644 --- a/src/modules/selectors/chats.ts +++ b/src/modules/selectors/chats.ts @@ -5,7 +5,9 @@ import { getPrivateChatUserId, isChatChannel, isChatPrivate, isHistoryClearMessage, isUserBot, isUserOnline, selectIsChatMuted, } from '../helpers'; import { selectUser } from './users'; -import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE } from '../../config'; +import { + ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID, +} from '../../config'; import { selectNotifyExceptions, selectNotifySettings } from './settings'; export function selectChat(global: GlobalState, chatId: number): ApiChat | undefined { @@ -175,3 +177,7 @@ export function selectCountNotMutedUnread(global: GlobalState) { return acc; }, 0); } + +export function selectIsServiceChatReady(global: GlobalState) { + return Boolean(selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)); +} diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index e523c624..8b4fe8b7 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -348,18 +348,20 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes const isBasicGroup = isChatBasicGroup(chat); const isSuperGroup = isChatSuperGroup(chat); const isChannel = isChatChannel(chat); + const isLocal = isMessageLocal(message); const isServiceNotification = isServiceNotificationMessage(message); - const isOwn = isOwnMessage(message); const isAction = isActionMessage(message); const { content } = message; + const canEditMessagesIndefinitely = isChatWithSelf || (isSuperGroup && getHasAdminRight(chat, 'pinMessages')) || (isChannel && getHasAdminRight(chat, 'editMessages')); const isMessageEditable = ( - (canEditMessagesIndefinitely - || getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME) - && !( + ( + canEditMessagesIndefinitely + || getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME + ) && !( content.sticker || content.contact || content.poll || content.action || content.audio || (content.video?.isRound) ) @@ -367,7 +369,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes && !message.viaBotId ); - const canReply = getCanPostInChat(chat, threadId) && !isServiceNotification; + const canReply = !isLocal && !isServiceNotification && getCanPostInChat(chat, threadId); const hasPinPermission = isPrivate || ( chat.isCreator @@ -375,7 +377,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes || getHasAdminRight(chat, 'pinMessages') ); - let canPin = !isAction && hasPinPermission; + let canPin = !isLocal && !isServiceNotification && !isAction && hasPinPermission; let canUnpin = false; const pinnedMessageIds = selectPinnedIds(global, chat.id); @@ -385,27 +387,29 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes canPin = !canUnpin; } - const canDelete = isPrivate + const canDelete = !isLocal && !isServiceNotification && ( + isPrivate || isOwn || isBasicGroup || chat.isCreator - || getHasAdminRight(chat, 'deleteMessages'); + || getHasAdminRight(chat, 'deleteMessages') + ); const canReport = !isPrivate && !isOwn; - const canDeleteForAll = canDelete && !isServiceNotification && ( + const canDeleteForAll = canDelete && ( (isPrivate && !isChatWithSelf) || (isBasicGroup && ( isOwn || getHasAdminRight(chat, 'deleteMessages') || chat.isCreator )) ); - const canEdit = !isAction && isMessageEditable && ( + const canEdit = !isLocal && !isAction && isMessageEditable && ( isOwn || (isChannel && (chat.isCreator || getHasAdminRight(chat, 'editMessages'))) ); - const canForward = !isAction && !isServiceNotification; + const canForward = !isLocal && !isServiceNotification && !isAction; const hasSticker = Boolean(message.content.sticker); const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!); @@ -751,3 +755,10 @@ export function selectShouldAutoPlayMedia(global: GlobalState, message: ApiMessa export function selectShouldLoopStickers(global: GlobalState) { return global.settings.byKey.shouldLoopStickers; } + +export function selectLastServiceNotification(global: GlobalState) { + const { serviceNotifications } = global; + const maxId = Math.max(...serviceNotifications.map(({ id }) => id)); + + return serviceNotifications.find(({ id }) => id === maxId); +} diff --git a/src/versionNotification.txt b/src/versionNotification.txt new file mode 100644 index 00000000..76d1afff --- /dev/null +++ b/src/versionNotification.txt @@ -0,0 +1,3 @@ +Welcome to dev version. + +This is a demo notification. diff --git a/webpack.config.js b/webpack.config.js index 4bf40cdf..b267c859 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,12 +1,15 @@ const path = require('path'); const dotenv = require('dotenv'); -const { EnvironmentPlugin, ProvidePlugin } = require('webpack'); +const { + EnvironmentPlugin, + ProvidePlugin, +} = require('webpack'); const HtmlPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const TerserJSPlugin = require('terser-webpack-plugin'); -const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); dotenv.config(); @@ -84,7 +87,7 @@ module.exports = (env = {}, argv = {}) => { }, }, { - test: /\.tl$/i, + test: /\.(txt|tl)$/i, loader: 'raw-loader', }, ], @@ -92,11 +95,11 @@ module.exports = (env = {}, argv = {}) => { resolve: { extensions: ['.js', '.ts', '.tsx'], fallback: { - path: require.resolve("path-browserify"), - os: require.resolve("os-browserify/browser"), - buffer: require.resolve("buffer/"), + path: require.resolve('path-browserify'), + os: require.resolve('os-browserify/browser'), + buffer: require.resolve('buffer/'), fs: false, - } + }, }, plugins: [ new HtmlPlugin({