537 lines
16 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useMemo,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiBotVerification,
ApiChat,
ApiCountryCode,
ApiUser,
ApiUserFullInfo,
ApiUsername,
} from '../../../api/types';
import type { BotAppPermissions } from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH } from '../../../config';
import {
buildStaticMapHash,
getChatLink,
getHasAdminRight,
isChatChannel,
isUserRightBanned,
} from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import {
selectBotAppPermissions,
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectNotifyDefaults,
selectNotifyException,
selectTopicLink,
selectUser,
selectUserFullInfo,
} from '../../../global/selectors';
import { copyTextToClipboard } from '../../../util/clipboard';
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
import stopEvent from '../../../util/stopEvent';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { ChatAnimationTypes } from '../../left/main/hooks';
import formatUsername from '../helpers/formatUsername';
import renderText from '../helpers/renderText';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
import Chat from '../../left/main/Chat';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem';
import Skeleton from '../../ui/placeholder/Skeleton';
import Switcher from '../../ui/Switcher';
import CustomEmoji from '../CustomEmoji';
import SafeLink from '../SafeLink';
import BusinessHours from './BusinessHours';
import UserBirthday from './UserBirthday';
import styles from './ChatExtra.module.scss';
type OwnProps = {
chatOrUserId: string;
isSavedDialog?: boolean;
isInSettings?: boolean;
};
type StateProps = {
user?: ApiUser;
chat?: ApiChat;
userFullInfo?: ApiUserFullInfo;
canInviteUsers?: boolean;
isMuted?: boolean;
phoneCodeList: ApiCountryCode[];
topicId?: number;
description?: string;
chatInviteLink?: string;
topicLink?: string;
hasSavedMessages?: boolean;
personalChannel?: ApiChat;
hasMainMiniApp?: boolean;
isBotCanManageEmojiStatus?: boolean;
botAppPermissions?: BotAppPermissions;
botVerification?: ApiBotVerification;
};
const DEFAULT_MAP_CONFIG = {
width: 64,
height: 64,
zoom: 15,
};
const BOT_VERIFICATION_ICON_SIZE = 16;
const ChatExtra: FC<OwnProps & StateProps> = ({
chatOrUserId,
user,
chat,
userFullInfo,
isInSettings,
canInviteUsers,
isMuted,
phoneCodeList,
topicId,
description,
chatInviteLink,
topicLink,
hasSavedMessages,
personalChannel,
hasMainMiniApp,
isBotCanManageEmojiStatus,
botAppPermissions,
botVerification,
}) => {
const {
showNotification,
updateChatMutedState,
updateTopicMutedState,
loadPeerStories,
openSavedDialog,
openMapModal,
requestCollectibleInfo,
requestMainWebView,
toggleUserEmojiStatusPermission,
toggleUserLocationPermission,
} = getActions();
const {
id: userId,
usernames,
phoneNumber,
isSelf,
} = user || {};
const { id: chatId, usernames: chatUsernames } = chat || {};
const peerId = userId || chatId;
const {
businessLocation,
businessWorkHours,
personalChannelMessageId,
birthday,
} = userFullInfo || {};
const oldLang = useOldLang();
const lang = useLang();
useEffectWithPrevDeps(([prevPeerId]) => {
if (!peerId || prevPeerId === peerId) return;
if (user || (chat && isChatChannel(chat))) {
loadPeerStories({ peerId });
}
}, [peerId, chat, user]);
const { width, height, zoom } = DEFAULT_MAP_CONFIG;
const dpr = useDevicePixelRatio();
const locationMediaHash = businessLocation?.geo
&& buildStaticMapHash(businessLocation.geo, width, height, zoom, dpr);
const locationBlobUrl = useMedia(locationMediaHash);
const locationRightComponent = useMemo(() => {
if (!businessLocation?.geo) return undefined;
if (locationBlobUrl) {
return <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
}
return <Skeleton className={styles.businessLocation} />;
}, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium;
const activeUsernames = useMemo(() => {
const result = usernames?.filter((u) => u.isActive);
return result?.length ? result : undefined;
}, [usernames]);
const activeChatUsernames = useMemo(() => {
const result = !user ? chatUsernames?.filter((u) => u.isActive) : undefined;
return result?.length ? result : undefined;
}, [chatUsernames, user]);
const link = useMemo(() => {
if (!chat) {
return undefined;
}
return isTopicInfo ? topicLink! : getChatLink(chat) || chatInviteLink;
}, [chat, isTopicInfo, topicLink, chatInviteLink]);
const handleClickLocation = useLastCallback(() => {
const { address, geo } = businessLocation!;
if (!geo) {
copyTextToClipboard(address);
showNotification({ message: oldLang('BusinessLocationCopied') });
return;
}
openMapModal({ geoPoint: geo, zoom });
});
const handleNotificationChange = useLastCallback(() => {
if (isTopicInfo) {
updateTopicMutedState({
chatId: chatId!,
topicId: topicId!,
isMuted: !isMuted,
});
} else {
updateChatMutedState({ chatId: chatId!, isMuted: !isMuted });
}
});
const manageEmojiStatusChange = useLastCallback(() => {
if (!user) return;
toggleUserEmojiStatusPermission({ botId: user.id, isEnabled: !isBotCanManageEmojiStatus });
});
const handleLocationPermissionChange = useLastCallback(() => {
if (!user) return;
toggleUserLocationPermission({ botId: user.id, isAccessGranted: !botAppPermissions?.geolocation });
});
const handleOpenSavedDialog = useLastCallback(() => {
openSavedDialog({ chatId: chatOrUserId });
});
function copy(text: string, entity: string) {
copyTextToClipboard(text);
showNotification({ message: `${entity} was copied` });
}
const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber);
const handlePhoneClick = useLastCallback(() => {
if (phoneNumber?.length === FRAGMENT_PHONE_LENGTH && phoneNumber.startsWith(FRAGMENT_PHONE_CODE)) {
requestCollectibleInfo({ collectible: phoneNumber, peerId: peerId!, type: 'phone' });
return;
}
copy(formattedNumber!, oldLang('Phone'));
});
const handleUsernameClick = useLastCallback((username: ApiUsername, isChat?: boolean) => {
if (!username.isEditable) {
requestCollectibleInfo({ collectible: username.username, peerId: peerId!, type: 'username' });
return;
}
copy(formatUsername(username.username, isChat), oldLang(isChat ? 'Link' : 'Username'));
});
const handleOpenApp = useLastCallback(() => {
const botId = user?.id;
if (!botId) {
return;
}
const theme = extractCurrentThemeParams();
requestMainWebView({
botId,
peerId: botId,
theme,
shouldMarkBotTrusted: true,
});
});
const appTermsInfo = lang('ProfileOpenAppAbout', {
terms: (
<SafeLink
text={lang('ProfileOpenAppTerms')}
url={lang('ProfileBotOpenAppInfoLink')}
/>
),
}, { withNodes: true });
if (chat?.isRestricted || (isSelf && !isInSettings)) {
return undefined;
}
function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) {
const [mainUsername, ...otherUsernames] = usernameList;
const usernameLinks = otherUsernames.length
? (oldLang('UsernameAlso', '%USERNAMES%') as string)
.split('%')
.map((s) => {
return (s === 'USERNAMES' ? (
<>
{otherUsernames.map((username, idx) => {
return (
<>
{idx > 0 ? ', ' : ''}
<a
key={username.username}
href={formatUsername(username.username, true)}
onMouseDown={stopEvent}
onClick={(e) => {
stopEvent(e);
handleUsernameClick(username, isChat);
}}
className="text-entity-link username-link"
>
{formatUsername(username.username)}
</a>
</>
);
})}
</>
) : s);
})
: undefined;
return (
<ListItem
icon={isChat ? 'link' : 'mention'}
multiline
narrow
ripple
// eslint-disable-next-line react/jsx-no-bind
onClick={() => {
handleUsernameClick(mainUsername, isChat);
}}
>
<span className="title" dir={lang.isRtl ? 'rtl' : undefined}>
{formatUsername(mainUsername.username, isChat)}
</span>
<span className="subtitle">
{usernameLinks && <span className="other-usernames">{usernameLinks}</span>}
{oldLang(isChat ? 'Link' : 'Username')}
</span>
</ListItem>
);
}
return (
<div className="ChatExtra">
{personalChannel && (
<div className={styles.personalChannel}>
<h3 className={styles.personalChannelTitle}>{oldLang('ProfileChannel')}</h3>
<span className={styles.personalChannelSubscribers}>
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
</span>
<Chat
chatId={personalChannel.id}
orderDiff={0}
animationType={ChatAnimationTypes.None}
isPreview
previewMessageId={personalChannelMessageId}
className={styles.personalChannelItem}
/>
</div>
)}
{Boolean(formattedNumber?.length) && (
// eslint-disable-next-line react/jsx-no-bind
<ListItem icon="phone" multiline narrow ripple onClick={handlePhoneClick}>
<span className="title" dir={lang.isRtl ? 'rtl' : undefined}>{formattedNumber}</span>
<span className="subtitle">{oldLang('Phone')}</span>
</ListItem>
)}
{activeUsernames && renderUsernames(activeUsernames)}
{description && Boolean(description.length) && (
<ListItem
icon="info"
multiline
narrow
isStatic
allowSelection
>
<span className="title word-break allow-selection" dir={lang.isRtl ? 'rtl' : undefined}>
{
renderText(description, [
'br',
shouldRenderAllLinks ? 'links' : 'tg_links',
'emoji',
])
}
</span>
<span className="subtitle">{oldLang(userId ? 'UserBio' : 'Info')}</span>
</ListItem>
)}
{activeChatUsernames && !isTopicInfo && renderUsernames(activeChatUsernames, true)}
{((!activeChatUsernames && canInviteUsers) || isTopicInfo) && link && (
<ListItem
icon="link"
multiline
narrow
ripple
// eslint-disable-next-line react/jsx-no-bind
onClick={() => copy(link, oldLang('SetUrlPlaceholder'))}
>
<div className="title">{link}</div>
<span className="subtitle">{oldLang('SetUrlPlaceholder')}</span>
</ListItem>
)}
{birthday && (
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
)}
{ hasMainMiniApp && (
<ListItem
multiline
isStatic
narrow
>
<Button
className={styles.openAppButton}
size="smaller"
onClick={handleOpenApp}
>
{oldLang('ProfileBotOpenApp')}
</Button>
<div className={styles.sectionInfo}>
{appTermsInfo}
</div>
</ListItem>
)}
{!isInSettings && (
<ListItem icon="unmute" narrow ripple onClick={handleNotificationChange}>
<span>{oldLang('Notifications')}</span>
<Switcher
id="group-notifications"
label={userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'}
checked={!isMuted}
inactive
/>
</ListItem>
)}
{businessWorkHours && (
<BusinessHours businessHours={businessWorkHours} />
)}
{businessLocation && (
<ListItem
icon="location"
ripple
multiline
narrow
rightElement={locationRightComponent}
onClick={handleClickLocation}
>
<div className="title">{businessLocation.address}</div>
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
</ListItem>
)}
{hasSavedMessages && !isInSettings && (
<ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}>
<span>{oldLang('SavedMessagesTab')}</span>
</ListItem>
)}
{userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && (
<ListItem icon="user" narrow ripple onClick={manageEmojiStatusChange}>
<span>{oldLang('BotProfilePermissionEmojiStatus')}</span>
<Switcher
label={oldLang('BotProfilePermissionEmojiStatus')}
checked={isBotCanManageEmojiStatus}
inactive
/>
</ListItem>
)}
{botAppPermissions?.geolocation !== undefined && (
<ListItem icon="location" narrow ripple onClick={handleLocationPermissionChange}>
<span>{oldLang('BotProfilePermissionLocation')}</span>
<Switcher
label={oldLang('BotProfilePermissionLocation')}
checked={botAppPermissions?.geolocation}
inactive
/>
</ListItem>
)}
{botVerification && (
<div className={styles.botVerificationSection}>
<CustomEmoji
className={styles.botVerificationIcon}
documentId={botVerification.iconId}
size={BOT_VERIFICATION_ICON_SIZE}
/>
{botVerification.description}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatOrUserId, isSavedDialog }): StateProps => {
const { countryList: { phoneCodes: phoneCodeList } } = global;
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined;
const botAppPermissions = chatOrUserId ? selectBotAppPermissions(global, chatOrUserId) : undefined;
const isForum = chat?.isForum;
const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id));
const { threadId } = selectCurrentMessageList(global) || {};
const topicId = isForum && threadId ? Number(threadId) : undefined;
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
const userFullInfo = user && selectUserFullInfo(global, user.id);
const botVerification = userFullInfo?.botVerification || chatFullInfo?.botVerification;
const chatInviteLink = chatFullInfo?.inviteLink;
const description = userFullInfo?.bio || chatFullInfo?.about;
const canInviteUsers = chat && !user && (
(!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers'))
|| getHasAdminRight(chat, 'inviteUsers')
);
const topicLink = topicId ? selectTopicLink(global, chatOrUserId, topicId) : undefined;
const hasSavedMessages = !isSavedDialog && global.chats.listIds.saved?.includes(chatOrUserId);
const personalChannel = userFullInfo?.personalChannelId
? selectChat(global, userFullInfo.personalChannelId)
: undefined;
const hasMainMiniApp = user?.hasMainMiniApp;
return {
phoneCodeList,
chat,
user,
userFullInfo,
canInviteUsers,
botAppPermissions,
isMuted,
topicId,
chatInviteLink,
description,
topicLink,
hasSavedMessages,
personalChannel,
hasMainMiniApp,
isBotCanManageEmojiStatus: userFullInfo?.isBotCanManageEmojiStatus,
botVerification,
};
},
)(ChatExtra));