367 lines
10 KiB
TypeScript
367 lines
10 KiB
TypeScript
import { memo, useEffect, useMemo } from '../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../global';
|
|
|
|
import type {
|
|
ApiChatMember, ApiTopic, ApiTypingStatus, ApiUser, ApiUserStatus,
|
|
} from '../../api/types';
|
|
import type { CustomPeer, StoryViewerOrigin, ThreadId } from '../../types';
|
|
import type { IconName } from '../../types/icons';
|
|
import { MediaViewerOrigin } from '../../types';
|
|
|
|
import {
|
|
getMainUsername, getUserStatus, isSystemBot, isUserOnline,
|
|
} from '../../global/helpers';
|
|
import {
|
|
selectChatMessages,
|
|
selectTopic,
|
|
selectUser,
|
|
selectUserStatus,
|
|
} from '../../global/selectors';
|
|
import { selectThreadMessagesCount } from '../../global/selectors/threads';
|
|
import buildClassName from '../../util/buildClassName';
|
|
import { hasRank } from './helpers/chatMember';
|
|
import { REM } from './helpers/mediaDimensions';
|
|
import renderText from './helpers/renderText';
|
|
|
|
import useIntervalForceUpdate from '../../hooks/schedulers/useIntervalForceUpdate';
|
|
import useLang from '../../hooks/useLang';
|
|
import useLastCallback from '../../hooks/useLastCallback';
|
|
import useOldLang from '../../hooks/useOldLang';
|
|
|
|
import RippleEffect from '../ui/RippleEffect';
|
|
import Transition from '../ui/Transition';
|
|
import Avatar from './Avatar';
|
|
import DotAnimation from './DotAnimation';
|
|
import FullNameTitle from './FullNameTitle';
|
|
import Icon from './icons/Icon';
|
|
import RankBadge from './RankBadge';
|
|
import TopicIcon from './TopicIcon';
|
|
import TypingStatus from './TypingStatus';
|
|
|
|
const TOPIC_ICON_SIZE = 2.5 * REM;
|
|
|
|
type BaseOwnProps = {
|
|
typingStatus?: ApiTypingStatus;
|
|
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
|
forceShowSelf?: boolean;
|
|
status?: string;
|
|
statusIcon?: IconName;
|
|
ripple?: boolean;
|
|
withDots?: boolean;
|
|
withMediaViewer?: boolean;
|
|
withUsername?: boolean;
|
|
withStory?: boolean;
|
|
withFullInfo?: boolean;
|
|
withUpdatingStatus?: boolean;
|
|
storyViewerOrigin?: StoryViewerOrigin;
|
|
noEmojiStatus?: boolean;
|
|
noFake?: boolean;
|
|
noVerified?: boolean;
|
|
emojiStatusSize?: number;
|
|
noStatusOrTyping?: boolean;
|
|
noRtl?: boolean;
|
|
chatMemberOriginId?: string;
|
|
chatMember?: ApiChatMember;
|
|
isSavedDialog?: boolean;
|
|
noAvatar?: boolean;
|
|
className?: string;
|
|
iconElement?: React.ReactNode;
|
|
rightElement?: React.ReactNode;
|
|
onClick?: VoidFunction;
|
|
onEmojiStatusClick?: VoidFunction;
|
|
};
|
|
|
|
type OwnProps = BaseOwnProps & ({
|
|
userId: string;
|
|
threadId?: ThreadId;
|
|
customPeer?: never;
|
|
} | {
|
|
userId?: never;
|
|
threadId?: never;
|
|
customPeer: CustomPeer;
|
|
});
|
|
|
|
type StateProps = {
|
|
user?: ApiUser;
|
|
userStatus?: ApiUserStatus;
|
|
self?: ApiUser;
|
|
isSavedMessages?: boolean;
|
|
areMessagesLoaded: boolean;
|
|
isSynced?: boolean;
|
|
topic?: ApiTopic;
|
|
messagesCount?: number;
|
|
};
|
|
|
|
const UPDATE_INTERVAL = 1000 * 60; // 1 min
|
|
|
|
const PrivateChatInfo = ({
|
|
userId,
|
|
customPeer,
|
|
typingStatus,
|
|
avatarSize = 'medium',
|
|
status,
|
|
statusIcon,
|
|
withDots,
|
|
withMediaViewer,
|
|
withUsername,
|
|
withStory,
|
|
withFullInfo,
|
|
withUpdatingStatus,
|
|
emojiStatusSize,
|
|
noStatusOrTyping,
|
|
noEmojiStatus,
|
|
noFake,
|
|
noVerified,
|
|
noRtl,
|
|
user,
|
|
userStatus,
|
|
self,
|
|
topic,
|
|
messagesCount,
|
|
isSavedMessages,
|
|
isSavedDialog,
|
|
areMessagesLoaded,
|
|
chatMember,
|
|
chatMemberOriginId,
|
|
ripple,
|
|
className,
|
|
storyViewerOrigin,
|
|
noAvatar,
|
|
isSynced,
|
|
iconElement,
|
|
rightElement,
|
|
onClick,
|
|
onEmojiStatusClick,
|
|
}: OwnProps & StateProps) => {
|
|
const {
|
|
loadFullUser,
|
|
openMediaViewer,
|
|
loadMoreProfilePhotos,
|
|
} = getActions();
|
|
|
|
const oldLang = useOldLang();
|
|
const lang = useLang();
|
|
|
|
const isTopic = Boolean(user?.isBotForum && topic);
|
|
const hasAvatarMediaViewer = withMediaViewer && !isSavedMessages;
|
|
|
|
useEffect(() => {
|
|
if (userId) {
|
|
if (withFullInfo && isSynced) loadFullUser({ userId });
|
|
if (withMediaViewer) loadMoreProfilePhotos({ peerId: userId, isPreload: true });
|
|
}
|
|
}, [userId, withFullInfo, withMediaViewer, isSynced]);
|
|
|
|
useIntervalForceUpdate(UPDATE_INTERVAL);
|
|
|
|
const handleAvatarViewerOpen = useLastCallback(
|
|
(e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
|
|
if (hasMedia) {
|
|
e.stopPropagation();
|
|
openMediaViewer({
|
|
isAvatarView: true,
|
|
chatId: userId,
|
|
mediaIndex: 0,
|
|
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]);
|
|
|
|
if (!user && !customPeer) {
|
|
return undefined;
|
|
}
|
|
|
|
function renderStatusOrTyping() {
|
|
if (status) {
|
|
return withDots ? (
|
|
<DotAnimation className="status" content={status} />
|
|
) : (
|
|
<span className="status" dir="auto">
|
|
{statusIcon && <Icon className="status-icon" name={statusIcon} />}
|
|
{renderText(status)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (withUpdatingStatus && !areMessagesLoaded) {
|
|
return (
|
|
<DotAnimation className="status" content={lang('Updating')} />
|
|
);
|
|
}
|
|
|
|
if (customPeer?.subtitleKey) {
|
|
return (
|
|
<span className="status" dir="auto">
|
|
<span className="user-status" dir="auto">{oldLang(customPeer.subtitleKey)}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return undefined;
|
|
}
|
|
|
|
if (typingStatus) {
|
|
return <TypingStatus typingStatus={typingStatus} />;
|
|
}
|
|
|
|
if (isTopic) {
|
|
return (
|
|
<span className="status" dir="auto">
|
|
<Transition
|
|
name="fade"
|
|
shouldRestoreHeight
|
|
activeKey={messagesCount !== undefined ? 1 : 2}
|
|
className="message-count-transition"
|
|
>
|
|
{messagesCount !== undefined
|
|
? (messagesCount > 0
|
|
? lang('Messages', { count: messagesCount }, { pluralValue: messagesCount })
|
|
: lang('ChatInfoNoMessages')
|
|
) : lang('ChatInfoForumTopic')}
|
|
</Transition>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (isSystemBot(user.id)) {
|
|
return undefined;
|
|
}
|
|
|
|
const translatedStatus = getUserStatus(oldLang, user, userStatus);
|
|
const mainUserNameClassName = buildClassName('handle', translatedStatus && 'withStatus');
|
|
return (
|
|
<span className={buildClassName('status', isUserOnline(user, userStatus, true) && 'online')}>
|
|
{mainUsername && <span className={mainUserNameClassName}>{mainUsername}</span>}
|
|
{translatedStatus && <span className="user-status" dir="auto">{translatedStatus}</span>}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function renderNameTitle() {
|
|
if (isTopic) {
|
|
return (
|
|
<h3 dir="auto" className="fullName">{renderText(topic!.title)}</h3>
|
|
);
|
|
}
|
|
|
|
if (chatMember && hasRank(chatMember)) {
|
|
return (
|
|
<div className="info-name-title">
|
|
<FullNameTitle
|
|
peer={customPeer || user!}
|
|
noFake={noFake}
|
|
noVerified={noVerified}
|
|
withEmojiStatus={!noEmojiStatus}
|
|
emojiStatusSize={emojiStatusSize}
|
|
isSavedMessages={isSavedMessages}
|
|
isSavedDialog={isSavedDialog}
|
|
iconElement={iconElement}
|
|
onEmojiStatusClick={onEmojiStatusClick}
|
|
/>
|
|
<RankBadge
|
|
chatId={chatMemberOriginId!}
|
|
userId={chatMember.userId}
|
|
isAdmin={chatMember.isAdmin}
|
|
isOwner={chatMember.isOwner}
|
|
rank={chatMember.rank}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<FullNameTitle
|
|
peer={customPeer || user!}
|
|
noFake={noFake}
|
|
noVerified={noVerified}
|
|
withEmojiStatus={!noEmojiStatus}
|
|
emojiStatusSize={emojiStatusSize}
|
|
isSavedMessages={isSavedMessages}
|
|
isSavedDialog={isSavedDialog}
|
|
onEmojiStatusClick={onEmojiStatusClick}
|
|
iconElement={iconElement}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={buildClassName('ChatInfo', className)}
|
|
dir={!noRtl && lang.isRtl ? 'rtl' : undefined}
|
|
onClick={onClick}
|
|
>
|
|
{isSavedDialog && self && (
|
|
<Avatar
|
|
key="saved-messages"
|
|
size={avatarSize}
|
|
peer={self}
|
|
isSavedMessages
|
|
className="saved-dialog-avatar"
|
|
/>
|
|
)}
|
|
{!noAvatar && !isTopic && (
|
|
<Avatar
|
|
key={user?.id}
|
|
size={avatarSize}
|
|
peer={customPeer || user}
|
|
className={buildClassName(isSavedDialog && 'overlay-avatar')}
|
|
isSavedMessages={isSavedMessages}
|
|
isSavedDialog={isSavedDialog}
|
|
withStory={withStory}
|
|
storyViewerOrigin={storyViewerOrigin}
|
|
storyViewerMode="single-peer"
|
|
onClick={hasAvatarMediaViewer ? handleAvatarViewerOpen : undefined}
|
|
/>
|
|
)}
|
|
{isTopic && (
|
|
<TopicIcon
|
|
topic={topic!}
|
|
className="topic-header-icon"
|
|
size={TOPIC_ICON_SIZE}
|
|
/>
|
|
)}
|
|
<div className="info">
|
|
{renderNameTitle()}
|
|
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
|
|
</div>
|
|
{ripple && <RippleEffect />}
|
|
{rightElement}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, {
|
|
userId, threadId, forceShowSelf, isSavedDialog,
|
|
}): Complete<StateProps> => {
|
|
const { isSynced } = global;
|
|
const user = userId ? selectUser(global, userId) : undefined;
|
|
const userStatus = userId ? selectUserStatus(global, userId) : undefined;
|
|
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
|
const self = isSavedMessages ? user : selectUser(global, global.currentUserId!);
|
|
const areMessagesLoaded = Boolean(
|
|
isSavedDialog
|
|
? selectChatMessages(global, global.currentUserId!)
|
|
: selectChatMessages(global, userId!),
|
|
);
|
|
|
|
const topic = threadId ? selectTopic(global, userId, threadId) : undefined;
|
|
const messagesCount = topic && userId ? selectThreadMessagesCount(global, userId, threadId!) : undefined;
|
|
|
|
return {
|
|
user,
|
|
userStatus,
|
|
isSavedMessages,
|
|
areMessagesLoaded,
|
|
self,
|
|
isSynced,
|
|
topic,
|
|
messagesCount,
|
|
};
|
|
},
|
|
)(PrivateChatInfo));
|