Middle Header: Update typing status lang strings (#6854)
This commit is contained in:
parent
881c09acdf
commit
230e9797d4
@ -22,12 +22,13 @@ import {
|
||||
type ApiSponsoredPeer,
|
||||
type ApiStarsSubscriptionPricing,
|
||||
type ApiThreadInfo,
|
||||
type ApiTypingStatus,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../types';
|
||||
|
||||
import { omitUndefined, pickTruthy } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
|
||||
import { serializeBytes } from '../helpers/misc';
|
||||
import {
|
||||
@ -382,52 +383,73 @@ export function buildChatMembers(
|
||||
|
||||
export function buildChatTypingStatus(
|
||||
update: GramJs.UpdateUserTyping | GramJs.UpdateChatUserTyping | GramJs.UpdateChannelUserTyping,
|
||||
) {
|
||||
let action: string = '';
|
||||
let emoticon: string | undefined;
|
||||
if (update.action instanceof GramJs.SendMessageCancelAction) {
|
||||
): ApiTypingStatus | undefined {
|
||||
const action = update.action;
|
||||
const timestamp = getServerTime();
|
||||
const buildTypingStatus = (
|
||||
type: Exclude<ApiTypingStatus['type'], 'watchingAnimations'>,
|
||||
): ApiTypingStatus => ({
|
||||
timestamp,
|
||||
type,
|
||||
});
|
||||
|
||||
if (action instanceof GramJs.SendMessageCancelAction) {
|
||||
return undefined;
|
||||
} else if (update.action instanceof GramJs.SendMessageTypingAction) {
|
||||
action = 'lng_user_typing';
|
||||
} else if (update.action instanceof GramJs.SendMessageRecordVideoAction) {
|
||||
action = 'lng_send_action_record_video';
|
||||
} else if (update.action instanceof GramJs.SendMessageUploadVideoAction) {
|
||||
action = 'lng_send_action_upload_video';
|
||||
} else if (update.action instanceof GramJs.SendMessageRecordAudioAction) {
|
||||
action = 'lng_send_action_record_audio';
|
||||
} else if (update.action instanceof GramJs.SendMessageUploadAudioAction) {
|
||||
action = 'lng_send_action_upload_audio';
|
||||
} else if (update.action instanceof GramJs.SendMessageUploadPhotoAction) {
|
||||
action = 'lng_send_action_upload_photo';
|
||||
} else if (update.action instanceof GramJs.SendMessageUploadDocumentAction) {
|
||||
action = 'lng_send_action_upload_file';
|
||||
} else if (update.action instanceof GramJs.SendMessageGeoLocationAction) {
|
||||
action = 'selecting a location to share';
|
||||
} else if (update.action instanceof GramJs.SendMessageChooseContactAction) {
|
||||
action = 'selecting a contact to share';
|
||||
} else if (update.action instanceof GramJs.SendMessageGamePlayAction) {
|
||||
action = 'lng_playing_game';
|
||||
} else if (update.action instanceof GramJs.SendMessageRecordRoundAction) {
|
||||
action = 'lng_send_action_record_round';
|
||||
} else if (update.action instanceof GramJs.SendMessageUploadRoundAction) {
|
||||
action = 'lng_send_action_upload_round';
|
||||
} else if (update.action instanceof GramJs.SendMessageChooseStickerAction) {
|
||||
action = 'lng_send_action_choose_sticker';
|
||||
} else if (update.action instanceof GramJs.SpeakingInGroupCallAction) {
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageTypingAction) {
|
||||
return buildTypingStatus('typing');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageRecordVideoAction) {
|
||||
return buildTypingStatus('recordVideo');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageUploadVideoAction) {
|
||||
return buildTypingStatus('uploadVideo');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageRecordAudioAction) {
|
||||
return buildTypingStatus('recordAudio');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageUploadAudioAction) {
|
||||
return buildTypingStatus('uploadAudio');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageUploadPhotoAction) {
|
||||
return buildTypingStatus('uploadPhoto');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageUploadDocumentAction) {
|
||||
return buildTypingStatus('uploadFile');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageGeoLocationAction) {
|
||||
return buildTypingStatus('chooseLocation');
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageChooseContactAction) {
|
||||
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;
|
||||
} else if (update.action instanceof GramJs.SendMessageEmojiInteractionSeen) {
|
||||
action = 'lng_user_action_watching_animations';
|
||||
emoticon = update.action.emoticon;
|
||||
} else if (update.action instanceof GramJs.SendMessageEmojiInteraction) {
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageEmojiInteractionSeen) {
|
||||
return {
|
||||
timestamp,
|
||||
type: 'watchingAnimations',
|
||||
emoji: action.emoticon,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.SendMessageEmojiInteraction) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
action,
|
||||
...(emoticon && { emoji: emoticon }),
|
||||
...(!(update instanceof GramJs.UpdateUserTyping) && { userId: getApiChatIdFromMtpPeer(update.fromId) }),
|
||||
timestamp: Date.now() + getServerTimeOffset() * 1000,
|
||||
};
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFilterChatlist): ApiChatFolder {
|
||||
|
||||
@ -677,6 +677,9 @@ export function updater(update: Update) {
|
||||
const chatId = update instanceof GramJs.UpdateUserTyping
|
||||
? buildApiPeerId(update.userId, 'user')
|
||||
: 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;
|
||||
|
||||
@ -700,16 +703,19 @@ export function updater(update: Update) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChatTypingStatus',
|
||||
id: chatId,
|
||||
peerId,
|
||||
threadId,
|
||||
typingStatus: buildChatTypingStatus(update),
|
||||
});
|
||||
}
|
||||
} else if (update instanceof GramJs.UpdateChannelUserTyping) {
|
||||
const id = buildApiPeerId(update.channelId, 'channel');
|
||||
const peerId = getApiChatIdFromMtpPeer(update.fromId);
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChatTypingStatus',
|
||||
id,
|
||||
peerId,
|
||||
threadId: update.topMsgId,
|
||||
typingStatus: buildChatTypingStatus(update),
|
||||
});
|
||||
|
||||
@ -100,12 +100,22 @@ export interface ApiChat {
|
||||
paidMessagesStars?: number;
|
||||
}
|
||||
|
||||
export interface ApiTypingStatus {
|
||||
userId?: string;
|
||||
action: string;
|
||||
type ApiTypingStatusBase = {
|
||||
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 {
|
||||
about?: string;
|
||||
|
||||
@ -145,6 +145,7 @@ export type ApiUpdateChatLeave = {
|
||||
export type ApiUpdateChatTypingStatus = {
|
||||
'@type': 'updateChatTypingStatus';
|
||||
id: string;
|
||||
peerId: string;
|
||||
threadId?: ThreadId;
|
||||
typingStatus: ApiTypingStatus | undefined;
|
||||
};
|
||||
|
||||
@ -15,18 +15,32 @@
|
||||
"AccDescrGroup" = "Group";
|
||||
"AccDescrChannel" = "Channel";
|
||||
"Nothing" = "Nothing";
|
||||
"Typing" = "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";
|
||||
"UserActionRecordVideo" = "{user} is recording a video";
|
||||
"SendActionUploadVideo" = "sending a video";
|
||||
"UserActionUploadVideo" = "{user} is sending a video";
|
||||
"SendActionRecordAudio" = "recording a voice message";
|
||||
"UserActionRecordAudio" = "{user} is recording a voice message";
|
||||
"SendActionUploadAudio" = "sending a voice message";
|
||||
"UserActionUploadAudio" = "{user} is sending a voice message";
|
||||
"SendActionUploadPhoto" = "sending a photo";
|
||||
"UserActionUploadPhoto" = "{user} is sending a photo";
|
||||
"SendActionUploadFile" = "sending a file";
|
||||
"UserActionUploadFile" = "{user} is sending a file";
|
||||
"PlayingGame" = "playing a game";
|
||||
"UserPlayingGame" = "{user} is playing a game";
|
||||
"SendActionRecordRound" = "recording a video message";
|
||||
"UserActionRecordRound" = "{user} is recording a video message";
|
||||
"SendActionUploadRound" = "sending a video message";
|
||||
"SendActionChooseSticker" = "choosing a sticker";
|
||||
"UserActionWatchingAnimations" = "watching {emoji}";
|
||||
"UserActionUploadRound" = "{user} is sending a video message";
|
||||
"SendActionChooseSticker" = "ch{eyes}sing a sticker";
|
||||
"UserActionChooseSticker" = "{user} is ch{eyes}sing a sticker";
|
||||
"ActionWatchingAnimations" = "watching {emoji}";
|
||||
"SetUrlAvailable" = "{url} is available.";
|
||||
"SetUrlInUse" = "Sorry, this link is already taken.";
|
||||
"UsernameAvailable" = "{username} is available.";
|
||||
|
||||
BIN
src/assets/tgs/message/Eyes.tgs
Normal file
BIN
src/assets/tgs/message/Eyes.tgs
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/assets/tgs/message/Writing.tgs
Normal file
BIN
src/assets/tgs/message/Writing.tgs
Normal file
Binary file not shown.
@ -45,7 +45,7 @@ type OwnProps = {
|
||||
threadId?: ThreadId;
|
||||
className?: string;
|
||||
statusIcon?: IconName;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||
status?: string;
|
||||
withDots?: boolean;
|
||||
@ -78,7 +78,7 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const GroupChatInfo = ({
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
className,
|
||||
statusIcon,
|
||||
avatarSize = 'medium',
|
||||
@ -184,8 +184,8 @@ const GroupChatInfo = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typingStatus) {
|
||||
return <TypingStatus typingStatus={typingStatus} />;
|
||||
if (typingStatusByPeerId) {
|
||||
return <TypingStatus typingStatusByPeerId={typingStatusByPeerId} />;
|
||||
}
|
||||
|
||||
if (isTopic) {
|
||||
|
||||
@ -41,7 +41,7 @@ import TypingStatus from './TypingStatus';
|
||||
const TOPIC_ICON_SIZE = 2.5 * REM;
|
||||
|
||||
type BaseOwnProps = {
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||
forceShowSelf?: boolean;
|
||||
status?: string;
|
||||
@ -97,7 +97,7 @@ const UPDATE_INTERVAL = 1000 * 60; // 1 min
|
||||
const PrivateChatInfo = ({
|
||||
userId,
|
||||
customPeer,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
avatarSize = 'medium',
|
||||
status,
|
||||
statusIcon,
|
||||
@ -204,8 +204,8 @@ const PrivateChatInfo = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typingStatus) {
|
||||
return <TypingStatus typingStatus={typingStatus} />;
|
||||
if (typingStatusByPeerId) {
|
||||
return <TypingStatus typingStatusByPeerId={typingStatusByPeerId} isPrivate />;
|
||||
}
|
||||
|
||||
if (isTopic) {
|
||||
|
||||
20
src/components/common/TypingStatus.module.scss
Normal file
20
src/components/common/TypingStatus.module.scss
Normal 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;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
.typing-status {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.sender-name {
|
||||
&::after {
|
||||
content: '\00a0';
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,53 +1,205 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
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 { selectUser } from '../../global/selectors';
|
||||
import renderText from './helpers/renderText';
|
||||
import { getPeerTitle } from '../../global/helpers/peers';
|
||||
import { selectPeer } from '../../global/selectors';
|
||||
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 = {
|
||||
typingStatus: ApiTypingStatus;
|
||||
typingStatusByPeerId: Record<string, ApiTypingStatus>;
|
||||
isPrivate?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
typingUser?: ApiUser;
|
||||
};
|
||||
const ICON_SIZE = 1.125 * REM;
|
||||
const EYES_ICON_SIZE = 1.25 * REM;
|
||||
|
||||
const TypingStatus: FC<OwnProps & StateProps> = ({ typingStatus, typingUser }) => {
|
||||
const lang = useOldLang();
|
||||
const typingUserName = typingUser && !typingUser.isSelf && getUserFirstOrLastName(typingUser);
|
||||
const content = lang(typingStatus.action)
|
||||
// Fix for translation "{user} is typing"
|
||||
.replace('{user}', '')
|
||||
.replace('{emoji}', typingStatus.emoji || '').trim();
|
||||
const TypingStatus = ({ typingStatusByPeerId, isPrivate }: OwnProps) => {
|
||||
const lang = useLang();
|
||||
const actionFallbackUser = lang('ActionFallbackUser');
|
||||
const typingPeerIds = useMemo(() => Object.keys(typingStatusByPeerId), [typingStatusByPeerId]);
|
||||
|
||||
const sortedTypingStatuses = useMemo(
|
||||
() => 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 (
|
||||
<p className="typing-status" dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||
{typingUserName && (
|
||||
<span className="sender-name" dir="auto">{renderText(typingUserName)}</span>
|
||||
<span className={buildClassName(styles.typingStatus, 'typing-status')} dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||
{shouldRenderTypingStatusIcon && (
|
||||
<AnimatedIconWithPreview
|
||||
className={styles.typingIcon}
|
||||
tgsUrl={LOCAL_TGS_URLS.Typing}
|
||||
size={ICON_SIZE}
|
||||
play
|
||||
noLoop={false}
|
||||
shouldUseTextColor
|
||||
/>
|
||||
)}
|
||||
<DotAnimation content={content} />
|
||||
</p>
|
||||
<span className={styles.content} dir="auto">{content}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { typingStatus }): Complete<StateProps> => {
|
||||
if (!typingStatus.userId) {
|
||||
return { typingUser: undefined };
|
||||
}
|
||||
function renderEyesIcon() {
|
||||
return (
|
||||
<AnimatedIconWithPreview
|
||||
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 };
|
||||
},
|
||||
)(TypingStatus));
|
||||
function getGroupTypingStatusContent(lang: LangFn, typingStatus: ApiTypingStatus, user: string) {
|
||||
switch (typingStatus.type) {
|
||||
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);
|
||||
|
||||
@ -209,7 +209,7 @@ const TypingWrapper = ({ formattedText, shouldAnimateMask, renderText }: OwnProp
|
||||
{renderText(truncatedText)}
|
||||
<span key="typing-placeholder" className={styles.placeholder}>
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={LOCAL_TGS_URLS.Typing}
|
||||
tgsUrl={LOCAL_TGS_URLS.Writing}
|
||||
size={PLACEHOLDER_SIZE}
|
||||
play
|
||||
noLoop={false}
|
||||
|
||||
@ -20,7 +20,9 @@ import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs';
|
||||
import Invite from '../../../assets/tgs/invites/Invite.tgs';
|
||||
import JoinRequest from '../../../assets/tgs/invites/Requests.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 Writing from '../../../assets/tgs/message/Writing.tgs';
|
||||
import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs';
|
||||
import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs';
|
||||
import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs';
|
||||
@ -98,5 +100,7 @@ export const LOCAL_TGS_URLS = {
|
||||
Passkeys,
|
||||
DuckCake,
|
||||
HandStop,
|
||||
Writing,
|
||||
Typing,
|
||||
Eyes,
|
||||
};
|
||||
|
||||
@ -119,7 +119,7 @@ type StateProps = {
|
||||
canScrollDown?: boolean;
|
||||
canChangeFolder?: boolean;
|
||||
lastMessageTopic?: ApiTopic;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
withInterfaceAnimations?: boolean;
|
||||
lastMessageId?: number;
|
||||
lastMessage?: ApiMessage;
|
||||
@ -158,7 +158,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
canScrollDown,
|
||||
canChangeFolder,
|
||||
lastMessageTopic,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
lastMessageId,
|
||||
lastMessage,
|
||||
isSavedDialog,
|
||||
@ -231,7 +231,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
chatId,
|
||||
lastMessage,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
draft,
|
||||
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
|
||||
lastMessageTopic,
|
||||
@ -567,7 +567,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const userStatus = selectUserStatus(global, chatId);
|
||||
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);
|
||||
|
||||
@ -593,7 +593,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
user,
|
||||
userStatus,
|
||||
lastMessageTopic,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
withInterfaceAnimations: selectCanAnimateInterface(global),
|
||||
lastMessage,
|
||||
lastMessageId,
|
||||
|
||||
@ -71,7 +71,7 @@ type StateProps = {
|
||||
lastMessageStory?: ApiTypeStory;
|
||||
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
||||
lastMessageSender?: ApiPeer;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
draft?: ApiDraft;
|
||||
canScrollDown?: boolean;
|
||||
wasTopicOpened?: boolean;
|
||||
@ -98,7 +98,7 @@ const Topic = ({
|
||||
withInterfaceAnimations,
|
||||
orderDiff,
|
||||
shiftDiff,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
draft,
|
||||
wasTopicOpened,
|
||||
topicIds,
|
||||
@ -153,7 +153,7 @@ const Topic = ({
|
||||
lastMessageTopic: topic,
|
||||
observeIntersection,
|
||||
isTopic: true,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
topicIds,
|
||||
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
|
||||
|
||||
@ -269,7 +269,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
? selectChatMessage(global, chatId, threadInfo.lastMessageId) : undefined;
|
||||
const { isOutgoing } = 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 readState = selectThreadReadState(global, chatId, topic.id);
|
||||
@ -289,7 +289,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
chat,
|
||||
lastMessage,
|
||||
lastMessageSender,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
isChatMuted,
|
||||
canDelete: selectCanDeleteTopic(global, chatId, topic.id),
|
||||
withInterfaceAnimations: selectCanAnimateInterface(global),
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
import { getMessageSenderName } from '../../../../global/helpers/peers';
|
||||
import { waitStartingTransitionsEnd } from '../../../../util/animations/waitTransitionEnd';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { isUserId } from '../../../../util/entities/ids';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
import { ChatAnimationTypes } from './useChatAnimationType';
|
||||
@ -34,13 +35,23 @@ import Icon from '../../../common/icons/Icon';
|
||||
import MessageSummary from '../../../common/MessageSummary';
|
||||
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({
|
||||
chat,
|
||||
topicIds,
|
||||
lastMessage,
|
||||
statefulMediaContent,
|
||||
chatId,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
draft,
|
||||
lastMessageTopic,
|
||||
lastMessageSender,
|
||||
@ -61,7 +72,7 @@ export default function useChatListEntry({
|
||||
lastMessage?: ApiMessage;
|
||||
statefulMediaContent: StatefulMediaContent | undefined;
|
||||
chatId: string;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
draft?: ApiDraft;
|
||||
lastMessageTopic?: ApiTopic;
|
||||
lastMessageSender?: ApiPeer;
|
||||
@ -97,9 +108,14 @@ export default function useChatListEntry({
|
||||
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
||||
|
||||
const renderLastMessageOrTyping = useCallback(() => {
|
||||
const latestTypingStatusTimestamp = getLatestTypingStatusTimestamp(typingStatusByPeerId);
|
||||
|
||||
if (!isSavedDialog && !isPreview
|
||||
&& typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
|
||||
return <TypingStatus typingStatus={typingStatus} />;
|
||||
&& typingStatusByPeerId && lastMessage
|
||||
&& latestTypingStatusTimestamp && latestTypingStatusTimestamp > lastMessage.date) {
|
||||
return (
|
||||
<TypingStatus typingStatusByPeerId={typingStatusByPeerId} isPrivate={isUserId(chatId)} />
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview,
|
||||
mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatusByPeerId, isSavedDialog, isPreview,
|
||||
]);
|
||||
|
||||
function renderSubtitle() {
|
||||
|
||||
@ -185,19 +185,6 @@
|
||||
.status,
|
||||
.typing-status {
|
||||
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) {
|
||||
#Main.right-column-open & {
|
||||
@ -254,16 +241,21 @@
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--color-text-secondary);
|
||||
&.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.status,
|
||||
.typing-status {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
margin: 0;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.125rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -271,10 +263,6 @@
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.font-emoji {
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isSavedDialog?: boolean;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
isSelectModeActive?: boolean;
|
||||
isLeftColumnShown?: boolean;
|
||||
isRightColumnShown?: boolean;
|
||||
@ -96,7 +96,7 @@ const MiddleHeader = ({
|
||||
threadId,
|
||||
messageListType,
|
||||
isMobile,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
isSelectModeActive,
|
||||
isLeftColumnShown,
|
||||
audioMessage,
|
||||
@ -306,7 +306,7 @@ const MiddleHeader = ({
|
||||
key={displayChatId}
|
||||
userId={displayChatId}
|
||||
threadId={!isSavedDialog ? threadId : undefined}
|
||||
typingStatus={typingStatus}
|
||||
typingStatusByPeerId={typingStatusByPeerId}
|
||||
status={connectionStatusText || savedMessagesStatus}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
withFullInfo={threadId === MAIN_THREAD_ID}
|
||||
@ -324,7 +324,7 @@ const MiddleHeader = ({
|
||||
key={displayChatId}
|
||||
chatId={displayChatId}
|
||||
threadId={!isSavedDialog ? threadId : undefined}
|
||||
typingStatus={typingStatus}
|
||||
typingStatusByPeerId={typingStatusByPeerId}
|
||||
withMonoforumStatus={chat?.isMonoforum}
|
||||
status={connectionStatusText || savedMessagesStatus}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
@ -427,14 +427,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
messagesCount = selectThreadMessagesCount(global, chatId, threadId);
|
||||
}
|
||||
|
||||
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatus');
|
||||
const typingStatusByPeerId = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatusByPeerId');
|
||||
|
||||
const emojiStatus = peer?.emojiStatus;
|
||||
const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId);
|
||||
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
|
||||
|
||||
return {
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
isLeftColumnShown,
|
||||
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
||||
isSelectModeActive: selectIsInSelectMode(global),
|
||||
|
||||
@ -43,10 +43,6 @@
|
||||
content: "@";
|
||||
}
|
||||
}
|
||||
|
||||
.user-status {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
@ -36,7 +36,7 @@ type StateProps = {
|
||||
connectionState?: ApiUpdateConnectionStateType;
|
||||
isSyncing?: boolean;
|
||||
isFetchingDifference?: boolean;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
isSavedDialog?: boolean;
|
||||
messagesCount?: number;
|
||||
unreadCount?: number;
|
||||
@ -52,7 +52,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
connectionState,
|
||||
isSyncing,
|
||||
isFetchingDifference,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
isSavedDialog,
|
||||
messagesCount,
|
||||
unreadCount,
|
||||
@ -104,7 +104,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
<PrivateChatInfo
|
||||
key={displayChatId}
|
||||
userId={displayChatId}
|
||||
typingStatus={typingStatus}
|
||||
typingStatusByPeerId={typingStatusByPeerId}
|
||||
status={connectionStatusText || savedMessagesStatus}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
withFullInfo={false}
|
||||
@ -120,7 +120,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
key={displayChatId}
|
||||
chatId={displayChatId}
|
||||
threadId={!isSavedDialog ? threadId : undefined}
|
||||
typingStatus={typingStatus}
|
||||
typingStatusByPeerId={typingStatusByPeerId}
|
||||
withMonoforumStatus={chat?.isMonoforum}
|
||||
status={connectionStatusText || savedMessagesStatus}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
@ -142,7 +142,12 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, threadId }): Complete<StateProps> => {
|
||||
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 messagesCount = isSavedDialog && threadId
|
||||
? selectThreadMessagesCount(global, chatId, threadId)
|
||||
@ -155,7 +160,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
connectionState: global.connectionState,
|
||||
isSyncing: global.isSyncing,
|
||||
isFetchingDifference: global.isFetchingDifference,
|
||||
typingStatus,
|
||||
typingStatusByPeerId,
|
||||
isSavedDialog,
|
||||
messagesCount,
|
||||
unreadCount,
|
||||
|
||||
@ -110,8 +110,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.user-status,
|
||||
.group-status,
|
||||
.title,
|
||||
.other-usernames,
|
||||
.subtitle {
|
||||
@ -325,27 +323,24 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--color-text-secondary);
|
||||
&.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.status,
|
||||
.typing-status {
|
||||
display: inline-block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
&.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&[dir="rtl"],
|
||||
&[dir="auto"] {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.group-status:only-child,
|
||||
.user-status:only-child {
|
||||
display: flow-root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -159,17 +159,53 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'updateChatTypingStatus': {
|
||||
const { id, threadId = MAIN_THREAD_ID, typingStatus } = update;
|
||||
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', typingStatus);
|
||||
const {
|
||||
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);
|
||||
|
||||
setTimeout(() => {
|
||||
global = getGlobal();
|
||||
const currentTypingStatus = selectThreadLocalStateParam(global, id, threadId, 'typingStatus');
|
||||
if (typingStatus && currentTypingStatus && typingStatus.timestamp === currentTypingStatus.timestamp) {
|
||||
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', undefined);
|
||||
setGlobal(global);
|
||||
const actualTypingStatusByPeerId = selectThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId');
|
||||
const currentTypingStatus = actualTypingStatusByPeerId?.[peerId];
|
||||
|
||||
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);
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -720,7 +720,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
localState: {
|
||||
...thread.localState,
|
||||
listedIds: thread.localState?.lastViewportIds,
|
||||
typingStatus: undefined,
|
||||
typingStatusByPeerId: undefined,
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
|
||||
@ -653,7 +653,7 @@ export interface ThreadLocalState {
|
||||
|
||||
noWebPage?: boolean;
|
||||
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingStatusByPeerId?: Record<string, ApiTypingStatus>;
|
||||
|
||||
typingDraftIdByRandomId?: Record<string, number>;
|
||||
}
|
||||
|
||||
45
src/types/language.d.ts
vendored
45
src/types/language.d.ts
vendored
@ -18,6 +18,7 @@ export interface LangPair {
|
||||
'AccDescrGroup': undefined;
|
||||
'AccDescrChannel': undefined;
|
||||
'Nothing': undefined;
|
||||
'Typing': undefined;
|
||||
'SendActionRecordVideo': undefined;
|
||||
'SendActionUploadVideo': undefined;
|
||||
'SendActionRecordAudio': undefined;
|
||||
@ -27,7 +28,6 @@ export interface LangPair {
|
||||
'PlayingGame': undefined;
|
||||
'SendActionRecordRound': undefined;
|
||||
'SendActionUploadRound': undefined;
|
||||
'SendActionChooseSticker': undefined;
|
||||
'SetUrlInUse': undefined;
|
||||
'UsernameInUse': undefined;
|
||||
'CreateGroupError': undefined;
|
||||
@ -2111,7 +2111,44 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'UserTyping': {
|
||||
'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;
|
||||
};
|
||||
'SetUrlAvailable': {
|
||||
@ -3703,6 +3740,10 @@ export interface LangPairPlural {
|
||||
}
|
||||
|
||||
export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'UserTypingMany': {
|
||||
'user': V;
|
||||
'count': V;
|
||||
};
|
||||
'Participants': {
|
||||
'count': V;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user