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( export function buildApiChatFromDialog(
dialog: GramJs.Dialog, dialog: GramJs.Dialog,
peerEntity: GramJs.TypeUser | GramJs.TypeChat, peerEntity: GramJs.TypeUser | GramJs.TypeChat,
serverTimeOffset: number,
): ApiChat { ): ApiChat {
const { const {
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, notifySettings: { silent, muteUntil }, peer, folderId, unreadMark, unreadCount, unreadMentionsCount, notifySettings: { silent, muteUntil },
readOutboxMaxId, readInboxMaxId, readOutboxMaxId, readInboxMaxId,
} = dialog; } = dialog;
const isMuted = silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000); const isMuted = silent || (typeof muteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < muteUntil * 1000);
return { return {
id: getApiChatIdFromMtpPeer(peer), id: getApiChatIdFromMtpPeer(peer),
@ -314,6 +315,7 @@ export function buildChatMembers(
export function buildChatTypingStatus( export function buildChatTypingStatus(
update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping, update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping,
serverTimeOffset: number,
) { ) {
let action: string = ''; let action: string = '';
if (update.action instanceof GramJs.SendMessageCancelAction) { if (update.action instanceof GramJs.SendMessageCancelAction) {
@ -347,7 +349,7 @@ export function buildChatTypingStatus(
return { return {
action, action,
...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }), ...(!(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 { return {
id: localId, id: localId,
chatId: SERVICE_NOTIFICATIONS_USER_ID, chatId: SERVICE_NOTIFICATIONS_USER_ID,
date: notification.inboxDate || (currentDate / 1000), date: notification.inboxDate || currentDate,
content, content,
isOutgoing: false, isOutgoing: false,
}; };
@ -715,6 +715,7 @@ export function buildLocalMessage(
poll?: ApiNewPoll, poll?: ApiNewPoll,
groupedId?: string, groupedId?: string,
scheduledAt?: number, scheduledAt?: number,
serverTimeOffset = 0,
): ApiMessage { ): ApiMessage {
const localId = localMessageCounter++; const localId = localMessageCounter++;
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
@ -735,7 +736,7 @@ export function buildLocalMessage(
...(gif && { video: gif }), ...(gif && { video: gif }),
...(poll && buildNewPoll(poll, localId)), ...(poll && buildNewPoll(poll, localId)),
}, },
date: scheduledAt || Math.round(Date.now() / 1000), date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
isOutgoing: !isChannel, isOutgoing: !isChannel,
senderId: currentUserId, senderId: currentUserId,
...(replyingTo && { replyToMessageId: replyingTo }), ...(replyingTo && { replyToMessageId: replyingTo }),
@ -750,6 +751,7 @@ export function buildLocalMessage(
export function buildForwardedMessage( export function buildForwardedMessage(
toChat: ApiChat, toChat: ApiChat,
message: ApiMessage, message: ApiMessage,
serverTimeOffset: number,
): ApiMessage { ): ApiMessage {
const localId = localMessageCounter++; const localId = localMessageCounter++;
const { const {
@ -770,7 +772,7 @@ export function buildForwardedMessage(
id: localId, id: localId,
chatId: toChat.id, chatId: toChat.id,
content, content,
date: Math.round(Date.now() / 1000), date: Math.round(Date.now() / 1000) + serverTimeOffset,
isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel', isOutgoing: !asIncomingInChatWithSelf && toChat.type !== 'chatTypeChannel',
senderId: currentUserId, senderId: currentUserId,
sendingState: 'messageSendingStatePending', sendingState: 'messageSendingStatePending',

View File

@ -50,11 +50,13 @@ export async function fetchChats({
offsetDate, offsetDate,
archived, archived,
withPinned, withPinned,
serverTimeOffset,
}: { }: {
limit: number; limit: number;
offsetDate?: number; offsetDate?: number;
archived?: boolean; archived?: boolean;
withPinned?: boolean; withPinned?: boolean;
serverTimeOffset: number;
}) { }) {
const result = await invokeRequest(new GramJs.messages.GetDialogs({ const result = await invokeRequest(new GramJs.messages.GetDialogs({
offsetPeer: new GramJs.InputPeerEmpty(), offsetPeer: new GramJs.InputPeerEmpty(),
@ -110,7 +112,7 @@ export async function fetchChats({
} }
const peerEntity = peersByKey[getPeerKey(dialog.peer)]; const peerEntity = peersByKey[getPeerKey(dialog.peer)];
const chat = buildApiChatFromDialog(dialog, peerEntity); const chat = buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset);
chat.lastMessage = lastMessagesByChatId[chat.id]; chat.lastMessage = lastMessagesByChatId[chat.id];
chats.push(chat); chats.push(chat);
@ -228,7 +230,12 @@ export async function fetchChat({
return { chatId: chat.id }; 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 { id, accessHash } = chat;
const result = await invokeRequest(new GramJs.messages.GetPeerDialogs({ const result = await invokeRequest(new GramJs.messages.GetPeerDialogs({
@ -260,7 +267,7 @@ export async function requestChatUpdate(chat: ApiChat) {
'@type': 'updateChat', '@type': 'updateChat',
id, id,
chat: { chat: {
...buildApiChatFromDialog(dialog, peerEntity), ...buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset),
lastMessage, lastMessage,
}, },
}); });
@ -396,9 +403,9 @@ async function getFullChannelInfo(
} }
export async function updateChatMutedState({ 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({ await invokeRequest(new GramJs.account.UpdateNotifySettings({
peer: new GramJs.InputNotifyPeer({ peer: new GramJs.InputNotifyPeer({
@ -407,7 +414,10 @@ export async function updateChatMutedState({
settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : undefined }), settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : undefined }),
})); }));
void requestChatUpdate(chat); void requestChatUpdate({
chat,
serverTimeOffset,
});
} }
export async function createChannel({ export async function createChannel({

View File

@ -117,6 +117,11 @@ function handleGramJsUpdate(update: any) {
isConnected = update.state === connection.UpdateConnectionState.connected; isConnected = update.state === connection.UpdateConnectionState.connected;
} else if (update instanceof GramJs.UpdatesTooLong) { } else if (update instanceof GramJs.UpdatesTooLong) {
void handleTerminatedSession(); void handleTerminatedSession();
} else if (update instanceof connection.UpdateServerTimeOffset) {
onUpdate({
'@type': 'updateServerTimeOffset',
serverTimeOffset: update.timeOffset,
});
} }
} }

View File

@ -168,6 +168,7 @@ export function sendMessage(
scheduledAt, scheduledAt,
groupedId, groupedId,
noWebPage, noWebPage,
serverTimeOffset,
}: { }: {
chat: ApiChat; chat: ApiChat;
text?: string; text?: string;
@ -181,11 +182,12 @@ export function sendMessage(
scheduledAt?: number; scheduledAt?: number;
groupedId?: string; groupedId?: string;
noWebPage?: boolean; noWebPage?: boolean;
serverTimeOffset?: number;
}, },
onProgress?: ApiOnProgress, onProgress?: ApiOnProgress,
) { ) {
const localMessage = buildLocalMessage( 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({ onUpdate({
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage', '@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
@ -405,14 +407,16 @@ export async function editMessage({
text, text,
entities, entities,
noWebPage, noWebPage,
serverTimeOffset,
}: { }: {
chat: ApiChat; chat: ApiChat;
message: ApiMessage; message: ApiMessage;
text: string; text: string;
entities?: ApiMessageEntity[]; entities?: ApiMessageEntity[];
noWebPage?: boolean; noWebPage?: boolean;
serverTimeOffset: number;
}) { }) {
const isScheduled = message.date * 1000 > Date.now(); const isScheduled = message.date * 1000 > Date.now() + serverTimeOffset * 1000;
const messageUpdate: Partial<ApiMessage> = { const messageUpdate: Partial<ApiMessage> = {
content: { content: {
...message.content, ...message.content,
@ -613,9 +617,9 @@ export async function deleteHistory({
} }
export async function markMessageListRead({ 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'; const isChannel = getEntityTypeById(chat.id) === 'channel';
@ -638,7 +642,7 @@ export async function markMessageListRead({
} }
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
void requestChatUpdate(chat); void requestChatUpdate({ chat, serverTimeOffset });
} else { } else {
void requestThreadInfoUpdate({ chat, threadId }); void requestThreadInfoUpdate({ chat, threadId });
} }
@ -968,16 +972,18 @@ export async function forwardMessages({
fromChat, fromChat,
toChat, toChat,
messages, messages,
serverTimeOffset,
}: { }: {
fromChat: ApiChat; fromChat: ApiChat;
toChat: ApiChat; toChat: ApiChat;
messages: ApiMessage[]; messages: ApiMessage[];
serverTimeOffset: number;
}) { }) {
const messageIds = messages.map(({ id }) => id); const messageIds = messages.map(({ id }) => id);
const randomIds = messages.map(generateRandomBigInt); const randomIds = messages.map(generateRandomBigInt);
messages.forEach((message, index) => { messages.forEach((message, index) => {
const localMessage = buildForwardedMessage(toChat, message); const localMessage = buildForwardedMessage(toChat, message, serverTimeOffset);
localDb.localMessages[String(randomIds[index])] = localMessage; localDb.localMessages[String(randomIds[index])] = localMessage;
onUpdate({ onUpdate({

View File

@ -164,7 +164,9 @@ export async function fetchNotificationExceptions() {
} }
} }
export async function fetchNotificationSettings() { export async function fetchNotificationSettings({
serverTimeOffset,
}: { serverTimeOffset: number }) {
const [ const [
isMutedContactSignUpNotification, isMutedContactSignUpNotification,
privateContactNotificationsSettings, privateContactNotificationsSettings,
@ -200,15 +202,18 @@ export async function fetchNotificationSettings() {
return { return {
hasContactJoinedNotifications: !isMutedContactSignUpNotification, hasContactJoinedNotifications: !isMutedContactSignUpNotification,
hasPrivateChatsNotifications: !( hasPrivateChatsNotifications: !(
privateSilent || (typeof privateMuteUntil === 'number' && Date.now() < privateMuteUntil * 1000) privateSilent
|| (typeof privateMuteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < privateMuteUntil * 1000)
), ),
hasPrivateChatsMessagePreview: privateShowPreviews, hasPrivateChatsMessagePreview: privateShowPreviews,
hasGroupNotifications: !( hasGroupNotifications: !(
groupSilent || (typeof groupMuteUntil === 'number' && Date.now() < groupMuteUntil * 1000) groupSilent || (typeof groupMuteUntil === 'number'
&& Date.now() + serverTimeOffset * 1000 < groupMuteUntil * 1000)
), ),
hasGroupMessagePreview: groupShowPreviews, hasGroupMessagePreview: groupShowPreviews,
hasBroadcastNotifications: !( hasBroadcastNotifications: !(
broadcastSilent || (typeof broadcastMuteUntil === 'number' && Date.now() < broadcastMuteUntil * 1000) broadcastSilent || (typeof broadcastMuteUntil === 'number'
&& Date.now() + serverTimeOffset * 1000 < broadcastMuteUntil * 1000)
), ),
hasBroadcastMessagePreview: broadcastShowPreviews, hasBroadcastMessagePreview: broadcastShowPreviews,
}; };

View File

@ -49,9 +49,12 @@ export function init(_onUpdate: OnApiUpdate) {
} }
const sentMessageIds = new Set(); const sentMessageIds = new Set();
let serverTimeOffset = 0;
export function updater(update: Update, originRequest?: GramJs.AnyRequest) { 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; let connectionState: ApiUpdateConnectionStateType;
switch (update.state) { switch (update.state) {
@ -88,7 +91,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
} else if (update instanceof GramJs.UpdateShortMessage) { } else if (update instanceof GramJs.UpdateShortMessage) {
message = buildApiMessageFromShort(update); message = buildApiMessageFromShort(update);
} else if (update instanceof GramJs.UpdateServiceNotification) { } else if (update instanceof GramJs.UpdateServiceNotification) {
const currentDate = Date.now(); const currentDate = Date.now() / 1000 + serverTimeOffset;
message = buildApiMessageFromNotification(update, currentDate); message = buildApiMessageFromNotification(update, currentDate);
if (isMessageWithMedia(update)) { if (isMessageWithMedia(update)) {
@ -347,7 +350,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
sentMessageIds.add(update.id); sentMessageIds.add(update.id);
// Edge case for "Send When Online" // 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({ onUpdate({
'@type': localMessage.isScheduled && !isAlreadySent '@type': localMessage.isScheduled && !isAlreadySent
@ -549,7 +552,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
silent, muteUntil, showPreviews, sound, silent, muteUntil, showPreviews, sound,
} = update.notifySettings; } = 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({ onUpdate({
'@type': 'updateNotifyExceptions', '@type': 'updateNotifyExceptions',
@ -569,7 +573,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
onUpdate({ onUpdate({
'@type': 'updateChatTypingStatus', '@type': 'updateChatTypingStatus',
id, id,
typingStatus: buildChatTypingStatus(update), typingStatus: buildChatTypingStatus(update, serverTimeOffset),
}); });
} else if (update instanceof GramJs.UpdateChannelUserTyping) { } else if (update instanceof GramJs.UpdateChannelUserTyping) {
const id = getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel); const id = getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel);
@ -577,7 +581,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
onUpdate({ onUpdate({
'@type': 'updateChatTypingStatus', '@type': 'updateChatTypingStatus',
id, id,
typingStatus: buildChatTypingStatus(update), typingStatus: buildChatTypingStatus(update, serverTimeOffset),
}); });
} else if (update instanceof GramJs.UpdateChannel) { } else if (update instanceof GramJs.UpdateChannel) {
const { _entities } = update; const { _entities } = update;
@ -745,7 +749,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
onUpdate({ onUpdate({
'@type': 'updateNotifySettings', '@type': 'updateNotifySettings',
peerType, 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), shouldShowPreviews: Boolean(showPreviews),
}); });
} else if (update instanceof GramJs.UpdatePeerBlocked) { } 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 = ( export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateReady | ApiUpdateSession |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
@ -389,7 +394,8 @@ export type ApiUpdate = (
ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages |
ApiUpdateTwoFaError | updateTwoFaStateWaitCode | ApiUpdateTwoFaError | updateTwoFaStateWaitCode |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

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

View File

@ -25,6 +25,7 @@ export type OwnProps = {
type StateProps = { type StateProps = {
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
contactIds?: number[]; contactIds?: number[];
serverTimeOffset: number;
}; };
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>; type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
@ -32,7 +33,7 @@ type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'openChat'>;
const runThrottled = throttle((cb) => cb(), 60000, true); const runThrottled = throttle((cb) => cb(), 60000, true);
const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({ 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, // Due to the parent Transition, this component never gets unmounted,
// that's why we use throttled API call on every update. // 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); return fullName && searchWords(fullName, filter);
}) : contactIds; }) : contactIds;
return sortUserIds(resultIds, usersById); return sortUserIds(resultIds, usersById, undefined, serverTimeOffset);
}, [filter, usersById, contactIds]); }, [contactIds, filter, usersById, serverTimeOffset]);
const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter)); const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter));
@ -100,6 +101,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
usersById, usersById,
contactIds, contactIds,
serverTimeOffset: global.serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'openChat']), (setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'openChat']),

View File

@ -33,6 +33,7 @@ type StateProps = {
isSearching?: boolean; isSearching?: boolean;
localUserIds?: number[]; localUserIds?: number[];
globalUserIds?: number[]; globalUserIds?: number[];
serverTimeOffset?: number;
}; };
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'setGlobalSearchQuery'>; type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'setGlobalSearchQuery'>;
@ -53,6 +54,7 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
isSearching, isSearching,
localUserIds, localUserIds,
globalUserIds, globalUserIds,
serverTimeOffset,
loadContactList, loadContactList,
setGlobalSearchQuery, setGlobalSearchQuery,
}) => { }) => {
@ -70,7 +72,8 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
const contactIds = localContactIds const contactIds = localContactIds
? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById) ? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById,
undefined, undefined, serverTimeOffset)
: []; : [];
if (!searchQuery) { if (!searchQuery) {
@ -95,9 +98,11 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
chatsById, chatsById,
false, false,
selectedMemberIds, selectedMemberIds,
serverTimeOffset,
); );
}, [ }, [
localContactIds, searchQuery, localUserIds, globalUserIds, usersById, chatsById, selectedMemberIds, currentUserId, localContactIds, chatsById, serverTimeOffset, searchQuery, localUserIds, globalUserIds, selectedMemberIds,
currentUserId, usersById,
]); ]);
const handleNextStep = useCallback(() => { const handleNextStep = useCallback(() => {
@ -152,7 +157,7 @@ export default memo(withGlobal<OwnProps>(
const { userIds: localContactIds } = global.contactList || {}; const { userIds: localContactIds } = global.contactList || {};
const { byId: usersById } = global.users; const { byId: usersById } = global.users;
const { byId: chatsById } = global.chats; const { byId: chatsById } = global.chats;
const { currentUserId } = global; const { currentUserId, serverTimeOffset } = global;
const { const {
query: searchQuery, query: searchQuery,
@ -172,6 +177,7 @@ export default memo(withGlobal<OwnProps>(
isSearching: fetchingStatus && fetchingStatus.chats, isSearching: fetchingStatus && fetchingStatus.chats,
globalUserIds, globalUserIds,
localUserIds, localUserIds,
serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'setGlobalSearchQuery']), (setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'setGlobalSearchQuery']),

View File

@ -45,6 +45,7 @@ type StateProps = {
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
fetchingStatus?: { chats?: boolean; messages?: boolean }; fetchingStatus?: { chats?: boolean; messages?: boolean };
lastSyncTime?: number; lastSyncTime?: number;
serverTimeOffset?: number;
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
@ -61,6 +62,7 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
localContactIds, localChatIds, localUserIds, globalChatIds, globalUserIds, localContactIds, localChatIds, localUserIds, globalChatIds, globalUserIds,
foundIds, globalMessagesByChatId, chatsById, usersById, fetchingStatus, lastSyncTime, foundIds, globalMessagesByChatId, chatsById, usersById, fetchingStatus, lastSyncTime,
onReset, onSearchDateSelect, openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, onReset, onSearchDateSelect, openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId,
serverTimeOffset,
}) => { }) => {
const lang = useLang(); const lang = useLang();
@ -120,17 +122,21 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
...foundContactIds, ...foundContactIds,
...(localChatIds || []), ...(localChatIds || []),
...(localUserIds || []), ...(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(() => { const globalResults = useMemo(() => {
if (!searchQuery || searchQuery.length < MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH || !globalChatIds || !globalUserIds) { if (!searchQuery || searchQuery.length < MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH || !globalChatIds || !globalUserIds) {
return MEMO_EMPTY_ARRAY; return MEMO_EMPTY_ARRAY;
} }
return sortChatIds(unique([...globalChatIds, ...globalUserIds]), chatsById, true); return sortChatIds(unique([...globalChatIds, ...globalUserIds]),
}, [chatsById, globalChatIds, globalUserIds, searchQuery]); chatsById, true, undefined, serverTimeOffset);
}, [chatsById, globalChatIds, globalUserIds, searchQuery, serverTimeOffset]);
const foundMessages = useMemo(() => { const foundMessages = useMemo(() => {
if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) { 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 { const {
fetchingStatus, globalResults, localResults, resultsByType, fetchingStatus, globalResults, localResults, resultsByType,
} = global.globalSearch; } = global.globalSearch;
@ -310,6 +318,7 @@ export default memo(withGlobal<OwnProps>(
usersById, usersById,
fetchingStatus, fetchingStatus,
lastSyncTime, lastSyncTime,
serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -37,6 +37,7 @@ type StateProps = {
archivedListIds?: number[]; archivedListIds?: number[];
orderedPinnedIds?: number[]; orderedPinnedIds?: number[];
currentUserId?: number; currentUserId?: number;
serverTimeOffset: number;
}; };
type DispatchProps = Pick<GlobalActions, 'setForwardChatId' | 'exitForwardMode' | 'loadMoreChats'>; type DispatchProps = Pick<GlobalActions, 'setForwardChatId' | 'exitForwardMode' | 'loadMoreChats'>;
@ -50,6 +51,7 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
activeListIds, activeListIds,
archivedListIds, archivedListIds,
currentUserId, currentUserId,
serverTimeOffset,
isOpen, isOpen,
setForwardChatId, setForwardChatId,
exitForwardMode, exitForwardMode,
@ -106,8 +108,8 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
return searchWords(getChatTitle(lang, chatsById[id], undefined, id === currentUserId), filter); return searchWords(getChatTitle(lang, chatsById[id], undefined, id === currentUserId), filter);
}), }),
], chatsById, undefined, currentUserId ? [currentUserId] : undefined); ], chatsById, undefined, currentUserId ? [currentUserId] : undefined, serverTimeOffset);
}, [activeListIds, archivedListIds, chatsById, currentUserId, filter, lang]); }, [activeListIds, archivedListIds, chatsById, currentUserId, filter, lang, serverTimeOffset]);
const [viewportIds, getMore] = useInfiniteScroll(loadMoreChats, chatIds, Boolean(filter)); const [viewportIds, getMore] = useInfiniteScroll(loadMoreChats, chatIds, Boolean(filter));
@ -186,6 +188,7 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const { const {
serverTimeOffset,
chats: { chats: {
byId: chatsById, byId: chatsById,
listIds, listIds,
@ -198,6 +201,7 @@ export default memo(withGlobal<OwnProps>(
activeListIds: listIds.active, activeListIds: listIds.active,
archivedListIds: listIds.archived, archivedListIds: listIds.archived,
currentUserId, currentUserId,
serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['setForwardChatId', 'exitForwardMode', 'loadMoreChats']), (setGlobal, actions): DispatchProps => pick(actions, ['setForwardChatId', 'exitForwardMode', 'loadMoreChats']),

View File

@ -122,6 +122,7 @@ type StateProps = {
shouldSuggestStickers?: boolean; shouldSuggestStickers?: boolean;
language: LangCode; language: LangCode;
emojiKeywords?: Record<string, string[]>; emojiKeywords?: Record<string, string[]>;
serverTimeOffset: number;
} & Pick<GlobalState, 'connectionState'>; } & Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
@ -180,6 +181,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
shouldSuggestStickers, shouldSuggestStickers,
language, language,
emojiKeywords, emojiKeywords,
serverTimeOffset,
recentEmojis, recentEmojis,
sendMessage, sendMessage,
editMessage, editMessage,
@ -442,7 +444,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
if (currentAttachments.length || text) { if (currentAttachments.length || text) {
if (slowMode && !isAdmin) { if (slowMode && !isAdmin) {
const nowSeconds = Math.floor(Date.now() / 1000); const nowSeconds = Math.floor(Date.now() / 1000) + serverTimeOffset;
const secondsSinceLastMessage = lastMessageSendTimeSeconds.current const secondsSinceLastMessage = lastMessageSendTimeSeconds.current
&& Math.floor(nowSeconds - lastMessageSendTimeSeconds.current); && Math.floor(nowSeconds - lastMessageSendTimeSeconds.current);
const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds; const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds;
@ -480,15 +482,15 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
forwardMessages(); forwardMessages();
} }
lastMessageSendTimeSeconds.current = Math.floor(Date.now() / 1000); lastMessageSendTimeSeconds.current = Math.floor(Date.now() / 1000) + serverTimeOffset;
clearDraft({ chatId, localOnly: true }); clearDraft({ chatId, localOnly: true });
// Wait until message animation starts // Wait until message animation starts
requestAnimationFrame(resetComposer); requestAnimationFrame(resetComposer);
}, [ }, [
activeVoiceRecording, attachments, connectionState, chatId, slowMode, isForwarding, isAdmin, connectionState, attachments, activeVoiceRecording, isForwarding, serverTimeOffset, clearDraft, chatId,
sendMessage, stopRecordingVoice, resetComposer, clearDraft, showError, forwardMessages, resetComposer, stopRecordingVoice, showError, slowMode, isAdmin, sendMessage, forwardMessages,
]); ]);
const handleStickerSelect = useCallback((sticker: ApiSticker) => { const handleStickerSelect = useCallback((sticker: ApiSticker) => {
@ -536,11 +538,12 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
} }
}, [handleSend, openCalendar, shouldSchedule]); }, [handleSend, openCalendar, shouldSchedule]);
const handleMessageSchedule = useCallback((date: Date) => { const handleMessageSchedule = useCallback((date: Date, isWhenOnline = false) => {
const { isSilent, ...restArgs } = scheduledMessageArgs || {}; const { isSilent, ...restArgs } = scheduledMessageArgs || {};
// Scheduled time can not be less than 10 seconds in future // 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) { if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) {
handleSend(!!isSilent, scheduledAt); handleSend(!!isSilent, scheduledAt);
@ -552,10 +555,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
requestAnimationFrame(resetComposer); requestAnimationFrame(resetComposer);
} }
closeCalendar(); closeCalendar();
}, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage]); }, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage, serverTimeOffset]);
const handleMessageScheduleUntilOnline = useCallback(() => { const handleMessageScheduleUntilOnline = useCallback(() => {
handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000)); handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true);
}, [handleMessageSchedule]); }, [handleMessageSchedule]);
const handleCloseCalendar = useCallback(() => { const handleCloseCalendar = useCallback(() => {
@ -961,6 +964,7 @@ export default memo(withGlobal<OwnProps>(
recentEmojis: global.recentEmojis, recentEmojis: global.recentEmojis,
language, language,
emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined, emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined,
serverTimeOffset: global.serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -38,6 +38,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
recentVoterIds?: number[]; recentVoterIds?: number[];
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
serverTimeOffset: number;
}; };
type DispatchProps = Pick<GlobalActions, ('loadMessage' | 'openPollResults')>; type DispatchProps = Pick<GlobalActions, ('loadMessage' | 'openPollResults')>;
@ -54,6 +55,7 @@ const Poll: FC<OwnProps & StateProps & DispatchProps> = ({
loadMessage, loadMessage,
onSendVote, onSendVote,
openPollResults, openPollResults,
serverTimeOffset,
}) => { }) => {
const { id: messageId, chatId } = message; const { id: messageId, chatId } = message;
const { summary, results } = poll; const { summary, results } = poll;
@ -63,7 +65,7 @@ const Poll: FC<OwnProps & StateProps & DispatchProps> = ({
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false); const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
const [closePeriod, setClosePeriod] = useState<number>( const [closePeriod, setClosePeriod] = useState<number>(
!summary.closed && summary.closeDate && summary.closeDate > 0 !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, : 0,
); );
// eslint-disable-next-line no-null/no-null // 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>( export default memo(withGlobal<OwnProps>(
(global, { poll }) => { (global, { poll }) => {
const { recentVoterIds } = poll.results; const { recentVoterIds } = poll.results;
const { byId: usersById } = global.users; const { serverTimeOffset, users: { byId: usersById } } = global;
if (!recentVoterIds || recentVoterIds.length === 0) { if (!recentVoterIds || recentVoterIds.length === 0) {
return {}; return {};
} }
@ -367,6 +369,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
recentVoterIds, recentVoterIds,
usersById, usersById,
serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadMessage', 'openPollResults']), (setGlobal, actions): DispatchProps => pick(actions, ['loadMessage', 'openPollResults']),

View File

@ -75,6 +75,7 @@ type StateProps = {
isRightColumnShown: boolean; isRightColumnShown: boolean;
isRestricted?: boolean; isRestricted?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
serverTimeOffset: number;
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
@ -115,6 +116,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
openUserInfo, openUserInfo,
focusMessage, focusMessage,
loadProfilePhotos, loadProfilePhotos,
serverTimeOffset,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -133,7 +135,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members, isRightColumnShown, loadMoreMembers, searchMediaMessagesLocal, tabType, mediaSearchType, members,
usersById, chatMessages, foundIds, chatId, lastSyncTime, usersById, chatMessages, foundIds, chatId, lastSyncTime, serverTimeOffset,
); );
const activeKey = tabs.findIndex(({ type }) => type === resultType); const activeKey = tabs.findIndex(({ type }) => type === resultType);
@ -406,6 +408,7 @@ export default memo(withGlobal<OwnProps>(
isRightColumnShown: selectIsRightColumnShown(global), isRightColumnShown: selectIsRightColumnShown(global),
isRestricted: chat && chat.isRestricted, isRestricted: chat && chat.isRestricted,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
serverTimeOffset: global.serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -34,6 +34,7 @@ type StateProps = {
chat?: ApiChat; chat?: ApiChat;
isSavedMessages?: boolean; isSavedMessages?: boolean;
animationLevel: 0 | 1 | 2; animationLevel: 0 | 1 | 2;
serverTimeOffset: number;
} & Pick<GlobalState, 'lastSyncTime'>; } & Pick<GlobalState, 'lastSyncTime'>;
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>; type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'openMediaViewer'>;
@ -46,6 +47,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
animationLevel, animationLevel,
loadFullUser, loadFullUser,
openMediaViewer, openMediaViewer,
serverTimeOffset,
}) => { }) => {
const { id: userId } = user || {}; const { id: userId } = user || {};
const { id: chatId } = chat || {}; const { id: chatId } = chat || {};
@ -157,7 +159,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
if (user) { if (user) {
return ( return (
<div className={`status ${isUserOnline(user) ? 'online' : ''}`}> <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> </div>
); );
} }
@ -219,14 +221,14 @@ const PrivateChatInfo: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { lastSyncTime } = global; const { lastSyncTime, serverTimeOffset } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { animationLevel } = global.settings.byKey; const { animationLevel } = global.settings.byKey;
return { return {
lastSyncTime, user, chat, isSavedMessages, animationLevel, lastSyncTime, user, chat, isSavedMessages, animationLevel, serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']), (setGlobal, actions): DispatchProps => pick(actions, ['loadFullUser', 'openMediaViewer']),

View File

@ -20,6 +20,7 @@ export default function useProfileViewportIds(
foundIds?: number[], foundIds?: number[],
chatId?: number, chatId?: number,
lastSyncTime?: number, lastSyncTime?: number,
serverTimeOffset = 0,
) { ) {
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType; const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
@ -28,8 +29,8 @@ export default function useProfileViewportIds(
return undefined; return undefined;
} }
return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById); return sortUserIds(groupChatMembers.map(({ userId }) => userId), usersById, undefined, serverTimeOffset);
}, [groupChatMembers, usersById]); }, [groupChatMembers, serverTimeOffset, usersById]);
const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers( const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForMembers(
resultType, loadMoreMembers, lastSyncTime, memberIds, resultType, loadMoreMembers, lastSyncTime, memberIds,

View File

@ -21,6 +21,7 @@ type StateProps = {
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
members?: ApiChatMember[]; members?: ApiChatMember[];
isChannel?: boolean; isChannel?: boolean;
serverTimeOffset: number;
}; };
type DispatchProps = Pick<GlobalActions, 'openUserInfo'>; type DispatchProps = Pick<GlobalActions, 'openUserInfo'>;
@ -30,14 +31,15 @@ const ManageGroupMembers: FC<OwnProps & StateProps & DispatchProps> = ({
usersById, usersById,
isChannel, isChannel,
openUserInfo, openUserInfo,
serverTimeOffset,
}) => { }) => {
const memberIds = useMemo(() => { const memberIds = useMemo(() => {
if (!members || !usersById) { if (!members || !usersById) {
return undefined; return undefined;
} }
return sortUserIds(members.map(({ userId }) => userId), usersById); return sortUserIds(members.map(({ userId }) => userId), usersById, undefined, serverTimeOffset);
}, [members, usersById]); }, [members, serverTimeOffset, usersById]);
const handleMemberClick = useCallback((id: number) => { const handleMemberClick = useCallback((id: number) => {
openUserInfo({ id }); openUserInfo({ id });
@ -82,6 +84,7 @@ export default memo(withGlobal<OwnProps>(
members, members,
usersById, usersById,
isChannel, isChannel,
serverTimeOffset: global.serverTimeOffset,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -23,6 +23,7 @@ type StateProps = {
usersById: Record<number, ApiUser>; usersById: Record<number, ApiUser>;
members?: ApiChatMember[]; members?: ApiChatMember[];
isChannel?: boolean; isChannel?: boolean;
serverTimeOffset: number;
}; };
const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
@ -31,14 +32,18 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
isChannel, isChannel,
onScreenSelect, onScreenSelect,
onChatMemberSelect, onChatMemberSelect,
serverTimeOffset,
}) => { }) => {
const memberIds = useMemo(() => { const memberIds = useMemo(() => {
if (!members || !usersById) { if (!members || !usersById) {
return undefined; return undefined;
} }
return sortUserIds(members.filter((member) => !member.isOwner).map(({ userId }) => userId), usersById); return sortUserIds(
}, [members, usersById]); members.filter((member) => !member.isOwner).map(({ userId }) => userId),
usersById, undefined, serverTimeOffset,
);
}, [members, serverTimeOffset, usersById]);
const handleExceptionMemberClick = useCallback((memberId: number) => { const handleExceptionMemberClick = useCallback((memberId: number) => {
onChatMemberSelect(memberId); onChatMemberSelect(memberId);
@ -85,6 +90,7 @@ export default memo(withGlobal<OwnProps>(
members, members,
usersById, usersById,
isChannel, isChannel,
serverTimeOffset: global.serverTimeOffset,
}; };
}, },
)(ManageGroupUserPermissionsCreate)); )(ManageGroupUserPermissionsCreate));

View File

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

View File

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

View File

@ -15,7 +15,7 @@ const {
} = require('../tl').constructors; } = require('../tl').constructors;
const MessagePacker = require('../extensions/MessagePacker'); const MessagePacker = require('../extensions/MessagePacker');
const BinaryReader = require('../extensions/BinaryReader'); const BinaryReader = require('../extensions/BinaryReader');
const { UpdateConnectionState } = require('./index'); const { UpdateConnectionState, UpdateServerTimeOffset } = require('./index');
const { BadMessageError } = require('../errors/Common'); const { BadMessageError } = require('../errors/Common');
const { const {
BadServerSalt, BadServerSalt,
@ -259,6 +259,10 @@ class MTProtoSender {
this._state.time_offset = res.timeOffset; 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 * This is *EXTREMELY* important since we don't control
* external references to the authorization key, we must * external references to the authorization key, we must
@ -630,6 +634,11 @@ class MTProtoSender {
// Sent msg_id too low or too high (respectively). // Sent msg_id too low or too high (respectively).
// Use the current msg_id to determine the right time offset. // Use the current msg_id to determine the right time offset.
const to = this._state.updateTimeOffset(message.msgId); 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`); this._log.info(`System clock is wrong, set time offset to ${to}s`);
} else if (badMsg.errorCode === 32) { } else if (badMsg.errorCode === 32) {
// msg_seqno too low, so just pump it up by some "large" amount // 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 { const {
Connection, Connection,
ConnectionTCPFull, ConnectionTCPFull,
@ -30,4 +36,5 @@ module.exports = {
doAuthentication, doAuthentication,
MTProtoSender, MTProtoSender,
UpdateConnectionState, UpdateConnectionState,
UpdateServerTimeOffset,
}; };

View File

@ -177,23 +177,28 @@ addReducer('loadTopChats', () => {
}); });
addReducer('requestChatUpdate', (global, actions, payload) => { addReducer('requestChatUpdate', (global, actions, payload) => {
const { serverTimeOffset } = global;
const { chatId } = payload!; const { chatId } = payload!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
} }
void callApi('requestChatUpdate', chat); void callApi('requestChatUpdate', {
chat,
serverTimeOffset,
});
}); });
addReducer('updateChatMutedState', (global, actions, payload) => { addReducer('updateChatMutedState', (global, actions, payload) => {
const { serverTimeOffset } = global;
const { chatId, isMuted } = payload!; const { chatId, isMuted } = payload!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
} }
void callApi('updateChatMutedState', { chat, isMuted }); void callApi('updateChatMutedState', { chat, isMuted, serverTimeOffset });
}); });
addReducer('createChannel', (global, actions, payload) => { addReducer('createChannel', (global, actions, payload) => {
@ -358,10 +363,11 @@ addReducer('deleteChatFolder', (global, actions, payload) => {
addReducer('toggleChatUnread', (global, actions, payload) => { addReducer('toggleChatUnread', (global, actions, payload) => {
const { id } = payload!; const { id } = payload!;
const { serverTimeOffset } = global;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (chat) { if (chat) {
if (chat.unreadCount) { if (chat.unreadCount) {
void callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID }); void callApi('markMessageListRead', { serverTimeOffset, chat, threadId: MAIN_THREAD_ID });
} else { } else {
void callApi('toggleDialogUnread', { void callApi('toggleDialogUnread', {
chat, chat,
@ -723,6 +729,7 @@ async function loadChats(listType: 'active' | 'archived', offsetId?: number, off
offsetDate, offsetDate,
archived: listType === 'archived', archived: listType === 'archived',
withPinned: getGlobal().chats.orderedPinnedIds[listType] === undefined, withPinned: getGlobal().chats.orderedPinnedIds[listType] === undefined,
serverTimeOffset: getGlobal().serverTimeOffset,
}); });
if (!result) { if (!result) {

View File

@ -243,6 +243,7 @@ addReducer('sendMessage', (global, actions, payload) => {
}); });
addReducer('editMessage', (global, actions, payload) => { addReducer('editMessage', (global, actions, payload) => {
const { serverTimeOffset } = global;
const { text, entities } = payload!; const { text, entities } = payload!;
const currentMessageList = selectCurrentMessageList(global); const currentMessageList = selectCurrentMessageList(global);
@ -258,7 +259,7 @@ addReducer('editMessage', (global, actions, payload) => {
} }
void callApi('editMessage', { 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 }); actions.setEditingId({ messageId: undefined });
@ -404,6 +405,7 @@ addReducer('deleteHistory', (global, actions, payload) => {
}); });
addReducer('markMessageListRead', (global, actions, payload) => { addReducer('markMessageListRead', (global, actions, payload) => {
const { serverTimeOffset } = global;
const currentMessageList = selectCurrentMessageList(global); const currentMessageList = selectCurrentMessageList(global);
if (!currentMessageList) { if (!currentMessageList) {
return; return;
@ -418,7 +420,9 @@ addReducer('markMessageListRead', (global, actions, payload) => {
const { maxId } = payload!; const { maxId } = payload!;
runThrottledForMarkRead(() => { runThrottledForMarkRead(() => {
void callApi('markMessageListRead', { chat, threadId, maxId }); void callApi('markMessageListRead', {
serverTimeOffset, chat, threadId, maxId,
});
}); });
}); });
@ -703,6 +707,7 @@ async function sendMessage(params: {
sticker: ApiSticker; sticker: ApiSticker;
gif: ApiVideo; gif: ApiVideo;
poll: ApiNewPoll; poll: ApiNewPoll;
serverTimeOffset?: number;
}) { }) {
let localId: number | undefined; let localId: number | undefined;
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => { const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
@ -730,6 +735,7 @@ async function sendMessage(params: {
} }
const global = getGlobal(); const global = getGlobal();
params.serverTimeOffset = global.serverTimeOffset;
const currentMessageList = selectCurrentMessageList(global); const currentMessageList = selectCurrentMessageList(global);
if (!currentMessageList) { if (!currentMessageList) {
return; return;
@ -756,6 +762,7 @@ function forwardMessages(
fromChat, fromChat,
toChat, toChat,
messages, messages,
serverTimeOffset: getGlobal().serverTimeOffset,
}); });
setGlobal({ setGlobal({

View File

@ -308,9 +308,12 @@ addReducer('loadNotificationExceptions', () => {
callApi('fetchNotificationExceptions'); callApi('fetchNotificationExceptions');
}); });
addReducer('loadNotificationSettings', () => { addReducer('loadNotificationSettings', (global) => {
const { serverTimeOffset } = global;
(async () => { (async () => {
const result = await callApi('fetchNotificationSettings'); const result = await callApi('fetchNotificationSettings', {
serverTimeOffset,
});
if (!result) { if (!result) {
return; return;
} }

View File

@ -85,6 +85,7 @@ async function loadAndReplaceChats() {
const result = await callApi('fetchChats', { const result = await callApi('fetchChats', {
limit: CHAT_LIST_LOAD_SLICE, limit: CHAT_LIST_LOAD_SLICE,
withPinned: true, withPinned: true,
serverTimeOffset: getGlobal().serverTimeOffset,
}); });
if (!result) { if (!result) {
return undefined; return undefined;
@ -166,7 +167,9 @@ async function loadAndReplaceArchivedChats() {
limit: CHAT_LIST_LOAD_SLICE, limit: CHAT_LIST_LOAD_SLICE,
archived: true, archived: true,
withPinned: true, withPinned: true,
serverTimeOffset: getGlobal().serverTimeOffset,
}); });
if (!result) { if (!result) {
return; return;
} }

View File

@ -15,7 +15,7 @@ import {
} from '../../reducers'; } from '../../reducers';
const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true); 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) => { addReducer('loadFullUser', (global, actions, payload) => {
const { userId } = payload!; const { userId } = payload!;
@ -49,9 +49,14 @@ addReducer('loadUser', (global, actions, payload) => {
}); });
addReducer('loadTopUsers', (global) => { 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); void loadTopUsers(hash);
} }
}); });
@ -95,7 +100,7 @@ async function loadTopUsers(usersHash?: number) {
...global.topPeers, ...global.topPeers,
hash, hash,
userIds: ids, userIds: ids,
lastRequestedAt: Date.now(), lastRequestedAt: Date.now() / 1000 + global.serverTimeOffset,
}, },
}; };
setGlobal(global); setGlobal(global);

View File

@ -10,7 +10,7 @@ import {
ApiUpdateAuthorizationError, ApiUpdateAuthorizationError,
ApiUpdateConnectionState, ApiUpdateConnectionState,
ApiUpdateSession, ApiUpdateSession,
ApiUpdateCurrentUser, ApiUpdateCurrentUser, ApiUpdateServerTimeOffset,
} from '../../../api/types'; } from '../../../api/types';
import { DEBUG, SESSION_USER_KEY } from '../../../config'; import { DEBUG, SESSION_USER_KEY } from '../../../config';
import { subscribe } from '../../../util/notifications'; import { subscribe } from '../../../util/notifications';
@ -46,6 +46,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
onUpdateSession(update); onUpdateSession(update);
break; break;
case 'updateServerTimeOffset':
onUpdateServerTimeOffset(update);
break;
case 'updateCurrentUser': case 'updateCurrentUser':
onUpdateCurrentUser(update); onUpdateCurrentUser(update);
break; break;
@ -155,6 +159,13 @@ function onUpdateSession(update: ApiUpdateSession) {
getDispatch().saveSession({ sessionData }); getDispatch().saveSession({ sessionData });
} }
function onUpdateServerTimeOffset(update: ApiUpdateServerTimeOffset) {
setGlobal({
...getGlobal(),
serverTimeOffset: update.serverTimeOffset,
});
}
function onUpdateCurrentUser(update: ApiUpdateCurrentUser) { function onUpdateCurrentUser(update: ApiUpdateCurrentUser) {
const { currentUser } = update; const { currentUser } = update;

View File

@ -469,6 +469,7 @@ export function sortChatIds(
chatsById: Record<number, ApiChat>, chatsById: Record<number, ApiChat>,
shouldPrioritizeVerified = false, shouldPrioritizeVerified = false,
priorityIds?: number[], priorityIds?: number[],
serverTimeOffset = 0,
) { ) {
return orderBy(chatIds, (id) => { return orderBy(chatIds, (id) => {
const chat = chatsById[id]; const chat = chatsById[id];
@ -490,7 +491,7 @@ export function sortChatIds(
// Assuming that last message date can't be less than now, // Assuming that last message date can't be less than now,
// this should place prioritized on top of the list. // this should place prioritized on top of the list.
// Then we subtract index of `id` in `priorityIds` to preserve selected order // 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; return priority;

View File

@ -64,7 +64,7 @@ export function getUserFullName(user?: ApiUser) {
return undefined; 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) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) {
return lang('ServiceNotifications').toLowerCase(); return lang('ServiceNotifications').toLowerCase();
} }
@ -95,7 +95,7 @@ export function getUserStatus(lang: LangFn, user: ApiUser) {
if (!wasOnline) return lang('LastSeen.Offline'); 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); const wasOnlineDate = new Date(wasOnline * 1000);
if (wasOnlineDate >= now) { if (wasOnlineDate >= now) {
@ -118,7 +118,8 @@ export function getUserStatus(lang: LangFn, user: ApiUser) {
// today // today
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); 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 // up to 6 hours ago
if (diff.getTime() / 1000 < 6 * 60 * 60) { if (diff.getTime() / 1000 < 6 * 60 * 60) {
const hours = Math.floor(diff.getTime() / 1000 / 60 / 60); const hours = Math.floor(diff.getTime() / 1000 / 60 / 60);
@ -132,8 +133,9 @@ export function getUserStatus(lang: LangFn, user: ApiUser) {
// yesterday // yesterday
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(now.getDate() - 1); yesterday.setDate(now.getDate() - 1);
today.setHours(0, 0, 0, 0); yesterday.setHours(0, 0, 0, 0);
if (wasOnlineDate > yesterday) { const serverYesterday = new Date(yesterday.getTime() + serverTimeOffset * 1000);
if (wasOnlineDate > serverYesterday) {
return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate)); return lang('LastSeen.YesterdayAt', formatTime(wasOnlineDate));
} }
@ -184,9 +186,10 @@ export function sortUserIds(
userIds: number[], userIds: number[],
usersById: Record<number, ApiUser>, usersById: Record<number, ApiUser>,
priorityIds?: number[], priorityIds?: number[],
serverTimeOffset = 0,
) { ) {
return orderBy(userIds, (id) => { return orderBy(userIds, (id) => {
const now = Date.now() / 1000; const now = Date.now() / 1000 + serverTimeOffset;
if (priorityIds && priorityIds.includes(id)) { if (priorityIds && priorityIds.includes(id)) {
// Assuming that online status expiration date can't be as far as two days from now, // 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) { export function selectAllowedMessageActions(global: GlobalState, message: ApiMessage, threadId: number) {
const { serverTimeOffset } = global;
const chat = selectChat(global, message.chatId); const chat = selectChat(global, message.chatId);
if (!chat || chat.isRestricted) { if (!chat || chat.isRestricted) {
return {}; return {};
@ -351,7 +352,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
const isAction = isActionMessage(message); const isAction = isActionMessage(message);
const { content } = message; const { content } = message;
const isMessageEditable = ( 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.sticker || content.contact || content.poll || content.action || content.audio
|| (content.video && content.video.isRound) || (content.video && content.video.isRound)

View File

@ -133,10 +133,13 @@ let areSettingsLoaded = false;
async function loadNotificationSettings() { async function loadNotificationSettings() {
if (areSettingsLoaded) return; if (areSettingsLoaded) return;
const [result] = await Promise.all([ const [result] = await Promise.all([
callApi('fetchNotificationSettings'), callApi('fetchNotificationSettings', {
serverTimeOffset: getGlobal().serverTimeOffset,
}),
callApi('fetchNotificationExceptions'), callApi('fetchNotificationExceptions'),
]); ]);
if (!result) return; if (!result) return;
setGlobal(replaceSettings(getGlobal(), result)); setGlobal(replaceSettings(getGlobal(), result));
areSettingsLoaded = true; areSettingsLoaded = true;
} }