Middle Header: Update typing status lang strings (#6854)

This commit is contained in:
zubiden 2026-04-27 14:29:26 +02:00 committed by Alexander Zinchuk
parent 881c09acdf
commit 230e9797d4
27 changed files with 471 additions and 176 deletions

View File

@ -22,12 +22,13 @@ import {
type ApiSponsoredPeer, type ApiSponsoredPeer,
type ApiStarsSubscriptionPricing, type ApiStarsSubscriptionPricing,
type ApiThreadInfo, type ApiThreadInfo,
type ApiTypingStatus,
MAIN_THREAD_ID, MAIN_THREAD_ID,
} from '../../types'; } from '../../types';
import { omitUndefined, pickTruthy } from '../../../util/iteratees'; import { omitUndefined, pickTruthy } from '../../../util/iteratees';
import { toJSNumber } from '../../../util/numbers'; import { toJSNumber } from '../../../util/numbers';
import { getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc'; import { serializeBytes } from '../helpers/misc';
import { import {
@ -382,52 +383,73 @@ export function buildChatMembers(
export function buildChatTypingStatus( export function buildChatTypingStatus(
update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping, update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping,
) { ): ApiTypingStatus | undefined {
let action: string = ''; const action = update.action;
let emoticon: string | undefined; const timestamp = getServerTime();
if (update.action instanceof GramJs.SendMessageCancelAction) { const buildTypingStatus = (
type: Exclude<ApiTypingStatus['type'], 'watchingAnimations'>,
): ApiTypingStatus => ({
timestamp,
type,
});
if (action instanceof GramJs.SendMessageCancelAction) {
return undefined; return undefined;
} else if (update.action instanceof GramJs.SendMessageTypingAction) { }
action = 'lng_user_typing'; if (action instanceof GramJs.SendMessageTypingAction) {
} else if (update.action instanceof GramJs.SendMessageRecordVideoAction) { return buildTypingStatus('typing');
action = 'lng_send_action_record_video'; }
} else if (update.action instanceof GramJs.SendMessageUploadVideoAction) { if (action instanceof GramJs.SendMessageRecordVideoAction) {
action = 'lng_send_action_upload_video'; return buildTypingStatus('recordVideo');
} else if (update.action instanceof GramJs.SendMessageRecordAudioAction) { }
action = 'lng_send_action_record_audio'; if (action instanceof GramJs.SendMessageUploadVideoAction) {
} else if (update.action instanceof GramJs.SendMessageUploadAudioAction) { return buildTypingStatus('uploadVideo');
action = 'lng_send_action_upload_audio'; }
} else if (update.action instanceof GramJs.SendMessageUploadPhotoAction) { if (action instanceof GramJs.SendMessageRecordAudioAction) {
action = 'lng_send_action_upload_photo'; return buildTypingStatus('recordAudio');
} else if (update.action instanceof GramJs.SendMessageUploadDocumentAction) { }
action = 'lng_send_action_upload_file'; if (action instanceof GramJs.SendMessageUploadAudioAction) {
} else if (update.action instanceof GramJs.SendMessageGeoLocationAction) { return buildTypingStatus('uploadAudio');
action = 'selecting a location to share'; }
} else if (update.action instanceof GramJs.SendMessageChooseContactAction) { if (action instanceof GramJs.SendMessageUploadPhotoAction) {
action = 'selecting a contact to share'; return buildTypingStatus('uploadPhoto');
} else if (update.action instanceof GramJs.SendMessageGamePlayAction) { }
action = 'lng_playing_game'; if (action instanceof GramJs.SendMessageUploadDocumentAction) {
} else if (update.action instanceof GramJs.SendMessageRecordRoundAction) { return buildTypingStatus('uploadFile');
action = 'lng_send_action_record_round'; }
} else if (update.action instanceof GramJs.SendMessageUploadRoundAction) { if (action instanceof GramJs.SendMessageGeoLocationAction) {
action = 'lng_send_action_upload_round'; return buildTypingStatus('chooseLocation');
} else if (update.action instanceof GramJs.SendMessageChooseStickerAction) { }
action = 'lng_send_action_choose_sticker'; if (action instanceof GramJs.SendMessageChooseContactAction) {
} else if (update.action instanceof GramJs.SpeakingInGroupCallAction) { return buildTypingStatus('chooseContact');
}
if (action instanceof GramJs.SendMessageGamePlayAction) {
return buildTypingStatus('playingGame');
}
if (action instanceof GramJs.SendMessageRecordRoundAction) {
return buildTypingStatus('recordRound');
}
if (action instanceof GramJs.SendMessageUploadRoundAction) {
return buildTypingStatus('uploadRound');
}
if (action instanceof GramJs.SendMessageChooseStickerAction) {
return buildTypingStatus('chooseSticker');
}
if (action instanceof GramJs.SpeakingInGroupCallAction) {
return undefined; return undefined;
} else if (update.action instanceof GramJs.SendMessageEmojiInteractionSeen) { }
action = 'lng_user_action_watching_animations'; if (action instanceof GramJs.SendMessageEmojiInteractionSeen) {
emoticon = update.action.emoticon; return {
} else if (update.action instanceof GramJs.SendMessageEmojiInteraction) { timestamp,
type: 'watchingAnimations',
emoji: action.emoticon,
};
}
if (action instanceof GramJs.SendMessageEmojiInteraction) {
return undefined; return undefined;
} }
return { return undefined;
action,
...(emoticon && { emoji: emoticon }),
...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }),
timestamp: Date.now() + getServerTimeOffset() * 1000,
};
} }
export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFilterChatlist): ApiChatFolder { export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFilterChatlist): ApiChatFolder {

View File

@ -677,6 +677,9 @@ export function updater(update: Update) {
const chatId = update instanceof GramJs.UpdateUserTyping const chatId = update instanceof GramJs.UpdateUserTyping
? buildApiPeerId(update.userId, 'user') ? buildApiPeerId(update.userId, 'user')
: buildApiPeerId(update.chatId, 'chat'); : buildApiPeerId(update.chatId, 'chat');
const peerId = update instanceof GramJs.UpdateUserTyping
? buildApiPeerId(update.userId, 'user')
: getApiChatIdFromMtpPeer(update.fromId);
const threadId = update instanceof GramJs.UpdateUserTyping ? update.topMsgId : undefined; const threadId = update instanceof GramJs.UpdateUserTyping ? update.topMsgId : undefined;
@ -700,16 +703,19 @@ export function updater(update: Update) {
sendApiUpdate({ sendApiUpdate({
'@type': 'updateChatTypingStatus', '@type': 'updateChatTypingStatus',
id: chatId, id: chatId,
peerId,
threadId, threadId,
typingStatus: buildChatTypingStatus(update), typingStatus: buildChatTypingStatus(update),
}); });
} }
} else if (update instanceof GramJs.UpdateChannelUserTyping) { } else if (update instanceof GramJs.UpdateChannelUserTyping) {
const id = buildApiPeerId(update.channelId, 'channel'); const id = buildApiPeerId(update.channelId, 'channel');
const peerId = getApiChatIdFromMtpPeer(update.fromId);
sendApiUpdate({ sendApiUpdate({
'@type': 'updateChatTypingStatus', '@type': 'updateChatTypingStatus',
id, id,
peerId,
threadId: update.topMsgId, threadId: update.topMsgId,
typingStatus: buildChatTypingStatus(update), typingStatus: buildChatTypingStatus(update),
}); });

View File

@ -100,12 +100,22 @@ export interface ApiChat {
paidMessagesStars?: number; paidMessagesStars?: number;
} }
export interface ApiTypingStatus { type ApiTypingStatusBase = {
userId?: string;
action: string;
timestamp: number; timestamp: number;
emoji?: string; };
}
type ApiTypingStatusSimple = ApiTypingStatusBase & {
type: 'typing' | 'recordVideo' | 'uploadVideo' | 'recordAudio' | 'uploadAudio'
| 'uploadPhoto' | 'uploadFile' | 'playingGame' | 'recordRound' | 'uploadRound'
| 'chooseSticker' | 'chooseLocation' | 'chooseContact';
};
type ApiTypingStatusWatchingAnimations = ApiTypingStatusBase & {
type: 'watchingAnimations';
emoji: string;
};
export type ApiTypingStatus = ApiTypingStatusSimple | ApiTypingStatusWatchingAnimations;
export interface ApiChatFullInfo { export interface ApiChatFullInfo {
about?: string; about?: string;

View File

@ -145,6 +145,7 @@ export type ApiUpdateChatLeave = {
export type ApiUpdateChatTypingStatus = { export type ApiUpdateChatTypingStatus = {
'@type': 'updateChatTypingStatus'; '@type': 'updateChatTypingStatus';
id: string; id: string;
peerId: string;
threadId?: ThreadId; threadId?: ThreadId;
typingStatus: ApiTypingStatus | undefined; typingStatus: ApiTypingStatus | undefined;
}; };

View File

@ -15,18 +15,32 @@
"AccDescrGroup" = "Group"; "AccDescrGroup" = "Group";
"AccDescrChannel" = "Channel"; "AccDescrChannel" = "Channel";
"Nothing" = "Nothing"; "Nothing" = "Nothing";
"Typing" = "typing";
"UserTyping" = "{user} is typing"; "UserTyping" = "{user} is typing";
"UserTypingSeveral" = "{users} are typing";
"UserTypingMany_one" = "{user} and {count} more is typing";
"UserTypingMany_other" = "{user} and {count} more are typing";
"SendActionRecordVideo" = "recording a video"; "SendActionRecordVideo" = "recording a video";
"UserActionRecordVideo" = "{user} is recording a video";
"SendActionUploadVideo" = "sending a video"; "SendActionUploadVideo" = "sending a video";
"UserActionUploadVideo" = "{user} is sending a video";
"SendActionRecordAudio" = "recording a voice message"; "SendActionRecordAudio" = "recording a voice message";
"UserActionRecordAudio" = "{user} is recording a voice message";
"SendActionUploadAudio" = "sending a voice message"; "SendActionUploadAudio" = "sending a voice message";
"UserActionUploadAudio" = "{user} is sending a voice message";
"SendActionUploadPhoto" = "sending a photo"; "SendActionUploadPhoto" = "sending a photo";
"UserActionUploadPhoto" = "{user} is sending a photo";
"SendActionUploadFile" = "sending a file"; "SendActionUploadFile" = "sending a file";
"UserActionUploadFile" = "{user} is sending a file";
"PlayingGame" = "playing a game"; "PlayingGame" = "playing a game";
"UserPlayingGame" = "{user} is playing a game";
"SendActionRecordRound" = "recording a video message"; "SendActionRecordRound" = "recording a video message";
"UserActionRecordRound" = "{user} is recording a video message";
"SendActionUploadRound" = "sending a video message"; "SendActionUploadRound" = "sending a video message";
"SendActionChooseSticker" = "choosing a sticker"; "UserActionUploadRound" = "{user} is sending a video message";
"UserActionWatchingAnimations" = "watching {emoji}"; "SendActionChooseSticker" = "ch{eyes}sing a sticker";
"UserActionChooseSticker" = "{user} is ch{eyes}sing a sticker";
"ActionWatchingAnimations" = "watching {emoji}";
"SetUrlAvailable" = "{url} is available."; "SetUrlAvailable" = "{url} is available.";
"SetUrlInUse" = "Sorry, this link is already taken."; "SetUrlInUse" = "Sorry, this link is already taken.";
"UsernameAvailable" = "{username} is available."; "UsernameAvailable" = "{username} is available.";

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -45,7 +45,7 @@ type OwnProps = {
threadId?: ThreadId; threadId?: ThreadId;
className?: string; className?: string;
statusIcon?: IconName; statusIcon?: IconName;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
status?: string; status?: string;
withDots?: boolean; withDots?: boolean;
@ -78,7 +78,7 @@ type StateProps = {
}; };
const GroupChatInfo = ({ const GroupChatInfo = ({
typingStatus, typingStatusByPeerId,
className, className,
statusIcon, statusIcon,
avatarSize = 'medium', avatarSize = 'medium',
@ -184,8 +184,8 @@ const GroupChatInfo = ({
return undefined; return undefined;
} }
if (typingStatus) { if (typingStatusByPeerId) {
return <TypingStatus typingStatus={typingStatus} />; return <TypingStatus typingStatusByPeerId={typingStatusByPeerId} />;
} }
if (isTopic) { if (isTopic) {

View File

@ -41,7 +41,7 @@ import TypingStatus from './TypingStatus';
const TOPIC_ICON_SIZE = 2.5 * REM; const TOPIC_ICON_SIZE = 2.5 * REM;
type BaseOwnProps = { type BaseOwnProps = {
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
forceShowSelf?: boolean; forceShowSelf?: boolean;
status?: string; status?: string;
@ -97,7 +97,7 @@ const UPDATE_INTERVAL = 1000 * 60; // 1 min
const PrivateChatInfo = ({ const PrivateChatInfo = ({
userId, userId,
customPeer, customPeer,
typingStatus, typingStatusByPeerId,
avatarSize = 'medium', avatarSize = 'medium',
status, status,
statusIcon, statusIcon,
@ -204,8 +204,8 @@ const PrivateChatInfo = ({
return undefined; return undefined;
} }
if (typingStatus) { if (typingStatusByPeerId) {
return <TypingStatus typingStatus={typingStatus} />; return <TypingStatus typingStatusByPeerId={typingStatusByPeerId} isPrivate />;
} }
if (isTopic) { if (isTopic) {

View File

@ -0,0 +1,20 @@
.typingStatus {
display: flex;
gap: 0.125rem;
align-items: center;
color: var(--color-primary);
}
.typingIcon {
margin-top: -2px;
}
.eyesIcon {
display: inline-block !important;
margin-inline: -0.0625rem;
vertical-align: middle;
}
.content {
display: inline;
}

View File

@ -1,11 +0,0 @@
.typing-status {
display: flex;
align-items: baseline;
.sender-name {
&::after {
content: '\00a0';
color: var(--color-text-secondary);
}
}
}

View File

@ -1,53 +1,205 @@
import type { FC } from '../../lib/teact/teact'; import type { TeactNode } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact'; import { memo, useCallback, useMemo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiTypingStatus, ApiUser } from '../../api/types'; import type { ApiTypingStatus } from '../../api/types';
import type { GlobalState } from '../../global/types';
import type { LangFn } from '../../util/localization';
import { getUserFirstOrLastName } from '../../global/helpers'; import { getPeerTitle } from '../../global/helpers/peers';
import { selectUser } from '../../global/selectors'; import { selectPeer } from '../../global/selectors';
import renderText from './helpers/renderText'; import buildClassName from '../../util/buildClassName';
import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import { REM } from './helpers/mediaDimensions';
import useOldLang from '../../hooks/useOldLang'; import { useShallowSelector } from '../../hooks/data/useSelector';
import useLang from '../../hooks/useLang';
import DotAnimation from './DotAnimation'; import AnimatedIconWithPreview from './AnimatedIconWithPreview';
import './TypingStatus.scss'; import styles from './TypingStatus.module.scss';
type OwnProps = { type OwnProps = {
typingStatus: ApiTypingStatus; typingStatusByPeerId: Record<string, ApiTypingStatus>;
isPrivate?: boolean;
}; };
type StateProps = { const ICON_SIZE = 1.125 * REM;
typingUser?: ApiUser; const EYES_ICON_SIZE = 1.25 * REM;
};
const TypingStatus: FC<OwnProps & StateProps> = ({ typingStatus, typingUser }) => { const TypingStatus = ({ typingStatusByPeerId, isPrivate }: OwnProps) => {
const lang = useOldLang(); const lang = useLang();
const typingUserName = typingUser && !typingUser.isSelf && getUserFirstOrLastName(typingUser); const actionFallbackUser = lang('ActionFallbackUser');
const content = lang(typingStatus.action) const typingPeerIds = useMemo(() => Object.keys(typingStatusByPeerId), [typingStatusByPeerId]);
// Fix for translation "{user} is typing"
.replace('{user}', '') const sortedTypingStatuses = useMemo(
.replace('{emoji}', typingStatus.emoji || '').trim(); () => Object.entries(typingStatusByPeerId).sort(([, a], [, b]) => b.timestamp - a.timestamp),
[typingStatusByPeerId],
);
const latestTypingStatusEntry = sortedTypingStatuses[0];
const latestPeerId = latestTypingStatusEntry?.[0];
const latestTypingStatus = latestTypingStatusEntry?.[1];
const shouldRenderGroupedTyping = typingPeerIds.length >= 2;
const groupedPeersSelector = useCallback(
(global: GlobalState) => typingPeerIds.map((peerId) => selectPeer(global, peerId)),
[typingPeerIds],
);
const groupedPeers = useShallowSelector(groupedPeersSelector);
const latestPeerIndex = latestPeerId ? typingPeerIds.indexOf(latestPeerId) : -1;
const latestPeer = latestPeerIndex >= 0 ? groupedPeers[latestPeerIndex] : undefined;
const latestUserName = getTypingPeerName(lang, latestPeer, actionFallbackUser);
const groupedUserNames = useMemo(
() => typingPeerIds
.map((peerId, index) => ({
peerId,
name: getTypingPeerName(lang, groupedPeers[index], actionFallbackUser),
}))
.sort(compareTypingPeerNames)
.map(({ name }) => name),
[actionFallbackUser, groupedPeers, typingPeerIds, lang],
);
if (!latestTypingStatus) {
return undefined;
}
const user = latestUserName;
let content: string | TeactNode;
if (shouldRenderGroupedTyping) {
if (sortedTypingStatuses.length === 2) {
content = lang('UserTypingSeveral', {
users: lang.conjunction([
groupedUserNames[0] || actionFallbackUser,
groupedUserNames[1] || actionFallbackUser,
]),
}, { withNodes: true });
} else {
content = lang('UserTypingMany', {
user: groupedUserNames[0] || actionFallbackUser,
count: lang.number(sortedTypingStatuses.length - 1),
}, { withNodes: true, pluralValue: sortedTypingStatuses.length - 1 });
}
} else if (isPrivate) {
content = getPrivateTypingStatusContent(lang, latestTypingStatus);
} else {
content = getGroupTypingStatusContent(lang, latestTypingStatus, user);
}
const shouldRenderTypingStatusIcon = shouldRenderGroupedTyping || shouldRenderTypingIcon(latestTypingStatus);
return ( return (
<p className="typing-status" dir={lang.isRtl ? 'rtl' : 'auto'}> <span className={buildClassName(styles.typingStatus, 'typing-status')} dir={lang.isRtl ? 'rtl' : 'auto'}>
{typingUserName && ( {shouldRenderTypingStatusIcon && (
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span> <AnimatedIconWithPreview
className={styles.typingIcon}
tgsUrl={LOCAL_TGS_URLS.Typing}
size={ICON_SIZE}
play
noLoop={false}
shouldUseTextColor
/>
)} )}
<DotAnimation content={content} /> <span className={styles.content} dir="auto">{content}</span>
</p> </span>
); );
}; };
export default memo(withGlobal<OwnProps>( function renderEyesIcon() {
(global, { typingStatus }): Complete<StateProps> => { return (
if (!typingStatus.userId) { <AnimatedIconWithPreview
return { typingUser: undefined }; className={styles.eyesIcon}
} tgsUrl={LOCAL_TGS_URLS.Eyes}
size={EYES_ICON_SIZE}
play
noLoop={false}
shouldUseTextColor
/>
);
}
const typingUser = selectUser(global, typingStatus.userId); function getPrivateTypingStatusContent(lang: LangFn, typingStatus: ApiTypingStatus) {
switch (typingStatus.type) {
case 'recordVideo':
return lang('SendActionRecordVideo');
case 'uploadVideo':
return lang('SendActionUploadVideo');
case 'recordAudio':
return lang('SendActionRecordAudio');
case 'uploadAudio':
return lang('SendActionUploadAudio');
case 'uploadPhoto':
return lang('SendActionUploadPhoto');
case 'uploadFile':
return lang('SendActionUploadFile');
case 'playingGame':
return lang('PlayingGame');
case 'recordRound':
return lang('SendActionRecordRound');
case 'uploadRound':
return lang('SendActionUploadRound');
case 'chooseSticker':
return lang('SendActionChooseSticker', { eyes: renderEyesIcon() }, { withNodes: true });
case 'watchingAnimations':
return lang('ActionWatchingAnimations', { emoji: typingStatus.emoji });
case 'typing':
case 'chooseLocation':
case 'chooseContact':
default:
return lang('Typing');
}
}
return { typingUser }; function getGroupTypingStatusContent(lang: LangFn, typingStatus: ApiTypingStatus, user: string) {
}, switch (typingStatus.type) {
)(TypingStatus)); case 'recordVideo':
return lang('UserActionRecordVideo', { user }, { withNodes: true });
case 'uploadVideo':
return lang('UserActionUploadVideo', { user }, { withNodes: true });
case 'recordAudio':
return lang('UserActionRecordAudio', { user }, { withNodes: true });
case 'uploadAudio':
return lang('UserActionUploadAudio', { user }, { withNodes: true });
case 'uploadPhoto':
return lang('UserActionUploadPhoto', { user }, { withNodes: true });
case 'uploadFile':
return lang('UserActionUploadFile', { user }, { withNodes: true });
case 'playingGame':
return lang('UserPlayingGame', { user }, { withNodes: true });
case 'recordRound':
return lang('UserActionRecordRound', { user }, { withNodes: true });
case 'uploadRound':
return lang('UserActionUploadRound', { user }, { withNodes: true });
case 'chooseSticker':
return lang('UserActionChooseSticker', { user, eyes: renderEyesIcon() }, { withNodes: true });
case 'chooseLocation':
case 'chooseContact':
case 'typing':
default:
return lang('UserTyping', { user }, { withNodes: true });
}
}
function shouldRenderTypingIcon(typingStatus: ApiTypingStatus) {
return typingStatus.type === 'typing'
|| typingStatus.type === 'chooseLocation'
|| typingStatus.type === 'chooseContact';
}
function getTypingPeerName(lang: LangFn, peer: ReturnType<typeof selectPeer>, fallback: string) {
const title = peer ? getPeerTitle(lang, peer) : undefined;
return title || fallback;
}
function compareTypingPeerNames(
a: { peerId: string; name: string },
b: { peerId: string; name: string },
) {
return a.name.localeCompare(b.name) || a.peerId.localeCompare(b.peerId);
}
export default memo(TypingStatus);

View File

@ -209,7 +209,7 @@ const TypingWrapper = ({ formattedText, shouldAnimateMask, renderText }: OwnProp
{renderText(truncatedText)} {renderText(truncatedText)}
<span key="typing-placeholder" className={styles.placeholder}> <span key="typing-placeholder" className={styles.placeholder}>
<AnimatedIconWithPreview <AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.Typing} tgsUrl={LOCAL_TGS_URLS.Writing}
size={PLACEHOLDER_SIZE} size={PLACEHOLDER_SIZE}
play play
noLoop={false} noLoop={false}

View File

@ -20,7 +20,9 @@ import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs';
import Invite from '../../../assets/tgs/invites/Invite.tgs'; import Invite from '../../../assets/tgs/invites/Invite.tgs';
import JoinRequest from '../../../assets/tgs/invites/Requests.tgs'; import JoinRequest from '../../../assets/tgs/invites/Requests.tgs';
import LastSeen from '../../../assets/tgs/LastSeen.tgs'; import LastSeen from '../../../assets/tgs/LastSeen.tgs';
import Eyes from '../../../assets/tgs/message/Eyes.tgs';
import Typing from '../../../assets/tgs/message/Typing.tgs'; import Typing from '../../../assets/tgs/message/Typing.tgs';
import Writing from '../../../assets/tgs/message/Writing.tgs';
import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs'; import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs';
import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs'; import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs';
import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs'; import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs';
@ -98,5 +100,7 @@ export const LOCAL_TGS_URLS = {
Passkeys, Passkeys,
DuckCake, DuckCake,
HandStop, HandStop,
Writing,
Typing, Typing,
Eyes,
}; };

View File

@ -119,7 +119,7 @@ type StateProps = {
canScrollDown?: boolean; canScrollDown?: boolean;
canChangeFolder?: boolean; canChangeFolder?: boolean;
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
lastMessageId?: number; lastMessageId?: number;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
@ -158,7 +158,7 @@ const Chat: FC<OwnProps & StateProps> = ({
canScrollDown, canScrollDown,
canChangeFolder, canChangeFolder,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatusByPeerId,
lastMessageId, lastMessageId,
lastMessage, lastMessage,
isSavedDialog, isSavedDialog,
@ -231,7 +231,7 @@ const Chat: FC<OwnProps & StateProps> = ({
chat, chat,
chatId, chatId,
lastMessage, lastMessage,
typingStatus, typingStatusByPeerId,
draft, draft,
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }), statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
lastMessageTopic, lastMessageTopic,
@ -567,7 +567,7 @@ export default memo(withGlobal<OwnProps>(
const userStatus = selectUserStatus(global, chatId); const userStatus = selectUserStatus(global, chatId);
const lastMessageTopic = lastMessage && selectTopicFromMessage(global, lastMessage); const lastMessageTopic = lastMessage && selectTopicFromMessage(global, lastMessage);
const typingStatus = selectThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'typingStatus'); const typingStatusByPeerId = selectThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'typingStatusByPeerId');
const topicsInfo = selectTopicsInfo(global, chatId); const topicsInfo = selectTopicsInfo(global, chatId);
@ -593,7 +593,7 @@ export default memo(withGlobal<OwnProps>(
user, user,
userStatus, userStatus,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatusByPeerId,
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
lastMessage, lastMessage,
lastMessageId, lastMessageId,

View File

@ -71,7 +71,7 @@ type StateProps = {
lastMessageStory?: ApiTypeStory; lastMessageStory?: ApiTypeStory;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
lastMessageSender?: ApiPeer; lastMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
draft?: ApiDraft; draft?: ApiDraft;
canScrollDown?: boolean; canScrollDown?: boolean;
wasTopicOpened?: boolean; wasTopicOpened?: boolean;
@ -98,7 +98,7 @@ const Topic = ({
withInterfaceAnimations, withInterfaceAnimations,
orderDiff, orderDiff,
shiftDiff, shiftDiff,
typingStatus, typingStatusByPeerId,
draft, draft,
wasTopicOpened, wasTopicOpened,
topicIds, topicIds,
@ -153,7 +153,7 @@ const Topic = ({
lastMessageTopic: topic, lastMessageTopic: topic,
observeIntersection, observeIntersection,
isTopic: true, isTopic: true,
typingStatus, typingStatusByPeerId,
topicIds, topicIds,
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }), statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
@ -269,7 +269,7 @@ export default memo(withGlobal<OwnProps>(
? selectChatMessage(global, chatId, threadInfo.lastMessageId) : undefined; ? selectChatMessage(global, chatId, threadInfo.lastMessageId) : undefined;
const { isOutgoing } = lastMessage || {}; const { isOutgoing } = lastMessage || {};
const lastMessageSender = lastMessage && selectSender(global, lastMessage); const lastMessageSender = lastMessage && selectSender(global, lastMessage);
const typingStatus = selectThreadLocalStateParam(global, chatId, topic.id, 'typingStatus'); const typingStatusByPeerId = selectThreadLocalStateParam(global, chatId, topic.id, 'typingStatusByPeerId');
const draft = selectDraft(global, chatId, topic.id); const draft = selectDraft(global, chatId, topic.id);
const readState = selectThreadReadState(global, chatId, topic.id); const readState = selectThreadReadState(global, chatId, topic.id);
@ -289,7 +289,7 @@ export default memo(withGlobal<OwnProps>(
chat, chat,
lastMessage, lastMessage,
lastMessageSender, lastMessageSender,
typingStatus, typingStatusByPeerId,
isChatMuted, isChatMuted,
canDelete: selectCanDeleteTopic(global, chatId, topic.id), canDelete: selectCanDeleteTopic(global, chatId, topic.id),
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),

View File

@ -19,6 +19,7 @@ import {
import { getMessageSenderName } from '../../../../global/helpers/peers'; import { getMessageSenderName } from '../../../../global/helpers/peers';
import { waitStartingTransitionsEnd } from '../../../../util/animations/waitTransitionEnd'; import { waitStartingTransitionsEnd } from '../../../../util/animations/waitTransitionEnd';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { isUserId } from '../../../../util/entities/ids';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../../common/helpers/renderText';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import { ChatAnimationTypes } from './useChatAnimationType'; import { ChatAnimationTypes } from './useChatAnimationType';
@ -34,13 +35,23 @@ import Icon from '../../../common/icons/Icon';
import MessageSummary from '../../../common/MessageSummary'; import MessageSummary from '../../../common/MessageSummary';
import TypingStatus from '../../../common/TypingStatus'; import TypingStatus from '../../../common/TypingStatus';
function getLatestTypingStatusTimestamp(typingStatusByPeerId?: Record<string, ApiTypingStatus>) {
if (!typingStatusByPeerId) {
return undefined;
}
const timestamps = Object.values(typingStatusByPeerId).map(({ timestamp }) => timestamp);
return timestamps.length ? Math.max(...timestamps) : undefined;
}
export default function useChatListEntry({ export default function useChatListEntry({
chat, chat,
topicIds, topicIds,
lastMessage, lastMessage,
statefulMediaContent, statefulMediaContent,
chatId, chatId,
typingStatus, typingStatusByPeerId,
draft, draft,
lastMessageTopic, lastMessageTopic,
lastMessageSender, lastMessageSender,
@ -61,7 +72,7 @@ export default function useChatListEntry({
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
statefulMediaContent: StatefulMediaContent | undefined; statefulMediaContent: StatefulMediaContent | undefined;
chatId: string; chatId: string;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
draft?: ApiDraft; draft?: ApiDraft;
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
lastMessageSender?: ApiPeer; lastMessageSender?: ApiPeer;
@ -97,9 +108,14 @@ export default function useChatListEntry({
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage)); const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
const renderLastMessageOrTyping = useCallback(() => { const renderLastMessageOrTyping = useCallback(() => {
const latestTypingStatusTimestamp = getLatestTypingStatusTimestamp(typingStatusByPeerId);
if (!isSavedDialog && !isPreview if (!isSavedDialog && !isPreview
&& typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) { && typingStatusByPeerId && lastMessage
return <TypingStatus typingStatus={typingStatus} />; && latestTypingStatusTimestamp && latestTypingStatusTimestamp > lastMessage.date) {
return (
<TypingStatus typingStatusByPeerId={typingStatusByPeerId} isPrivate={isUserId(chatId)} />
);
} }
const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id; const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id;
@ -149,7 +165,7 @@ export default function useChatListEntry({
); );
}, [ }, [
chat, chatId, draft, isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, chat, chatId, draft, isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic,
mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview, mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatusByPeerId, isSavedDialog, isPreview,
]); ]);
function renderSubtitle() { function renderSubtitle() {

View File

@ -185,19 +185,6 @@
.status, .status,
.typing-status { .typing-status {
unicode-bidi: plaintext; unicode-bidi: plaintext;
display: inline;
@media (min-width: 1275px) {
#Main.right-column-open & {
max-width: calc(100% - var(--right-column-width));
}
}
}
.user-status {
unicode-bidi: plaintext;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: 1275px) { @media (min-width: 1275px) {
#Main.right-column-open & { #Main.right-column-open & {
@ -254,16 +241,21 @@
font-size: 1.0625rem; font-size: 1.0625rem;
} }
.status {
color: var(--color-text-secondary);
&.online {
color: var(--color-primary);
}
}
.status, .status,
.typing-status { .typing-status {
overflow: hidden; overflow: hidden;
display: inline-block;
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.125rem; line-height: 1.125rem;
color: var(--color-text-secondary);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -271,10 +263,6 @@
display: inline-flex; display: inline-flex;
} }
&.online {
color: var(--color-primary);
}
.font-emoji { .font-emoji {
line-height: 1rem; line-height: 1rem;
} }

View File

@ -75,7 +75,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
isSavedDialog?: boolean; isSavedDialog?: boolean;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
isLeftColumnShown?: boolean; isLeftColumnShown?: boolean;
isRightColumnShown?: boolean; isRightColumnShown?: boolean;
@ -96,7 +96,7 @@ const MiddleHeader = ({
threadId, threadId,
messageListType, messageListType,
isMobile, isMobile,
typingStatus, typingStatusByPeerId,
isSelectModeActive, isSelectModeActive,
isLeftColumnShown, isLeftColumnShown,
audioMessage, audioMessage,
@ -306,7 +306,7 @@ const MiddleHeader = ({
key={displayChatId} key={displayChatId}
userId={displayChatId} userId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined} threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus} typingStatusByPeerId={typingStatusByPeerId}
status={connectionStatusText || savedMessagesStatus} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
withFullInfo={threadId === MAIN_THREAD_ID} withFullInfo={threadId === MAIN_THREAD_ID}
@ -324,7 +324,7 @@ const MiddleHeader = ({
key={displayChatId} key={displayChatId}
chatId={displayChatId} chatId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined} threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus} typingStatusByPeerId={typingStatusByPeerId}
withMonoforumStatus={chat?.isMonoforum} withMonoforumStatus={chat?.isMonoforum}
status={connectionStatusText || savedMessagesStatus} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
@ -427,14 +427,14 @@ export default memo(withGlobal<OwnProps>(
messagesCount = selectThreadMessagesCount(global, chatId, threadId); messagesCount = selectThreadMessagesCount(global, chatId, threadId);
} }
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatus'); const typingStatusByPeerId = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatusByPeerId');
const emojiStatus = peer?.emojiStatus; const emojiStatus = peer?.emojiStatus;
const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId); const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId);
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
return { return {
typingStatus, typingStatusByPeerId,
isLeftColumnShown, isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),

View File

@ -43,10 +43,6 @@
content: "@"; content: "@";
} }
} }
.user-status {
display: none !important;
}
} }
@media (max-width: 600px) { @media (max-width: 600px) {

View File

@ -36,7 +36,7 @@ type StateProps = {
connectionState?: ApiUpdateConnectionStateType; connectionState?: ApiUpdateConnectionStateType;
isSyncing?: boolean; isSyncing?: boolean;
isFetchingDifference?: boolean; isFetchingDifference?: boolean;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
isSavedDialog?: boolean; isSavedDialog?: boolean;
messagesCount?: number; messagesCount?: number;
unreadCount?: number; unreadCount?: number;
@ -52,7 +52,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
connectionState, connectionState,
isSyncing, isSyncing,
isFetchingDifference, isFetchingDifference,
typingStatus, typingStatusByPeerId,
isSavedDialog, isSavedDialog,
messagesCount, messagesCount,
unreadCount, unreadCount,
@ -104,7 +104,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
<PrivateChatInfo <PrivateChatInfo
key={displayChatId} key={displayChatId}
userId={displayChatId} userId={displayChatId}
typingStatus={typingStatus} typingStatusByPeerId={typingStatusByPeerId}
status={connectionStatusText || savedMessagesStatus} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
withFullInfo={false} withFullInfo={false}
@ -120,7 +120,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
key={displayChatId} key={displayChatId}
chatId={displayChatId} chatId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined} threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus} typingStatusByPeerId={typingStatusByPeerId}
withMonoforumStatus={chat?.isMonoforum} withMonoforumStatus={chat?.isMonoforum}
status={connectionStatusText || savedMessagesStatus} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
@ -142,7 +142,12 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId }): Complete<StateProps> => { (global, { chatId, threadId }): Complete<StateProps> => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId || MAIN_THREAD_ID, 'typingStatus'); const typingStatusByPeerId = selectThreadLocalStateParam(
global,
chatId,
threadId || MAIN_THREAD_ID,
'typingStatusByPeerId',
);
const isSavedDialog = getIsSavedDialog(chatId, threadId || MAIN_THREAD_ID, global.currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId || MAIN_THREAD_ID, global.currentUserId);
const messagesCount = isSavedDialog && threadId const messagesCount = isSavedDialog && threadId
? selectThreadMessagesCount(global, chatId, threadId) ? selectThreadMessagesCount(global, chatId, threadId)
@ -155,7 +160,7 @@ export default memo(withGlobal<OwnProps>(
connectionState: global.connectionState, connectionState: global.connectionState,
isSyncing: global.isSyncing, isSyncing: global.isSyncing,
isFetchingDifference: global.isFetchingDifference, isFetchingDifference: global.isFetchingDifference,
typingStatus, typingStatusByPeerId,
isSavedDialog, isSavedDialog,
messagesCount, messagesCount,
unreadCount, unreadCount,

View File

@ -110,8 +110,6 @@
} }
} }
.user-status,
.group-status,
.title, .title,
.other-usernames, .other-usernames,
.subtitle { .subtitle {
@ -325,27 +323,24 @@
white-space: nowrap; white-space: nowrap;
} }
.status {
color: var(--color-text-secondary);
&.online {
color: var(--color-primary);
}
}
.status, .status,
.typing-status { .typing-status {
display: inline-block; display: inline-block;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
color: var(--color-text-secondary);
&.online {
color: var(--color-primary);
}
&[dir="rtl"], &[dir="rtl"],
&[dir="auto"] { &[dir="auto"] {
width: 100%; width: 100%;
text-align: initial; text-align: initial;
} }
.group-status:only-child,
.user-status:only-child {
display: flow-root;
}
} }
} }

View File

@ -159,17 +159,53 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
case 'updateChatTypingStatus': { case 'updateChatTypingStatus': {
const { id, threadId = MAIN_THREAD_ID, typingStatus } = update; const {
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', typingStatus); id, threadId = MAIN_THREAD_ID, typingStatus, peerId,
} = update;
const currentTypingStatusByPeerId = selectThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId');
if (!typingStatus) {
if (!currentTypingStatusByPeerId?.[peerId]) {
return undefined;
}
const nextTypingStatusByPeerId = omit(currentTypingStatusByPeerId, [peerId]);
global = replaceThreadLocalStateParam(
global,
id,
threadId,
'typingStatusByPeerId',
Object.keys(nextTypingStatusByPeerId).length ? nextTypingStatusByPeerId : undefined,
);
setGlobal(global);
return undefined;
}
const updatedTypingStatusByPeerId = currentTypingStatusByPeerId
? { ...currentTypingStatusByPeerId, [peerId]: typingStatus }
: { [peerId]: typingStatus };
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId', updatedTypingStatusByPeerId);
setGlobal(global); setGlobal(global);
setTimeout(() => { setTimeout(() => {
global = getGlobal(); global = getGlobal();
const currentTypingStatus = selectThreadLocalStateParam(global, id, threadId, 'typingStatus'); const actualTypingStatusByPeerId = selectThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId');
if (typingStatus && currentTypingStatus && typingStatus.timestamp === currentTypingStatus.timestamp) { const currentTypingStatus = actualTypingStatusByPeerId?.[peerId];
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', undefined);
setGlobal(global); if (!currentTypingStatus || typingStatus.timestamp !== currentTypingStatus.timestamp) {
return;
} }
const nextTypingStatusByPeerId = omit(actualTypingStatusByPeerId, [peerId]);
global = replaceThreadLocalStateParam(
global,
id,
threadId,
'typingStatusByPeerId',
Object.keys(nextTypingStatusByPeerId).length ? nextTypingStatusByPeerId : undefined,
);
setGlobal(global);
}, TYPING_STATUS_CLEAR_DELAY); }, TYPING_STATUS_CLEAR_DELAY);
return undefined; return undefined;

View File

@ -720,7 +720,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
localState: { localState: {
...thread.localState, ...thread.localState,
listedIds: thread.localState?.lastViewportIds, listedIds: thread.localState?.lastViewportIds,
typingStatus: undefined, typingStatusByPeerId: undefined,
}, },
}; };
return acc; return acc;

View File

@ -653,7 +653,7 @@ export interface ThreadLocalState {
noWebPage?: boolean; noWebPage?: boolean;
typingStatus?: ApiTypingStatus; typingStatusByPeerId?: Record<string, ApiTypingStatus>;
typingDraftIdByRandomId?: Record<string, number>; typingDraftIdByRandomId?: Record<string, number>;
} }

View File

@ -18,6 +18,7 @@ export interface LangPair {
'AccDescrGroup': undefined; 'AccDescrGroup': undefined;
'AccDescrChannel': undefined; 'AccDescrChannel': undefined;
'Nothing': undefined; 'Nothing': undefined;
'Typing': undefined;
'SendActionRecordVideo': undefined; 'SendActionRecordVideo': undefined;
'SendActionUploadVideo': undefined; 'SendActionUploadVideo': undefined;
'SendActionRecordAudio': undefined; 'SendActionRecordAudio': undefined;
@ -27,7 +28,6 @@ export interface LangPair {
'PlayingGame': undefined; 'PlayingGame': undefined;
'SendActionRecordRound': undefined; 'SendActionRecordRound': undefined;
'SendActionUploadRound': undefined; 'SendActionUploadRound': undefined;
'SendActionChooseSticker': undefined;
'SetUrlInUse': undefined; 'SetUrlInUse': undefined;
'UsernameInUse': undefined; 'UsernameInUse': undefined;
'CreateGroupError': undefined; 'CreateGroupError': undefined;
@ -2111,7 +2111,44 @@ export interface LangPairWithVariables<V = LangVariable> {
'UserTyping': { 'UserTyping': {
'user': V; 'user': V;
}; };
'UserActionWatchingAnimations': { 'UserTypingSeveral': {
'users': V;
};
'UserActionRecordVideo': {
'user': V;
};
'UserActionUploadVideo': {
'user': V;
};
'UserActionRecordAudio': {
'user': V;
};
'UserActionUploadAudio': {
'user': V;
};
'UserActionUploadPhoto': {
'user': V;
};
'UserActionUploadFile': {
'user': V;
};
'UserPlayingGame': {
'user': V;
};
'UserActionRecordRound': {
'user': V;
};
'UserActionUploadRound': {
'user': V;
};
'SendActionChooseSticker': {
'eyes': V;
};
'UserActionChooseSticker': {
'user': V;
'eyes': V;
};
'ActionWatchingAnimations': {
'emoji': V; 'emoji': V;
}; };
'SetUrlAvailable': { 'SetUrlAvailable': {
@ -3703,6 +3740,10 @@ export interface LangPairPlural {
} }
export interface LangPairPluralWithVariables<V = LangVariable> { export interface LangPairPluralWithVariables<V = LangVariable> {
'UserTypingMany': {
'user': V;
'count': V;
};
'Participants': { 'Participants': {
'count': V; 'count': V;
}; };