Support server time offset (#1186)

This commit is contained in:
Alexander Zinchuk 2021-06-19 11:20:55 +03:00
parent e39d723b70
commit dcd3803263
34 changed files with 235 additions and 87 deletions

View File

@ -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,
};
}

View File

@ -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',

View File

@ -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({

View File

@ -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,
});
}
}

View File

@ -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<ApiMessage> = {
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({

View File

@ -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,
};

View File

@ -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) {

View File

@ -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;

View File

@ -36,6 +36,7 @@ type StateProps = {
user?: ApiUser;
isSavedMessages?: boolean;
areMessagesLoaded: boolean;
serverTimeOffset: number;
} & Pick<GlobalState, 'lastSyncTime'>;
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
@ -56,6 +57,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
lastSyncTime,
loadFullUser,
openMediaViewer,
serverTimeOffset,
}) => {
const { id: userId } = user || {};
const fullName = getUserFullName(user);
@ -106,7 +108,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}>
{withUsername && user.username && <span className="handle">{user.username}</span>}
<span className="user-status" dir="auto">{getUserStatus(lang, user)}</span>
<span className="user-status" dir="auto">{getUserStatus(lang, user, serverTimeOffset)}</span>
</div>
);
}
@ -139,13 +141,13 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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']),

View File

@ -25,6 +25,7 @@ export type OwnProps = {
type StateProps = {
usersById: Record<number, ApiUser>;
contactIds?: number[];
serverTimeOffset: number;
};
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
@ -32,7 +33,7 @@ type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
const runThrottled = throttle((cb) => cb(), 60000, true);
const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
return {
usersById,
contactIds,
serverTimeOffset: global.serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'openChat']),

View File

@ -33,6 +33,7 @@ type StateProps = {
isSearching?: boolean;
localUserIds?: number[];
globalUserIds?: number[];
serverTimeOffset?: number;
};
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'setGlobalSearchQuery'>;
@ -53,6 +54,7 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
isSearching,
localUserIds,
globalUserIds,
serverTimeOffset,
loadContactList,
setGlobalSearchQuery,
}) => {
@ -70,7 +72,8 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
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<OwnProps>(
isSearching: fetchingStatus && fetchingStatus.chats,
globalUserIds,
localUserIds,
serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'setGlobalSearchQuery']),

View File

@ -45,6 +45,7 @@ type StateProps = {
usersById: Record<number, ApiUser>;
fetchingStatus?: { chats?: boolean; messages?: boolean };
lastSyncTime?: number;
serverTimeOffset?: number;
};
type DispatchProps = Pick<GlobalActions, (
@ -61,6 +62,7 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
...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<OwnProps>(
};
}
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<OwnProps>(
usersById,
fetchingStatus,
lastSyncTime,
serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -37,6 +37,7 @@ type StateProps = {
archivedListIds?: number[];
orderedPinnedIds?: number[];
currentUserId?: number;
serverTimeOffset: number;
};
type DispatchProps = Pick<GlobalActions, 'setForwardChatId' | 'exitForwardMode' | 'loadMoreChats'>;
@ -50,6 +51,7 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
activeListIds,
archivedListIds,
currentUserId,
serverTimeOffset,
isOpen,
setForwardChatId,
exitForwardMode,
@ -106,8 +108,8 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
serverTimeOffset,
chats: {
byId: chatsById,
listIds,
@ -198,6 +201,7 @@ export default memo(withGlobal<OwnProps>(
activeListIds: listIds.active,
archivedListIds: listIds.archived,
currentUserId,
serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['setForwardChatId', 'exitForwardMode', 'loadMoreChats']),

View File

@ -122,6 +122,7 @@ type StateProps = {
shouldSuggestStickers?: boolean;
language: LangCode;
emojiKeywords?: Record<string, string[]>;
serverTimeOffset: number;
} & Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, (
@ -180,6 +181,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
shouldSuggestStickers,
language,
emojiKeywords,
serverTimeOffset,
recentEmojis,
sendMessage,
editMessage,
@ -442,7 +444,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
}
}, [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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
recentEmojis: global.recentEmojis,
language,
emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined,
serverTimeOffset: global.serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -38,6 +38,7 @@ type OwnProps = {
type StateProps = {
recentVoterIds?: number[];
usersById: Record<number, ApiUser>;
serverTimeOffset: number;
};
type DispatchProps = Pick<GlobalActions, ('loadMessage' | 'openPollResults')>;
@ -54,6 +55,7 @@ const Poll: FC<OwnProps & StateProps & DispatchProps> = ({
loadMessage,
onSendVote,
openPollResults,
serverTimeOffset,
}) => {
const { id: messageId, chatId } = message;
const { summary, results } = poll;
@ -63,7 +65,7 @@ const Poll: FC<OwnProps & StateProps & DispatchProps> = ({
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
const [closePeriod, setClosePeriod] = useState<number>(
!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<OwnProps>(
(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<OwnProps>(
return {
recentVoterIds,
usersById,
serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadMessage', 'openPollResults']),

View File

@ -75,6 +75,7 @@ type StateProps = {
isRightColumnShown: boolean;
isRestricted?: boolean;
lastSyncTime?: number;
serverTimeOffset: number;
};
type DispatchProps = Pick<GlobalActions, (
@ -115,6 +116,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
openUserInfo,
focusMessage,
loadProfilePhotos,
serverTimeOffset,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
@ -133,7 +135,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
isRightColumnShown: selectIsRightColumnShown(global),
isRestricted: chat && chat.isRestricted,
lastSyncTime: global.lastSyncTime,
serverTimeOffset: global.serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -34,6 +34,7 @@ type StateProps = {
chat?: ApiChat;
isSavedMessages?: boolean;
animationLevel: 0 | 1 | 2;
serverTimeOffset: number;
} & Pick<GlobalState, 'lastSyncTime'>;
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
@ -46,6 +47,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
animationLevel,
loadFullUser,
openMediaViewer,
serverTimeOffset,
}) => {
const { id: userId } = user || {};
const { id: chatId } = chat || {};
@ -157,7 +159,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
if (user) {
return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}>
<span className="user-status" dir="auto">{getUserStatus(lang, user)}</span>
<span className="user-status" dir="auto">{getUserStatus(lang, user, serverTimeOffset)}</span>
</div>
);
}
@ -219,14 +221,14 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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']),

View File

@ -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,

View File

@ -21,6 +21,7 @@ type StateProps = {
usersById: Record<number, ApiUser>;
members?: ApiChatMember[];
isChannel?: boolean;
serverTimeOffset: number;
};
type DispatchProps = Pick<GlobalActions, 'openUserInfo'>;
@ -30,14 +31,15 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
members,
usersById,
isChannel,
serverTimeOffset: global.serverTimeOffset,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -23,6 +23,7 @@ type StateProps = {
usersById: Record<number, ApiUser>;
members?: ApiChatMember[];
isChannel?: boolean;
serverTimeOffset: number;
};
const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
@ -31,14 +32,18 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
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<OwnProps>(
members,
usersById,
isChannel,
serverTimeOffset: global.serverTimeOffset,
};
},
)(ManageGroupUserPermissionsCreate));

View File

@ -8,6 +8,7 @@ export const INITIAL_STATE: GlobalState = {
isLeftColumnShown: true,
isChatInfoShown: false,
uiReadyState: 0,
serverTimeOffset: 0,
authRememberMe: true,

View File

@ -67,6 +67,7 @@ export type GlobalState = {
connectionState?: ApiUpdateConnectionStateType;
currentUserId?: number;
lastSyncTime?: number;
serverTimeOffset: number;
// TODO Move to `auth`.
isLoggingOut?: boolean;

View File

@ -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

View File

@ -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,
};

View File

@ -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) {

View File

@ -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({

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -469,6 +469,7 @@ export function sortChatIds(
chatsById: Record<number, ApiChat>,
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;

View File

@ -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<number, ApiUser>,
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,

View File

@ -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)

View File

@ -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;
}