[Refactoring] Rework sent message handling (#2763)
This commit is contained in:
parent
7bccdedf2a
commit
bfbd9bd2ed
1
package-lock.json
generated
1
package-lock.json
generated
@ -105,7 +105,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dev/eslint-multitab": {
|
"dev/eslint-multitab": {
|
||||||
"name": "eslint-plugin-eslint-multitab-tt",
|
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import type { Api as GramJs } from '../../lib/gramjs';
|
import type { Api as GramJs } from '../../lib/gramjs';
|
||||||
import type { ApiMessage } from '../types';
|
|
||||||
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
||||||
import { DATA_BROADCAST_CHANNEL_NAME } from '../../config';
|
import { DATA_BROADCAST_CHANNEL_NAME } from '../../config';
|
||||||
import { constructors } from '../../lib/gramjs/tl';
|
import { constructors } from '../../lib/gramjs/tl';
|
||||||
@ -10,7 +9,6 @@ import { throttle } from '../../util/schedulers';
|
|||||||
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
||||||
|
|
||||||
export interface LocalDb {
|
export interface LocalDb {
|
||||||
localMessages: Record<string, ApiMessage>;
|
|
||||||
// Used for loading avatars and media through in-memory Gram JS instances.
|
// Used for loading avatars and media through in-memory Gram JS instances.
|
||||||
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
||||||
users: Record<string, GramJs.User>;
|
users: Record<string, GramJs.User>;
|
||||||
|
|||||||
@ -232,7 +232,7 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldIgnoreUpdates) {
|
if (!shouldIgnoreUpdates) {
|
||||||
handleUpdatesFromRequest(request, result);
|
handleUpdates(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldReturnTrue ? result && true : result;
|
return shouldReturnTrue ? result && true : result;
|
||||||
@ -256,7 +256,7 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdatesFromRequest<T extends GramJs.AnyRequest>(request: T, result: T['__response']) {
|
export function handleUpdates(result: {}) {
|
||||||
let manyUpdates;
|
let manyUpdates;
|
||||||
let singleUpdate;
|
let singleUpdate;
|
||||||
|
|
||||||
@ -279,10 +279,10 @@ function handleUpdatesFromRequest<T extends GramJs.AnyRequest>(request: T, resul
|
|||||||
injectUpdateEntities(manyUpdates);
|
injectUpdateEntities(manyUpdates);
|
||||||
|
|
||||||
manyUpdates.updates.forEach((update) => {
|
manyUpdates.updates.forEach((update) => {
|
||||||
updater(update, request);
|
updater(update);
|
||||||
});
|
});
|
||||||
} else if (singleUpdate) {
|
} else if (singleUpdate) {
|
||||||
updater(singleUpdate, request);
|
updater(singleUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { invokeRequest, uploadFile } from './client';
|
import { handleUpdates, invokeRequest, uploadFile } from './client';
|
||||||
import {
|
import {
|
||||||
buildApiMessage,
|
buildApiMessage,
|
||||||
buildLocalForwardedMessage,
|
buildLocalForwardedMessage,
|
||||||
@ -38,6 +38,8 @@ import {
|
|||||||
buildWebPage,
|
buildWebPage,
|
||||||
buildApiSponsoredMessage,
|
buildApiSponsoredMessage,
|
||||||
buildApiFormattedText,
|
buildApiFormattedText,
|
||||||
|
buildMessageTextContent,
|
||||||
|
buildMessageMediaContent,
|
||||||
} from '../apiBuilders/messages';
|
} from '../apiBuilders/messages';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
@ -54,8 +56,8 @@ import {
|
|||||||
buildSendMessageAction,
|
buildSendMessageAction,
|
||||||
buildInputPollFromExisting,
|
buildInputPollFromExisting,
|
||||||
buildInputTextWithEntities,
|
buildInputTextWithEntities,
|
||||||
|
buildMessageFromUpdate,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
|
||||||
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
||||||
import { fetchFile } from '../../../util/files';
|
import { fetchFile } from '../../../util/files';
|
||||||
import {
|
import {
|
||||||
@ -287,7 +289,6 @@ export function sendMessage(
|
|||||||
}, FAST_SEND_TIMEOUT);
|
}, FAST_SEND_TIMEOUT);
|
||||||
|
|
||||||
const randomId = generateRandomBigInt();
|
const randomId = generateRandomBigInt();
|
||||||
localDb.localMessages[String(randomId)] = localMessage;
|
|
||||||
|
|
||||||
if (groupedId) {
|
if (groupedId) {
|
||||||
return sendGroupedMedia({
|
return sendGroupedMedia({
|
||||||
@ -339,7 +340,7 @@ export function sendMessage(
|
|||||||
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
|
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invokeRequest(new RequestClass({
|
const update = await invokeRequest(new RequestClass({
|
||||||
clearDraft: true,
|
clearDraft: true,
|
||||||
message: text || '',
|
message: text || '',
|
||||||
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
|
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
|
||||||
@ -353,7 +354,8 @@ export function sendMessage(
|
|||||||
...(noWebPage && { noWebpage: noWebPage }),
|
...(noWebPage && { noWebpage: noWebPage }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
...(shouldUpdateStickerSetsOrder && { updateStickersetsOrder: shouldUpdateStickerSetsOrder }),
|
...(shouldUpdateStickerSetsOrder && { updateStickersetsOrder: shouldUpdateStickerSetsOrder }),
|
||||||
}), true, true);
|
}), false, true, true);
|
||||||
|
if (update) handleLocalMessageUpdate(localMessage, update);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateMessageSendFailed',
|
'@type': 'updateMessageSendFailed',
|
||||||
@ -371,6 +373,7 @@ export function sendMessage(
|
|||||||
const groupedUploads: Record<string, {
|
const groupedUploads: Record<string, {
|
||||||
counter: number;
|
counter: number;
|
||||||
singleMediaByIndex: Record<number, GramJs.InputSingleMedia>;
|
singleMediaByIndex: Record<number, GramJs.InputSingleMedia>;
|
||||||
|
localMessages: Record<string, ApiMessage>;
|
||||||
}> = {};
|
}> = {};
|
||||||
|
|
||||||
function sendGroupedMedia(
|
function sendGroupedMedia(
|
||||||
@ -406,6 +409,7 @@ function sendGroupedMedia(
|
|||||||
groupedUploads[groupedId] = {
|
groupedUploads[groupedId] = {
|
||||||
counter: 0,
|
counter: 0,
|
||||||
singleMediaByIndex: {},
|
singleMediaByIndex: {},
|
||||||
|
localMessages: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,15 +457,16 @@ function sendGroupedMedia(
|
|||||||
message: text || '',
|
message: text || '',
|
||||||
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
|
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
|
||||||
});
|
});
|
||||||
|
groupedUploads[groupedId].localMessages[randomId.toString()] = localMessage;
|
||||||
|
|
||||||
if (Object.keys(groupedUploads[groupedId].singleMediaByIndex).length < groupedUploads[groupedId].counter) {
|
if (Object.keys(groupedUploads[groupedId].singleMediaByIndex).length < groupedUploads[groupedId].counter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { singleMediaByIndex } = groupedUploads[groupedId];
|
const { singleMediaByIndex, localMessages } = groupedUploads[groupedId];
|
||||||
delete groupedUploads[groupedId];
|
delete groupedUploads[groupedId];
|
||||||
|
|
||||||
await invokeRequest(new GramJs.messages.SendMultiMedia({
|
const update = await invokeRequest(new GramJs.messages.SendMultiMedia({
|
||||||
clearDraft: true,
|
clearDraft: true,
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
||||||
@ -470,7 +475,9 @@ function sendGroupedMedia(
|
|||||||
...(isSilent && { silent: isSilent }),
|
...(isSilent && { silent: isSilent }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}), true);
|
}), false, undefined, true);
|
||||||
|
|
||||||
|
if (update) handleMultipleLocalMessagesUpdate(localMessages, update);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return queue;
|
return queue;
|
||||||
@ -554,9 +561,6 @@ export async function editMessage({
|
|||||||
message: messageUpdate,
|
message: messageUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO Revise intersecting with scheduled
|
|
||||||
localDb.localMessages[message.id] = { ...message, ...messageUpdate };
|
|
||||||
|
|
||||||
const mtpEntities = entities && entities.map(buildMtpMessageEntity);
|
const mtpEntities = entities && entities.map(buildMtpMessageEntity);
|
||||||
|
|
||||||
await invokeRequest(new GramJs.messages.EditMessage({
|
await invokeRequest(new GramJs.messages.EditMessage({
|
||||||
@ -1267,6 +1271,7 @@ export async function forwardMessages({
|
|||||||
}) {
|
}) {
|
||||||
const messageIds = messages.map(({ id }) => id);
|
const messageIds = messages.map(({ id }) => id);
|
||||||
const randomIds = messages.map(generateRandomBigInt);
|
const randomIds = messages.map(generateRandomBigInt);
|
||||||
|
const localMessages: Record<string, ApiMessage> = {};
|
||||||
|
|
||||||
messages.forEach((message, index) => {
|
messages.forEach((message, index) => {
|
||||||
const localMessage = buildLocalForwardedMessage({
|
const localMessage = buildLocalForwardedMessage({
|
||||||
@ -1278,7 +1283,7 @@ export async function forwardMessages({
|
|||||||
noCaptions,
|
noCaptions,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
});
|
});
|
||||||
localDb.localMessages[String(randomIds[index])] = localMessage;
|
localMessages[randomIds[index].toString()] = localMessage;
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
||||||
@ -1288,7 +1293,7 @@ export async function forwardMessages({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await invokeRequest(new GramJs.messages.ForwardMessages({
|
const update = await invokeRequest(new GramJs.messages.ForwardMessages({
|
||||||
fromPeer: buildInputPeer(fromChat.id, fromChat.accessHash),
|
fromPeer: buildInputPeer(fromChat.id, fromChat.accessHash),
|
||||||
toPeer: buildInputPeer(toChat.id, toChat.accessHash),
|
toPeer: buildInputPeer(toChat.id, toChat.accessHash),
|
||||||
randomId: randomIds,
|
randomId: randomIds,
|
||||||
@ -1300,7 +1305,8 @@ export async function forwardMessages({
|
|||||||
...(toThreadId && { topMsgId: toThreadId }),
|
...(toThreadId && { topMsgId: toThreadId }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}), true);
|
}), false, undefined, true);
|
||||||
|
if (update) handleMultipleLocalMessagesUpdate(localMessages, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findFirstMessageIdAfterDate({
|
export async function findFirstMessageIdAfterDate({
|
||||||
@ -1632,3 +1638,74 @@ export async function translateText(params: TranslateTextParams) {
|
|||||||
|
|
||||||
return formattedText;
|
return formattedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMultipleLocalMessagesUpdate(
|
||||||
|
localMessages: Record<string, ApiMessage>, update: GramJs.TypeUpdates,
|
||||||
|
) {
|
||||||
|
if (!('updates' in update)) return;
|
||||||
|
update.updates.forEach((u) => {
|
||||||
|
if (u instanceof GramJs.UpdateMessageID) {
|
||||||
|
const localMessage = localMessages[u.randomId.toString()];
|
||||||
|
handleLocalMessageUpdate(localMessage, u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeUpdates) {
|
||||||
|
let messageUpdate;
|
||||||
|
if (update instanceof GramJs.UpdateShortSentMessage || update instanceof GramJs.UpdateMessageID) {
|
||||||
|
messageUpdate = update;
|
||||||
|
} else if ('updates' in update) {
|
||||||
|
messageUpdate = update.updates.find((u): u is GramJs.UpdateMessageID => u instanceof GramJs.UpdateMessageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageUpdate) {
|
||||||
|
handleUpdates(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newContent: ApiMessage['content'] | undefined;
|
||||||
|
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
|
||||||
|
if (localMessage.content.text && messageUpdate.entities) {
|
||||||
|
newContent = {
|
||||||
|
text: buildMessageTextContent(localMessage.content.text.text, messageUpdate.entities),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (messageUpdate.media) {
|
||||||
|
newContent = {
|
||||||
|
...newContent,
|
||||||
|
...buildMessageMediaContent(messageUpdate.media),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate);
|
||||||
|
if (isMessageWithMedia(mtpMessage)) {
|
||||||
|
addMessageToLocalDb(mtpMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge case for "Send When Online"
|
||||||
|
const isSentBefore = 'date' in messageUpdate && messageUpdate.date * 1000 < Date.now() + getServerTimeOffset() * 1000;
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': localMessage.isScheduled && !isSentBefore
|
||||||
|
? 'updateScheduledMessageSendSucceeded'
|
||||||
|
: 'updateMessageSendSucceeded',
|
||||||
|
chatId: localMessage.chatId,
|
||||||
|
localId: localMessage.id,
|
||||||
|
message: {
|
||||||
|
...localMessage,
|
||||||
|
...(newContent && {
|
||||||
|
content: {
|
||||||
|
...localMessage.content,
|
||||||
|
...newContent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
id: messageUpdate.id,
|
||||||
|
sendingState: undefined,
|
||||||
|
...('date' in messageUpdate && { date: messageUpdate.date }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handleUpdates(update);
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
buildApiMessageFromShort,
|
buildApiMessageFromShort,
|
||||||
buildApiMessageFromShortChat,
|
buildApiMessageFromShortChat,
|
||||||
buildMessageMediaContent,
|
buildMessageMediaContent,
|
||||||
buildMessageTextContent,
|
|
||||||
buildPoll,
|
buildPoll,
|
||||||
buildPollResults,
|
buildPollResults,
|
||||||
buildApiMessageFromNotification,
|
buildApiMessageFromNotification,
|
||||||
@ -116,7 +115,7 @@ function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
export function updater(update: Update) {
|
||||||
if (update instanceof connection.UpdateServerTimeOffset) {
|
if (update instanceof connection.UpdateServerTimeOffset) {
|
||||||
setServerTimeOffset(update.timeOffset);
|
setServerTimeOffset(update.timeOffset);
|
||||||
|
|
||||||
@ -440,70 +439,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ((
|
} else if (update instanceof GramJs.UpdateMessageID || update instanceof GramJs.UpdateShortSentMessage) {
|
||||||
originRequest instanceof GramJs.messages.SendMessage
|
|
||||||
|| originRequest instanceof GramJs.messages.SendMedia
|
|
||||||
|| originRequest instanceof GramJs.messages.SendMultiMedia
|
|
||||||
|| originRequest instanceof GramJs.messages.ForwardMessages
|
|
||||||
) && (
|
|
||||||
update instanceof GramJs.UpdateMessageID || update instanceof GramJs.UpdateShortSentMessage
|
|
||||||
)) {
|
|
||||||
let randomId;
|
|
||||||
if ('randomId' in update) {
|
|
||||||
randomId = update.randomId;
|
|
||||||
} else if ('randomId' in originRequest) {
|
|
||||||
randomId = originRequest.randomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localMessage = randomId && localDb.localMessages[String(randomId)];
|
|
||||||
if (!localMessage) {
|
|
||||||
throw new Error('Local message not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
let newContent: ApiMessage['content'] | undefined;
|
|
||||||
if (update instanceof GramJs.UpdateShortSentMessage) {
|
|
||||||
if (localMessage.content.text && update.entities) {
|
|
||||||
newContent = {
|
|
||||||
text: buildMessageTextContent(localMessage.content.text.text, update.entities),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (update.media) {
|
|
||||||
newContent = {
|
|
||||||
...newContent,
|
|
||||||
...buildMessageMediaContent(update.media),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mtpMessage = buildMessageFromUpdate(update.id, localMessage.chatId, update);
|
|
||||||
if (isMessageWithMedia(mtpMessage)) {
|
|
||||||
addMessageToLocalDb(mtpMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sentMessageIds.add(update.id);
|
sentMessageIds.add(update.id);
|
||||||
|
|
||||||
// Edge case for "Send When Online"
|
|
||||||
const isAlreadySent = 'date' in update && update.date * 1000 < Date.now() + getServerTimeOffset() * 1000;
|
|
||||||
|
|
||||||
onUpdate({
|
|
||||||
'@type': localMessage.isScheduled && !isAlreadySent
|
|
||||||
? 'updateScheduledMessageSendSucceeded'
|
|
||||||
: 'updateMessageSendSucceeded',
|
|
||||||
chatId: localMessage.chatId,
|
|
||||||
localId: localMessage.id,
|
|
||||||
message: {
|
|
||||||
...localMessage,
|
|
||||||
...(newContent && {
|
|
||||||
content: {
|
|
||||||
...localMessage.content,
|
|
||||||
...newContent,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
id: update.id,
|
|
||||||
sendingState: undefined,
|
|
||||||
...('date' in update && { date: update.date }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (update instanceof GramJs.UpdateReadMessagesContents) {
|
} else if (update instanceof GramJs.UpdateReadMessagesContents) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateCommonBoxMessages',
|
'@type': 'updateCommonBoxMessages',
|
||||||
|
|||||||
@ -25,7 +25,6 @@ let worker: Worker;
|
|||||||
const requestStates = new Map<string, RequestStates>();
|
const requestStates = new Map<string, RequestStates>();
|
||||||
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
||||||
const savedLocalDb: LocalDb = {
|
const savedLocalDb: LocalDb = {
|
||||||
localMessages: {},
|
|
||||||
chats: {},
|
chats: {},
|
||||||
users: {},
|
users: {},
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user