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 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 {
|
||||||
|
|||||||
@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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.";
|
||||||
|
|||||||
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;
|
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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
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 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);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -43,10 +43,6 @@
|
|||||||
content: "@";
|
content: "@";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-status {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/types/language.d.ts
vendored
45
src/types/language.d.ts
vendored
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user