From dcd380326326c219e12e1e6cd6e201dcd4c7542c Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 19 Jun 2021 11:20:55 +0300 Subject: [PATCH] Support server time offset (#1186) --- src/api/gramjs/apiBuilders/chats.ts | 6 +++-- src/api/gramjs/apiBuilders/messages.ts | 8 ++++--- src/api/gramjs/methods/chats.ts | 22 ++++++++++++++----- src/api/gramjs/methods/client.ts | 5 +++++ src/api/gramjs/methods/messages.ts | 18 ++++++++++----- src/api/gramjs/methods/settings.ts | 13 +++++++---- src/api/gramjs/updater.ts | 19 ++++++++++------ src/api/types/updates.ts | 8 ++++++- src/components/common/PrivateChatInfo.tsx | 8 ++++--- src/components/left/main/ContactList.tsx | 8 ++++--- src/components/left/newChat/NewChatStep1.tsx | 12 +++++++--- src/components/left/search/ChatResults.tsx | 19 +++++++++++----- src/components/main/ForwardPicker.tsx | 8 +++++-- src/components/middle/composer/Composer.tsx | 20 ++++++++++------- src/components/middle/message/Poll.tsx | 7 ++++-- src/components/right/Profile.tsx | 5 ++++- src/components/right/ProfileInfo.tsx | 8 ++++--- .../right/hooks/useProfileViewportIds.ts | 5 +++-- .../right/management/ManageGroupMembers.tsx | 7 ++++-- .../ManageGroupUserPermissionsCreate.tsx | 10 +++++++-- src/global/initial.ts | 1 + src/global/types.ts | 1 + src/lib/gramjs/network/MTProtoSender.js | 11 +++++++++- src/lib/gramjs/network/index.js | 7 ++++++ src/modules/actions/api/chats.ts | 13 ++++++++--- src/modules/actions/api/messages.ts | 11 ++++++++-- src/modules/actions/api/settings.ts | 7 ++++-- src/modules/actions/api/sync.ts | 3 +++ src/modules/actions/api/users.ts | 13 +++++++---- src/modules/actions/apiUpdaters/initial.ts | 13 ++++++++++- src/modules/helpers/chats.ts | 3 ++- src/modules/helpers/users.ts | 15 ++++++++----- src/modules/selectors/messages.ts | 3 ++- src/util/notifications.ts | 5 ++++- 34 files changed, 235 insertions(+), 87 deletions(-) diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 5c5f389d4..15ba16a38 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -57,12 +57,13 @@ function buildApiChatFieldsFromPeerEntity( export function buildApiChatFromDialog( dialog: GramJs.Dialog, peerEntity: GramJs.TypeUser | GramJs.TypeChat, + serverTimeOffset: number, ): ApiChat { const { peer, folderId, unreadMark, unreadCount, unreadMentionsCount, notifySettings: { silent, muteUntil }, readOutboxMaxId, readInboxMaxId, } = dialog; - const isMuted = silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000); + const isMuted = silent || (typeof muteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < muteUntil * 1000); return { id: getApiChatIdFromMtpPeer(peer), @@ -314,6 +315,7 @@ export function buildChatMembers( export function buildChatTypingStatus( update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping, + serverTimeOffset: number, ) { let action: string = ''; if (update.action instanceof GramJs.SendMessageCancelAction) { @@ -347,7 +349,7 @@ export function buildChatTypingStatus( return { action, ...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }), - timestamp: Date.now(), + timestamp: Date.now() + serverTimeOffset * 1000, }; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 88e3032c5..3b119c223 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -96,7 +96,7 @@ export function buildApiMessageFromNotification( return { id: localId, chatId: SERVICE_NOTIFICATIONS_USER_ID, - date: notification.inboxDate || (currentDate / 1000), + date: notification.inboxDate || currentDate, content, isOutgoing: false, }; @@ -715,6 +715,7 @@ export function buildLocalMessage( poll?: ApiNewPoll, groupedId?: string, scheduledAt?: number, + serverTimeOffset = 0, ): ApiMessage { const localId = localMessageCounter++; const media = attachment && buildUploadingMedia(attachment); @@ -735,7 +736,7 @@ export function buildLocalMessage( ...(gif && { video: gif }), ...(poll && buildNewPoll(poll, localId)), }, - date: scheduledAt || Math.round(Date.now() / 1000), + date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset, isOutgoing: !isChannel, senderId: currentUserId, ...(replyingTo && { replyToMessageId: replyingTo }), @@ -750,6 +751,7 @@ export function buildLocalMessage( export function buildForwardedMessage( toChat: ApiChat, message: ApiMessage, + serverTimeOffset: number, ): ApiMessage { const localId = localMessageCounter++; const { @@ -770,7 +772,7 @@ export function buildForwardedMessage( id: localId, chatId: toChat.id, content, - date: Math.round(Date.now() / 1000), + date: Math.round(Date.now() / 1000) + serverTimeOffset, isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel', senderId: currentUserId, sendingState: 'messageSendingStatePending', diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index bce3c2d07..be6c63675 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -50,11 +50,13 @@ export async function fetchChats({ offsetDate, archived, withPinned, + serverTimeOffset, }: { limit: number; offsetDate?: number; archived?: boolean; withPinned?: boolean; + serverTimeOffset: number; }) { const result = await invokeRequest(new GramJs.messages.GetDialogs({ offsetPeer: new GramJs.InputPeerEmpty(), @@ -110,7 +112,7 @@ export async function fetchChats({ } const peerEntity = peersByKey[getPeerKey(dialog.peer)]; - const chat = buildApiChatFromDialog(dialog, peerEntity); + const chat = buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset); chat.lastMessage = lastMessagesByChatId[chat.id]; chats.push(chat); @@ -228,7 +230,12 @@ export async function fetchChat({ return { chatId: chat.id }; } -export async function requestChatUpdate(chat: ApiChat) { +export async function requestChatUpdate({ + chat, + serverTimeOffset, +}: { + chat: ApiChat; serverTimeOffset: number; +}) { const { id, accessHash } = chat; const result = await invokeRequest(new GramJs.messages.GetPeerDialogs({ @@ -260,7 +267,7 @@ export async function requestChatUpdate(chat: ApiChat) { '@type': 'updateChat', id, chat: { - ...buildApiChatFromDialog(dialog, peerEntity), + ...buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset), lastMessage, }, }); @@ -396,9 +403,9 @@ async function getFullChannelInfo( } export async function updateChatMutedState({ - chat, isMuted, + chat, isMuted, serverTimeOffset, }: { - chat: ApiChat; isMuted: boolean; + chat: ApiChat; isMuted: boolean; serverTimeOffset: number; }) { await invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyPeer({ @@ -407,7 +414,10 @@ export async function updateChatMutedState({ settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : undefined }), })); - void requestChatUpdate(chat); + void requestChatUpdate({ + chat, + serverTimeOffset, + }); } export async function createChannel({ diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index fae7eb222..fbb139afb 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -117,6 +117,11 @@ function handleGramJsUpdate(update: any) { isConnected = update.state === connection.UpdateConnectionState.connected; } else if (update instanceof GramJs.UpdatesTooLong) { void handleTerminatedSession(); + } else if (update instanceof connection.UpdateServerTimeOffset) { + onUpdate({ + '@type': 'updateServerTimeOffset', + serverTimeOffset: update.timeOffset, + }); } } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index df6c7a11f..685b5e74a 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -168,6 +168,7 @@ export function sendMessage( scheduledAt, groupedId, noWebPage, + serverTimeOffset, }: { chat: ApiChat; text?: string; @@ -181,11 +182,12 @@ export function sendMessage( scheduledAt?: number; groupedId?: string; noWebPage?: boolean; + serverTimeOffset?: number; }, onProgress?: ApiOnProgress, ) { const localMessage = buildLocalMessage( - chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt, + chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt, serverTimeOffset, ); onUpdate({ '@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage', @@ -405,14 +407,16 @@ export async function editMessage({ text, entities, noWebPage, + serverTimeOffset, }: { chat: ApiChat; message: ApiMessage; text: string; entities?: ApiMessageEntity[]; noWebPage?: boolean; + serverTimeOffset: number; }) { - const isScheduled = message.date * 1000 > Date.now(); + const isScheduled = message.date * 1000 > Date.now() + serverTimeOffset * 1000; const messageUpdate: Partial = { content: { ...message.content, @@ -613,9 +617,9 @@ export async function deleteHistory({ } export async function markMessageListRead({ - chat, threadId, maxId, + chat, threadId, maxId, serverTimeOffset, }: { - chat: ApiChat; threadId: number; maxId?: number; + chat: ApiChat; threadId: number; maxId?: number; serverTimeOffset: number; }) { const isChannel = getEntityTypeById(chat.id) === 'channel'; @@ -638,7 +642,7 @@ export async function markMessageListRead({ } if (threadId === MAIN_THREAD_ID) { - void requestChatUpdate(chat); + void requestChatUpdate({ chat, serverTimeOffset }); } else { void requestThreadInfoUpdate({ chat, threadId }); } @@ -968,16 +972,18 @@ export async function forwardMessages({ fromChat, toChat, messages, + serverTimeOffset, }: { fromChat: ApiChat; toChat: ApiChat; messages: ApiMessage[]; + serverTimeOffset: number; }) { const messageIds = messages.map(({ id }) => id); const randomIds = messages.map(generateRandomBigInt); messages.forEach((message, index) => { - const localMessage = buildForwardedMessage(toChat, message); + const localMessage = buildForwardedMessage(toChat, message, serverTimeOffset); localDb.localMessages[String(randomIds[index])] = localMessage; onUpdate({ diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 17669746c..3d2083817 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -164,7 +164,9 @@ export async function fetchNotificationExceptions() { } } -export async function fetchNotificationSettings() { +export async function fetchNotificationSettings({ + serverTimeOffset, +}: { serverTimeOffset: number }) { const [ isMutedContactSignUpNotification, privateContactNotificationsSettings, @@ -200,15 +202,18 @@ export async function fetchNotificationSettings() { return { hasContactJoinedNotifications: !isMutedContactSignUpNotification, hasPrivateChatsNotifications: !( - privateSilent || (typeof privateMuteUntil === 'number' && Date.now() < privateMuteUntil * 1000) + privateSilent + || (typeof privateMuteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < privateMuteUntil * 1000) ), hasPrivateChatsMessagePreview: privateShowPreviews, hasGroupNotifications: !( - groupSilent || (typeof groupMuteUntil === 'number' && Date.now() < groupMuteUntil * 1000) + groupSilent || (typeof groupMuteUntil === 'number' + && Date.now() + serverTimeOffset * 1000 < groupMuteUntil * 1000) ), hasGroupMessagePreview: groupShowPreviews, hasBroadcastNotifications: !( - broadcastSilent || (typeof broadcastMuteUntil === 'number' && Date.now() < broadcastMuteUntil * 1000) + broadcastSilent || (typeof broadcastMuteUntil === 'number' + && Date.now() + serverTimeOffset * 1000 < broadcastMuteUntil * 1000) ), hasBroadcastMessagePreview: broadcastShowPreviews, }; diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index e9fb1662c..2de9769be 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -49,9 +49,12 @@ export function init(_onUpdate: OnApiUpdate) { } const sentMessageIds = new Set(); +let serverTimeOffset = 0; export function updater(update: Update, originRequest?: GramJs.AnyRequest) { - if (update instanceof connection.UpdateConnectionState) { + if (update instanceof connection.UpdateServerTimeOffset) { + serverTimeOffset = update.timeOffset; + } else if (update instanceof connection.UpdateConnectionState) { let connectionState: ApiUpdateConnectionStateType; switch (update.state) { @@ -88,7 +91,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { } else if (update instanceof GramJs.UpdateShortMessage) { message = buildApiMessageFromShort(update); } else if (update instanceof GramJs.UpdateServiceNotification) { - const currentDate = Date.now(); + const currentDate = Date.now() / 1000 + serverTimeOffset; message = buildApiMessageFromNotification(update, currentDate); if (isMessageWithMedia(update)) { @@ -347,7 +350,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { sentMessageIds.add(update.id); // Edge case for "Send When Online" - const isAlreadySent = 'date' in update && update.date * 1000 < Date.now(); + const isAlreadySent = 'date' in update && update.date * 1000 < Date.now() + serverTimeOffset * 1000; onUpdate({ '@type': localMessage.isScheduled && !isAlreadySent @@ -549,7 +552,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { silent, muteUntil, showPreviews, sound, } = update.notifySettings; - const isMuted = silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000); + const isMuted = silent + || (typeof muteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < muteUntil * 1000); onUpdate({ '@type': 'updateNotifyExceptions', @@ -569,7 +573,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { onUpdate({ '@type': 'updateChatTypingStatus', id, - typingStatus: buildChatTypingStatus(update), + typingStatus: buildChatTypingStatus(update, serverTimeOffset), }); } else if (update instanceof GramJs.UpdateChannelUserTyping) { const id = getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel); @@ -577,7 +581,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { onUpdate({ '@type': 'updateChatTypingStatus', id, - typingStatus: buildChatTypingStatus(update), + typingStatus: buildChatTypingStatus(update, serverTimeOffset), }); } else if (update instanceof GramJs.UpdateChannel) { const { _entities } = update; @@ -745,7 +749,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { onUpdate({ '@type': 'updateNotifySettings', peerType, - isSilent: Boolean(silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000)), + isSilent: Boolean(silent + || (typeof muteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < muteUntil * 1000)), shouldShowPreviews: Boolean(showPreviews), }); } else if (update instanceof GramJs.UpdatePeerBlocked) { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 06d46ff75..3af1d1fd0 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -373,6 +373,11 @@ export type ApiUpdatePrivacy = { }; }; +export type ApiUpdateServerTimeOffset = { + '@type': 'updateServerTimeOffset'; + serverTimeOffset: number; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -389,7 +394,8 @@ export type ApiUpdate = ( ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateTwoFaError | updateTwoFaStateWaitCode | - ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy + ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | + ApiUpdateServerTimeOffset ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index f8d7da3e2..1d6507c6e 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -36,6 +36,7 @@ type StateProps = { user?: ApiUser; isSavedMessages?: boolean; areMessagesLoaded: boolean; + serverTimeOffset: number; } & Pick; type DispatchProps = Pick; @@ -56,6 +57,7 @@ const PrivateChatInfo: FC = ({ lastSyncTime, loadFullUser, openMediaViewer, + serverTimeOffset, }) => { const { id: userId } = user || {}; const fullName = getUserFullName(user); @@ -106,7 +108,7 @@ const PrivateChatInfo: FC = ({ return (
{withUsername && user.username && {user.username}} - {getUserStatus(lang, user)} + {getUserStatus(lang, user, serverTimeOffset)}
); } @@ -139,13 +141,13 @@ const PrivateChatInfo: FC = ({ export default memo(withGlobal( (global, { userId, forceShowSelf }): StateProps => { - const { lastSyncTime } = global; + const { lastSyncTime, serverTimeOffset } = global; const user = selectUser(global, userId); const isSavedMessages = !forceShowSelf && user && user.isSelf; const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); return { - lastSyncTime, user, isSavedMessages, areMessagesLoaded, + lastSyncTime, user, isSavedMessages, areMessagesLoaded, serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index 55be93e8c..2b2280129 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -25,6 +25,7 @@ export type OwnProps = { type StateProps = { usersById: Record; contactIds?: number[]; + serverTimeOffset: number; }; type DispatchProps = Pick; @@ -32,7 +33,7 @@ type DispatchProps = Pick; const runThrottled = throttle((cb) => cb(), 60000, true); const ContactList: FC = ({ - filter, usersById, contactIds, loadContactList, openChat, + filter, usersById, contactIds, loadContactList, openChat, serverTimeOffset, }) => { // Due to the parent Transition, this component never gets unmounted, // that's why we use throttled API call on every update. @@ -63,8 +64,8 @@ const ContactList: FC = ({ return fullName && searchWords(fullName, filter); }) : contactIds; - return sortUserIds(resultIds, usersById); - }, [filter, usersById, contactIds]); + return sortUserIds(resultIds, usersById, undefined, serverTimeOffset); + }, [contactIds, filter, usersById, serverTimeOffset]); const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter)); @@ -100,6 +101,7 @@ export default memo(withGlobal( return { usersById, contactIds, + serverTimeOffset: global.serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'openChat']), diff --git a/src/components/left/newChat/NewChatStep1.tsx b/src/components/left/newChat/NewChatStep1.tsx index 3145110e1..8a957a694 100644 --- a/src/components/left/newChat/NewChatStep1.tsx +++ b/src/components/left/newChat/NewChatStep1.tsx @@ -33,6 +33,7 @@ type StateProps = { isSearching?: boolean; localUserIds?: number[]; globalUserIds?: number[]; + serverTimeOffset?: number; }; type DispatchProps = Pick; @@ -53,6 +54,7 @@ const NewChatStep1: FC = ({ isSearching, localUserIds, globalUserIds, + serverTimeOffset, loadContactList, setGlobalSearchQuery, }) => { @@ -70,7 +72,8 @@ const NewChatStep1: FC = ({ const displayedIds = useMemo(() => { const contactIds = localContactIds - ? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById) + ? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById, + undefined, undefined, serverTimeOffset) : []; if (!searchQuery) { @@ -95,9 +98,11 @@ const NewChatStep1: FC = ({ chatsById, false, selectedMemberIds, + serverTimeOffset, ); }, [ - localContactIds, searchQuery, localUserIds, globalUserIds, usersById, chatsById, selectedMemberIds, currentUserId, + localContactIds, chatsById, serverTimeOffset, searchQuery, localUserIds, globalUserIds, selectedMemberIds, + currentUserId, usersById, ]); const handleNextStep = useCallback(() => { @@ -152,7 +157,7 @@ export default memo(withGlobal( const { userIds: localContactIds } = global.contactList || {}; const { byId: usersById } = global.users; const { byId: chatsById } = global.chats; - const { currentUserId } = global; + const { currentUserId, serverTimeOffset } = global; const { query: searchQuery, @@ -172,6 +177,7 @@ export default memo(withGlobal( isSearching: fetchingStatus && fetchingStatus.chats, globalUserIds, localUserIds, + serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'setGlobalSearchQuery']), diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 854a39517..65a545c78 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -45,6 +45,7 @@ type StateProps = { usersById: Record; fetchingStatus?: { chats?: boolean; messages?: boolean }; lastSyncTime?: number; + serverTimeOffset?: number; }; type DispatchProps = Pick = ({ localContactIds, localChatIds, localUserIds, globalChatIds, globalUserIds, foundIds, globalMessagesByChatId, chatsById, usersById, fetchingStatus, lastSyncTime, onReset, onSearchDateSelect, openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, + serverTimeOffset, }) => { const lang = useLang(); @@ -120,17 +122,21 @@ const ChatResults: FC = ({ ...foundContactIds, ...(localChatIds || []), ...(localUserIds || []), - ]), chatsById), + ]), chatsById, undefined, undefined, serverTimeOffset), ]; - }, [searchQuery, localContactIds, currentUserId, lang, localChatIds, localUserIds, chatsById, usersById]); + }, [ + searchQuery, localContactIds, currentUserId, lang, localChatIds, localUserIds, chatsById, + serverTimeOffset, usersById, + ]); const globalResults = useMemo(() => { if (!searchQuery || searchQuery.length < MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH || !globalChatIds || !globalUserIds) { return MEMO_EMPTY_ARRAY; } - return sortChatIds(unique([...globalChatIds, ...globalUserIds]), chatsById, true); - }, [chatsById, globalChatIds, globalUserIds, searchQuery]); + return sortChatIds(unique([...globalChatIds, ...globalUserIds]), + chatsById, true, undefined, serverTimeOffset); + }, [chatsById, globalChatIds, globalUserIds, searchQuery, serverTimeOffset]); const foundMessages = useMemo(() => { if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) { @@ -288,7 +294,9 @@ export default memo(withGlobal( }; } - const { currentUserId, messages, lastSyncTime } = global; + const { + currentUserId, messages, lastSyncTime, serverTimeOffset, + } = global; const { fetchingStatus, globalResults, localResults, resultsByType, } = global.globalSearch; @@ -310,6 +318,7 @@ export default memo(withGlobal( usersById, fetchingStatus, lastSyncTime, + serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/main/ForwardPicker.tsx b/src/components/main/ForwardPicker.tsx index dbefadabc..d5b35b164 100644 --- a/src/components/main/ForwardPicker.tsx +++ b/src/components/main/ForwardPicker.tsx @@ -37,6 +37,7 @@ type StateProps = { archivedListIds?: number[]; orderedPinnedIds?: number[]; currentUserId?: number; + serverTimeOffset: number; }; type DispatchProps = Pick; @@ -50,6 +51,7 @@ const ForwardPicker: FC = ({ activeListIds, archivedListIds, currentUserId, + serverTimeOffset, isOpen, setForwardChatId, exitForwardMode, @@ -106,8 +108,8 @@ const ForwardPicker: FC = ({ return searchWords(getChatTitle(lang, chatsById[id], undefined, id === currentUserId), filter); }), - ], chatsById, undefined, currentUserId ? [currentUserId] : undefined); - }, [activeListIds, archivedListIds, chatsById, currentUserId, filter, lang]); + ], chatsById, undefined, currentUserId ? [currentUserId] : undefined, serverTimeOffset); + }, [activeListIds, archivedListIds, chatsById, currentUserId, filter, lang, serverTimeOffset]); const [viewportIds, getMore] = useInfiniteScroll(loadMoreChats, chatIds, Boolean(filter)); @@ -186,6 +188,7 @@ const ForwardPicker: FC = ({ export default memo(withGlobal( (global): StateProps => { const { + serverTimeOffset, chats: { byId: chatsById, listIds, @@ -198,6 +201,7 @@ export default memo(withGlobal( activeListIds: listIds.active, archivedListIds: listIds.archived, currentUserId, + serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['setForwardChatId', 'exitForwardMode', 'loadMoreChats']), diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index f3e5245ec..d829fbea0 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -122,6 +122,7 @@ type StateProps = { shouldSuggestStickers?: boolean; language: LangCode; emojiKeywords?: Record; + serverTimeOffset: number; } & Pick; type DispatchProps = Pick = ({ shouldSuggestStickers, language, emojiKeywords, + serverTimeOffset, recentEmojis, sendMessage, editMessage, @@ -442,7 +444,7 @@ const Composer: FC = ({ if (currentAttachments.length || text) { if (slowMode && !isAdmin) { - const nowSeconds = Math.floor(Date.now() / 1000); + const nowSeconds = Math.floor(Date.now() / 1000) + serverTimeOffset; const secondsSinceLastMessage = lastMessageSendTimeSeconds.current && Math.floor(nowSeconds - lastMessageSendTimeSeconds.current); const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds; @@ -480,15 +482,15 @@ const Composer: FC = ({ forwardMessages(); } - lastMessageSendTimeSeconds.current = Math.floor(Date.now() / 1000); + lastMessageSendTimeSeconds.current = Math.floor(Date.now() / 1000) + serverTimeOffset; clearDraft({ chatId, localOnly: true }); // Wait until message animation starts requestAnimationFrame(resetComposer); }, [ - activeVoiceRecording, attachments, connectionState, chatId, slowMode, isForwarding, isAdmin, - sendMessage, stopRecordingVoice, resetComposer, clearDraft, showError, forwardMessages, + connectionState, attachments, activeVoiceRecording, isForwarding, serverTimeOffset, clearDraft, chatId, + resetComposer, stopRecordingVoice, showError, slowMode, isAdmin, sendMessage, forwardMessages, ]); const handleStickerSelect = useCallback((sticker: ApiSticker) => { @@ -536,11 +538,12 @@ const Composer: FC = ({ } }, [handleSend, openCalendar, shouldSchedule]); - const handleMessageSchedule = useCallback((date: Date) => { + const handleMessageSchedule = useCallback((date: Date, isWhenOnline = false) => { const { isSilent, ...restArgs } = scheduledMessageArgs || {}; // Scheduled time can not be less than 10 seconds in future - const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000); + const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000) + + (isWhenOnline ? 0 : serverTimeOffset); if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) { handleSend(!!isSilent, scheduledAt); @@ -552,10 +555,10 @@ const Composer: FC = ({ requestAnimationFrame(resetComposer); } closeCalendar(); - }, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage]); + }, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage, serverTimeOffset]); const handleMessageScheduleUntilOnline = useCallback(() => { - handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000)); + handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true); }, [handleMessageSchedule]); const handleCloseCalendar = useCallback(() => { @@ -961,6 +964,7 @@ export default memo(withGlobal( recentEmojis: global.recentEmojis, language, emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined, + serverTimeOffset: global.serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/middle/message/Poll.tsx b/src/components/middle/message/Poll.tsx index 4b5ef1971..9b113c6b5 100644 --- a/src/components/middle/message/Poll.tsx +++ b/src/components/middle/message/Poll.tsx @@ -38,6 +38,7 @@ type OwnProps = { type StateProps = { recentVoterIds?: number[]; usersById: Record; + serverTimeOffset: number; }; type DispatchProps = Pick; @@ -54,6 +55,7 @@ const Poll: FC = ({ loadMessage, onSendVote, openPollResults, + serverTimeOffset, }) => { const { id: messageId, chatId } = message; const { summary, results } = poll; @@ -63,7 +65,7 @@ const Poll: FC = ({ const [wasSubmitted, setWasSubmitted] = useState(false); const [closePeriod, setClosePeriod] = useState( !summary.closed && summary.closeDate && summary.closeDate > 0 - ? Math.min(summary.closeDate - Math.floor(Date.now() / 1000), summary.closePeriod!) + ? Math.min(summary.closeDate - Math.floor(Date.now() / 1000) + serverTimeOffset, summary.closePeriod!) : 0, ); // eslint-disable-next-line no-null/no-null @@ -359,7 +361,7 @@ function getReadableVotersCount(lang: LangFn, isQuiz: true | undefined, count?: export default memo(withGlobal( (global, { poll }) => { const { recentVoterIds } = poll.results; - const { byId: usersById } = global.users; + const { serverTimeOffset, users: { byId: usersById } } = global; if (!recentVoterIds || recentVoterIds.length === 0) { return {}; } @@ -367,6 +369,7 @@ export default memo(withGlobal( return { recentVoterIds, usersById, + serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadMessage', 'openPollResults']), diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 93d4046dd..7b5c1ca02 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -75,6 +75,7 @@ type StateProps = { isRightColumnShown: boolean; isRestricted?: boolean; lastSyncTime?: number; + serverTimeOffset: number; }; type DispatchProps = Pick = ({ openUserInfo, focusMessage, loadProfilePhotos, + serverTimeOffset, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); @@ -133,7 +135,7 @@ const Profile: FC = ({ const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members, - usersById, chatMessages, foundIds, chatId, lastSyncTime, + usersById, chatMessages, foundIds, chatId, lastSyncTime, serverTimeOffset, ); const activeKey = tabs.findIndex(({ type }) => type === resultType); @@ -406,6 +408,7 @@ export default memo(withGlobal( isRightColumnShown: selectIsRightColumnShown(global), isRestricted: chat && chat.isRestricted, lastSyncTime: global.lastSyncTime, + serverTimeOffset: global.serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/right/ProfileInfo.tsx b/src/components/right/ProfileInfo.tsx index fbee1ee59..43b3450d5 100644 --- a/src/components/right/ProfileInfo.tsx +++ b/src/components/right/ProfileInfo.tsx @@ -34,6 +34,7 @@ type StateProps = { chat?: ApiChat; isSavedMessages?: boolean; animationLevel: 0 | 1 | 2; + serverTimeOffset: number; } & Pick; type DispatchProps = Pick; @@ -46,6 +47,7 @@ const PrivateChatInfo: FC = ({ animationLevel, loadFullUser, openMediaViewer, + serverTimeOffset, }) => { const { id: userId } = user || {}; const { id: chatId } = chat || {}; @@ -157,7 +159,7 @@ const PrivateChatInfo: FC = ({ if (user) { return (
- {getUserStatus(lang, user)} + {getUserStatus(lang, user, serverTimeOffset)}
); } @@ -219,14 +221,14 @@ const PrivateChatInfo: FC = ({ export default memo(withGlobal( (global, { userId, forceShowSelf }): StateProps => { - const { lastSyncTime } = global; + const { lastSyncTime, serverTimeOffset } = global; const user = selectUser(global, userId); const chat = selectChat(global, userId); const isSavedMessages = !forceShowSelf && user && user.isSelf; const { animationLevel } = global.settings.byKey; return { - lastSyncTime, user, chat, isSavedMessages, animationLevel, + lastSyncTime, user, chat, isSavedMessages, animationLevel, serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index e2df1fc0d..a260e1ca8 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -20,6 +20,7 @@ export default function useProfileViewportIds( foundIds?: number[], chatId?: number, lastSyncTime?: number, + serverTimeOffset = 0, ) { const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType; @@ -28,8 +29,8 @@ export default function useProfileViewportIds( return undefined; } - return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById); - }, [groupChatMembers, usersById]); + return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); + }, [groupChatMembers, serverTimeOffset, usersById]); const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers( resultType, loadMoreMembers, lastSyncTime, memberIds, diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index 87bf55967..630522914 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -21,6 +21,7 @@ type StateProps = { usersById: Record; members?: ApiChatMember[]; isChannel?: boolean; + serverTimeOffset: number; }; type DispatchProps = Pick; @@ -30,14 +31,15 @@ const ManageGroupMembers: FC = ({ usersById, isChannel, openUserInfo, + serverTimeOffset, }) => { const memberIds = useMemo(() => { if (!members || !usersById) { return undefined; } - return sortUserIds(members.map(({ userId }) => userId), usersById); - }, [members, usersById]); + return sortUserIds(members.map(({ userId }) => userId), usersById, undefined, serverTimeOffset); + }, [members, serverTimeOffset, usersById]); const handleMemberClick = useCallback((id: number) => { openUserInfo({ id }); @@ -82,6 +84,7 @@ export default memo(withGlobal( members, usersById, isChannel, + serverTimeOffset: global.serverTimeOffset, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx index 4f297d0e2..3e0c9bd32 100644 --- a/src/components/right/management/ManageGroupUserPermissionsCreate.tsx +++ b/src/components/right/management/ManageGroupUserPermissionsCreate.tsx @@ -23,6 +23,7 @@ type StateProps = { usersById: Record; members?: ApiChatMember[]; isChannel?: boolean; + serverTimeOffset: number; }; const ManageGroupUserPermissionsCreate: FC = ({ @@ -31,14 +32,18 @@ const ManageGroupUserPermissionsCreate: FC = ({ isChannel, onScreenSelect, onChatMemberSelect, + serverTimeOffset, }) => { const memberIds = useMemo(() => { if (!members || !usersById) { return undefined; } - return sortUserIds(members.filter((member) => !member.isOwner).map(({ userId }) => userId), usersById); - }, [members, usersById]); + return sortUserIds( + members.filter((member) => !member.isOwner).map(({ userId }) => userId), + usersById, undefined, serverTimeOffset, + ); + }, [members, serverTimeOffset, usersById]); const handleExceptionMemberClick = useCallback((memberId: number) => { onChatMemberSelect(memberId); @@ -85,6 +90,7 @@ export default memo(withGlobal( members, usersById, isChannel, + serverTimeOffset: global.serverTimeOffset, }; }, )(ManageGroupUserPermissionsCreate)); diff --git a/src/global/initial.ts b/src/global/initial.ts index a41a86f8e..cf2b8efd3 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -8,6 +8,7 @@ export const INITIAL_STATE: GlobalState = { isLeftColumnShown: true, isChatInfoShown: false, uiReadyState: 0, + serverTimeOffset: 0, authRememberMe: true, diff --git a/src/global/types.ts b/src/global/types.ts index 03a15e81a..63da83d38 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -67,6 +67,7 @@ export type GlobalState = { connectionState?: ApiUpdateConnectionStateType; currentUserId?: number; lastSyncTime?: number; + serverTimeOffset: number; // TODO Move to `auth`. isLoggingOut?: boolean; diff --git a/src/lib/gramjs/network/MTProtoSender.js b/src/lib/gramjs/network/MTProtoSender.js index 6252c7d1d..9e944294c 100644 --- a/src/lib/gramjs/network/MTProtoSender.js +++ b/src/lib/gramjs/network/MTProtoSender.js @@ -15,7 +15,7 @@ const { } = require('../tl').constructors; const MessagePacker = require('../extensions/MessagePacker'); const BinaryReader = require('../extensions/BinaryReader'); -const { UpdateConnectionState } = require('./index'); +const { UpdateConnectionState, UpdateServerTimeOffset } = require('./index'); const { BadMessageError } = require('../errors/Common'); const { BadServerSalt, @@ -259,6 +259,10 @@ class MTProtoSender { this._state.time_offset = res.timeOffset; + if (this._updateCallback) { + this._updateCallback(new UpdateServerTimeOffset(this._state.time_offset)); + } + /** * This is *EXTREMELY* important since we don't control * external references to the authorization key, we must @@ -630,6 +634,11 @@ class MTProtoSender { // Sent msg_id too low or too high (respectively). // Use the current msg_id to determine the right time offset. const to = this._state.updateTimeOffset(message.msgId); + + if (this._updateCallback) { + this._updateCallback(new UpdateServerTimeOffset(to)); + } + this._log.info(`System clock is wrong, set time offset to ${to}s`); } else if (badMsg.errorCode === 32) { // msg_seqno too low, so just pump it up by some "large" amount diff --git a/src/lib/gramjs/network/index.js b/src/lib/gramjs/network/index.js index 04f640dee..98f833214 100644 --- a/src/lib/gramjs/network/index.js +++ b/src/lib/gramjs/network/index.js @@ -14,6 +14,12 @@ class UpdateConnectionState { } } +class UpdateServerTimeOffset { + constructor(timeOffset) { + this.timeOffset = timeOffset; + } +} + const { Connection, ConnectionTCPFull, @@ -30,4 +36,5 @@ module.exports = { doAuthentication, MTProtoSender, UpdateConnectionState, + UpdateServerTimeOffset, }; diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 42f3dcef4..1d2ccc765 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -177,23 +177,28 @@ addReducer('loadTopChats', () => { }); addReducer('requestChatUpdate', (global, actions, payload) => { + const { serverTimeOffset } = global; const { chatId } = payload!; const chat = selectChat(global, chatId); if (!chat) { return; } - void callApi('requestChatUpdate', chat); + void callApi('requestChatUpdate', { + chat, + serverTimeOffset, + }); }); addReducer('updateChatMutedState', (global, actions, payload) => { + const { serverTimeOffset } = global; const { chatId, isMuted } = payload!; const chat = selectChat(global, chatId); if (!chat) { return; } - void callApi('updateChatMutedState', { chat, isMuted }); + void callApi('updateChatMutedState', { chat, isMuted, serverTimeOffset }); }); addReducer('createChannel', (global, actions, payload) => { @@ -358,10 +363,11 @@ addReducer('deleteChatFolder', (global, actions, payload) => { addReducer('toggleChatUnread', (global, actions, payload) => { const { id } = payload!; + const { serverTimeOffset } = global; const chat = selectChat(global, id); if (chat) { if (chat.unreadCount) { - void callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID }); + void callApi('markMessageListRead', { serverTimeOffset, chat, threadId: MAIN_THREAD_ID }); } else { void callApi('toggleDialogUnread', { chat, @@ -723,6 +729,7 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: number, off offsetDate, archived: listType === 'archived', withPinned: getGlobal().chats.orderedPinnedIds[listType] === undefined, + serverTimeOffset: getGlobal().serverTimeOffset, }); if (!result) { diff --git a/src/modules/actions/api/messages.ts b/src/modules/actions/api/messages.ts index 2a8b5909e..d4542a1a7 100644 --- a/src/modules/actions/api/messages.ts +++ b/src/modules/actions/api/messages.ts @@ -243,6 +243,7 @@ addReducer('sendMessage', (global, actions, payload) => { }); addReducer('editMessage', (global, actions, payload) => { + const { serverTimeOffset } = global; const { text, entities } = payload!; const currentMessageList = selectCurrentMessageList(global); @@ -258,7 +259,7 @@ addReducer('editMessage', (global, actions, payload) => { } void callApi('editMessage', { - chat, message, text, entities, noWebPage: selectNoWebPage(global, chatId, threadId), + chat, message, text, entities, noWebPage: selectNoWebPage(global, chatId, threadId), serverTimeOffset, }); actions.setEditingId({ messageId: undefined }); @@ -404,6 +405,7 @@ addReducer('deleteHistory', (global, actions, payload) => { }); addReducer('markMessageListRead', (global, actions, payload) => { + const { serverTimeOffset } = global; const currentMessageList = selectCurrentMessageList(global); if (!currentMessageList) { return; @@ -418,7 +420,9 @@ addReducer('markMessageListRead', (global, actions, payload) => { const { maxId } = payload!; runThrottledForMarkRead(() => { - void callApi('markMessageListRead', { chat, threadId, maxId }); + void callApi('markMessageListRead', { + serverTimeOffset, chat, threadId, maxId, + }); }); }); @@ -703,6 +707,7 @@ async function sendMessage(params: { sticker: ApiSticker; gif: ApiVideo; poll: ApiNewPoll; + serverTimeOffset?: number; }) { let localId: number | undefined; const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => { @@ -730,6 +735,7 @@ async function sendMessage(params: { } const global = getGlobal(); + params.serverTimeOffset = global.serverTimeOffset; const currentMessageList = selectCurrentMessageList(global); if (!currentMessageList) { return; @@ -756,6 +762,7 @@ function forwardMessages( fromChat, toChat, messages, + serverTimeOffset: getGlobal().serverTimeOffset, }); setGlobal({ diff --git a/src/modules/actions/api/settings.ts b/src/modules/actions/api/settings.ts index 9b91bf98e..6d30cf445 100644 --- a/src/modules/actions/api/settings.ts +++ b/src/modules/actions/api/settings.ts @@ -308,9 +308,12 @@ addReducer('loadNotificationExceptions', () => { callApi('fetchNotificationExceptions'); }); -addReducer('loadNotificationSettings', () => { +addReducer('loadNotificationSettings', (global) => { + const { serverTimeOffset } = global; (async () => { - const result = await callApi('fetchNotificationSettings'); + const result = await callApi('fetchNotificationSettings', { + serverTimeOffset, + }); if (!result) { return; } diff --git a/src/modules/actions/api/sync.ts b/src/modules/actions/api/sync.ts index 8b79a71a9..f55dc6c66 100644 --- a/src/modules/actions/api/sync.ts +++ b/src/modules/actions/api/sync.ts @@ -85,6 +85,7 @@ async function loadAndReplaceChats() { const result = await callApi('fetchChats', { limit: CHAT_LIST_LOAD_SLICE, withPinned: true, + serverTimeOffset: getGlobal().serverTimeOffset, }); if (!result) { return undefined; @@ -166,7 +167,9 @@ async function loadAndReplaceArchivedChats() { limit: CHAT_LIST_LOAD_SLICE, archived: true, withPinned: true, + serverTimeOffset: getGlobal().serverTimeOffset, }); + if (!result) { return; } diff --git a/src/modules/actions/api/users.ts b/src/modules/actions/api/users.ts index ce7d3a75e..624eb7fe3 100644 --- a/src/modules/actions/api/users.ts +++ b/src/modules/actions/api/users.ts @@ -15,7 +15,7 @@ import { } from '../../reducers'; const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true); -const TOP_PEERS_REQUEST_COOLDOWN = 60000; // 1 min +const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min addReducer('loadFullUser', (global, actions, payload) => { const { userId } = payload!; @@ -49,9 +49,14 @@ addReducer('loadUser', (global, actions, payload) => { }); addReducer('loadTopUsers', (global) => { - const { hash, lastRequestedAt } = global.topPeers; + const { + serverTimeOffset, + topPeers: { + hash, lastRequestedAt, + }, + } = global; - if (!lastRequestedAt || Date.now() - lastRequestedAt > TOP_PEERS_REQUEST_COOLDOWN) { + if (!lastRequestedAt || Date.now() / 1000 + serverTimeOffset - lastRequestedAt > TOP_PEERS_REQUEST_COOLDOWN) { void loadTopUsers(hash); } }); @@ -95,7 +100,7 @@ async function loadTopUsers(usersHash?: number) { ...global.topPeers, hash, userIds: ids, - lastRequestedAt: Date.now(), + lastRequestedAt: Date.now() / 1000 + global.serverTimeOffset, }, }; setGlobal(global); diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index f705b1431..b7279a880 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -10,7 +10,7 @@ import { ApiUpdateAuthorizationError, ApiUpdateConnectionState, ApiUpdateSession, - ApiUpdateCurrentUser, + ApiUpdateCurrentUser, ApiUpdateServerTimeOffset, } from '../../../api/types'; import { DEBUG, SESSION_USER_KEY } from '../../../config'; import { subscribe } from '../../../util/notifications'; @@ -46,6 +46,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { onUpdateSession(update); break; + case 'updateServerTimeOffset': + onUpdateServerTimeOffset(update); + break; + case 'updateCurrentUser': onUpdateCurrentUser(update); break; @@ -155,6 +159,13 @@ function onUpdateSession(update: ApiUpdateSession) { getDispatch().saveSession({ sessionData }); } +function onUpdateServerTimeOffset(update: ApiUpdateServerTimeOffset) { + setGlobal({ + ...getGlobal(), + serverTimeOffset: update.serverTimeOffset, + }); +} + function onUpdateCurrentUser(update: ApiUpdateCurrentUser) { const { currentUser } = update; diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index b119ea00e..a67b7b509 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -469,6 +469,7 @@ export function sortChatIds( chatsById: Record, shouldPrioritizeVerified = false, priorityIds?: number[], + serverTimeOffset = 0, ) { return orderBy(chatIds, (id) => { const chat = chatsById[id]; @@ -490,7 +491,7 @@ export function sortChatIds( // Assuming that last message date can't be less than now, // this should place prioritized on top of the list. // Then we subtract index of `id` in `priorityIds` to preserve selected order - priority += Date.now() + (priorityIds.length - priorityIds.indexOf(id)); + priority += Date.now() + serverTimeOffset * 1000 + (priorityIds.length - priorityIds.indexOf(id)); } return priority; diff --git a/src/modules/helpers/users.ts b/src/modules/helpers/users.ts index 10595b367..e37443e89 100644 --- a/src/modules/helpers/users.ts +++ b/src/modules/helpers/users.ts @@ -64,7 +64,7 @@ export function getUserFullName(user?: ApiUser) { return undefined; } -export function getUserStatus(lang: LangFn, user: ApiUser) { +export function getUserStatus(lang: LangFn, user: ApiUser, serverTimeOffset: number) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) { return lang('ServiceNotifications').toLowerCase(); } @@ -95,7 +95,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser) { if (!wasOnline) return lang('LastSeen.Offline'); - const now = new Date(); + const now = new Date(new Date().getTime() + serverTimeOffset * 1000); const wasOnlineDate = new Date(wasOnline * 1000); if (wasOnlineDate >= now) { @@ -118,7 +118,8 @@ export function getUserStatus(lang: LangFn, user: ApiUser) { // today const today = new Date(); today.setHours(0, 0, 0, 0); - if (wasOnlineDate > today) { + const serverToday = new Date(today.getTime() + serverTimeOffset * 1000); + if (wasOnlineDate > serverToday) { // up to 6 hours ago if (diff.getTime() / 1000 < 6 * 60 * 60) { const hours = Math.floor(diff.getTime() / 1000 / 60 / 60); @@ -132,8 +133,9 @@ export function getUserStatus(lang: LangFn, user: ApiUser) { // yesterday const yesterday = new Date(); yesterday.setDate(now.getDate() - 1); - today.setHours(0, 0, 0, 0); - if (wasOnlineDate > yesterday) { + yesterday.setHours(0, 0, 0, 0); + const serverYesterday = new Date(yesterday.getTime() + serverTimeOffset * 1000); + if (wasOnlineDate > serverYesterday) { return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate)); } @@ -184,9 +186,10 @@ export function sortUserIds( userIds: number[], usersById: Record, priorityIds?: number[], + serverTimeOffset = 0, ) { return orderBy(userIds, (id) => { - const now = Date.now() / 1000; + const now = Date.now() / 1000 + serverTimeOffset; if (priorityIds && priorityIds.includes(id)) { // Assuming that online status expiration date can't be as far as two days from now, diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index 0950eaeb1..0b7884584 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -335,6 +335,7 @@ export function selectForwardedSender(global: GlobalState, message: ApiMessage): } export function selectAllowedMessageActions(global: GlobalState, message: ApiMessage, threadId: number) { + const { serverTimeOffset } = global; const chat = selectChat(global, message.chatId); if (!chat || chat.isRestricted) { return {}; @@ -351,7 +352,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes const isAction = isActionMessage(message); const { content } = message; const isMessageEditable = ( - (isChatWithSelf || Date.now() - message.date * 1000 < MESSAGE_EDIT_ALLOWED_TIME_MS) + (isChatWithSelf || Date.now() + serverTimeOffset * 1000 - message.date * 1000 < MESSAGE_EDIT_ALLOWED_TIME_MS) && !( content.sticker || content.contact || content.poll || content.action || content.audio || (content.video && content.video.isRound) diff --git a/src/util/notifications.ts b/src/util/notifications.ts index 6ed3dcfa6..afa427a03 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -133,10 +133,13 @@ let areSettingsLoaded = false; async function loadNotificationSettings() { if (areSettingsLoaded) return; const [result] = await Promise.all([ - callApi('fetchNotificationSettings'), + callApi('fetchNotificationSettings', { + serverTimeOffset: getGlobal().serverTimeOffset, + }), callApi('fetchNotificationExceptions'), ]); if (!result) return; + setGlobal(replaceSettings(getGlobal(), result)); areSettingsLoaded = true; }