Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> Co-authored-by: Dmitry Kabanov <dmitrykabanovdev@gmail.com>
563 lines
17 KiB
TypeScript
563 lines
17 KiB
TypeScript
import type { FC } from '../../lib/teact/teact';
|
|
import {
|
|
memo, useCallback, useMemo, useRef, useState,
|
|
} from '../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../global';
|
|
|
|
import type { IAnchorPosition, MessageListType, ThreadId } from '../../types';
|
|
import { MAIN_THREAD_ID } from '../../api/types';
|
|
import { ManagementScreens } from '../../types';
|
|
|
|
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
|
import {
|
|
getHasAdminRight,
|
|
getIsSavedDialog,
|
|
isAnonymousForwardsChat,
|
|
isChatBasicGroup, isChatChannel, isChatSuperGroup,
|
|
} from '../../global/helpers';
|
|
import {
|
|
selectBot,
|
|
selectCanAnimateInterface,
|
|
selectCanTranslateChat,
|
|
selectChat,
|
|
selectChatFullInfo,
|
|
selectIsChatBotNotStarted,
|
|
selectIsChatWithSelf,
|
|
selectIsCurrentUserFrozen,
|
|
selectIsInSelectMode,
|
|
selectIsRightColumnShown,
|
|
selectIsUserBlocked,
|
|
selectLanguageCode,
|
|
selectRequestedChatTranslationLanguage,
|
|
selectTranslationLanguage,
|
|
selectUserFullInfo,
|
|
} from '../../global/selectors';
|
|
import { ARE_CALLS_SUPPORTED, IS_APP } from '../../util/browser/windowEnvironment';
|
|
import { isUserId } from '../../util/entities/ids';
|
|
|
|
import { useHotkeys } from '../../hooks/useHotkeys';
|
|
import useLastCallback from '../../hooks/useLastCallback';
|
|
import useOldLang from '../../hooks/useOldLang';
|
|
|
|
import Icon from '../common/icons/Icon';
|
|
import Button from '../ui/Button';
|
|
import DropdownMenu from '../ui/DropdownMenu';
|
|
import MenuItem from '../ui/MenuItem';
|
|
import MenuSeparator from '../ui/MenuSeparator';
|
|
import HeaderMenuContainer from './HeaderMenuContainer.async';
|
|
|
|
interface OwnProps {
|
|
chatId: string;
|
|
threadId: ThreadId;
|
|
messageListType: MessageListType;
|
|
canExpandActions: boolean;
|
|
isForForum?: boolean;
|
|
isMobile?: boolean;
|
|
onTopicSearch?: NoneToVoidFunction;
|
|
}
|
|
|
|
interface StateProps {
|
|
noMenu?: boolean;
|
|
isChannel?: boolean;
|
|
isRightColumnShown?: boolean;
|
|
canStartBot?: boolean;
|
|
canRestartBot?: boolean;
|
|
canUnblock?: boolean;
|
|
canSubscribe?: boolean;
|
|
canSearch?: boolean;
|
|
canCall?: boolean;
|
|
canMute?: boolean;
|
|
canViewStatistics?: boolean;
|
|
canViewMonetization?: boolean;
|
|
canViewBoosts?: boolean;
|
|
canShowBoostModal?: boolean;
|
|
canLeave?: boolean;
|
|
canEnterVoiceChat?: boolean;
|
|
canCreateVoiceChat?: boolean;
|
|
pendingJoinRequests?: number;
|
|
shouldJoinToSend?: boolean;
|
|
shouldSendJoinRequest?: boolean;
|
|
noAnimation?: boolean;
|
|
canTranslate?: boolean;
|
|
isTranslating?: boolean;
|
|
translationLanguage: string;
|
|
language: string;
|
|
detectedChatLanguage?: string;
|
|
doNotTranslate: string[];
|
|
isAccountFrozen?: boolean;
|
|
}
|
|
|
|
// Chrome breaks layout when focusing input during transition
|
|
const SEARCH_FOCUS_DELAY_MS = 320;
|
|
|
|
const HeaderActions: FC<OwnProps & StateProps> = ({
|
|
chatId,
|
|
threadId,
|
|
noMenu,
|
|
isMobile,
|
|
isChannel,
|
|
canStartBot,
|
|
canRestartBot,
|
|
canUnblock,
|
|
canSubscribe,
|
|
canSearch,
|
|
canCall,
|
|
canMute,
|
|
canViewStatistics,
|
|
canViewMonetization,
|
|
canViewBoosts,
|
|
canShowBoostModal,
|
|
canLeave,
|
|
canEnterVoiceChat,
|
|
canCreateVoiceChat,
|
|
pendingJoinRequests,
|
|
isRightColumnShown,
|
|
isForForum,
|
|
canExpandActions,
|
|
shouldJoinToSend,
|
|
shouldSendJoinRequest,
|
|
noAnimation,
|
|
canTranslate,
|
|
isTranslating,
|
|
translationLanguage,
|
|
language,
|
|
detectedChatLanguage,
|
|
doNotTranslate,
|
|
isAccountFrozen,
|
|
onTopicSearch,
|
|
}) => {
|
|
const {
|
|
joinChannel,
|
|
sendBotCommand,
|
|
openMiddleSearch,
|
|
restartBot,
|
|
requestMasterAndRequestCall,
|
|
requestNextManagementScreen,
|
|
showNotification,
|
|
openChat,
|
|
requestChatTranslation,
|
|
togglePeerTranslations,
|
|
openChatLanguageModal,
|
|
setSettingOption,
|
|
unblockUser,
|
|
setViewForumAsMessages,
|
|
openFrozenAccountModal,
|
|
} = getActions();
|
|
const menuButtonRef = useRef<HTMLButtonElement>();
|
|
const lang = useOldLang();
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
|
|
|
|
const handleHeaderMenuOpen = useLastCallback(() => {
|
|
setIsMenuOpen(true);
|
|
const rect = menuButtonRef.current!.getBoundingClientRect();
|
|
setMenuAnchor({ x: rect.right, y: rect.bottom });
|
|
});
|
|
|
|
const handleHeaderMenuClose = useLastCallback(() => {
|
|
setIsMenuOpen(false);
|
|
});
|
|
|
|
const handleHeaderMenuHide = useLastCallback(() => {
|
|
setMenuAnchor(undefined);
|
|
});
|
|
|
|
const handleSubscribeClick = useLastCallback(() => {
|
|
joinChannel({ chatId });
|
|
if (shouldSendJoinRequest) {
|
|
showNotification({
|
|
message: isChannel ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'),
|
|
});
|
|
}
|
|
});
|
|
|
|
const handleStartBot = useLastCallback(() => {
|
|
sendBotCommand({ command: '/start' });
|
|
});
|
|
|
|
const handleRestartBot = useLastCallback(() => {
|
|
restartBot({ chatId });
|
|
});
|
|
|
|
const handleUnblock = useLastCallback(() => {
|
|
unblockUser({ userId: chatId });
|
|
});
|
|
|
|
const handleTranslateClick = useLastCallback(() => {
|
|
if (isTranslating) {
|
|
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
|
return;
|
|
}
|
|
|
|
requestChatTranslation({ chatId, toLanguageCode: translationLanguage });
|
|
});
|
|
|
|
const handleJoinRequestsClick = useLastCallback(() => {
|
|
requestNextManagementScreen({ screen: ManagementScreens.JoinRequests });
|
|
});
|
|
|
|
const handleSearchClick = useLastCallback(() => {
|
|
if (isForForum) {
|
|
onTopicSearch?.();
|
|
return;
|
|
}
|
|
|
|
openMiddleSearch();
|
|
|
|
if (isMobile) {
|
|
// iOS requires synchronous focus on user event.
|
|
setFocusInSearchInput();
|
|
} else if (noAnimation) {
|
|
// The second RAF is necessary because Teact must update the state and render the async component
|
|
requestMeasure(() => {
|
|
requestNextMutation(setFocusInSearchInput);
|
|
});
|
|
} else {
|
|
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
|
|
}
|
|
});
|
|
|
|
const handleAsMessagesClick = useLastCallback(() => {
|
|
openChat({ id: chatId });
|
|
setViewForumAsMessages({ chatId, isEnabled: true });
|
|
});
|
|
|
|
const handleRequestCall = useLastCallback(() => {
|
|
if (isAccountFrozen) {
|
|
openFrozenAccountModal();
|
|
return;
|
|
}
|
|
requestMasterAndRequestCall({ userId: chatId });
|
|
});
|
|
|
|
const handleHotkeySearchClick = useLastCallback((e: KeyboardEvent) => {
|
|
if (!canSearch || !IS_APP || e.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
handleSearchClick();
|
|
});
|
|
|
|
const getTextWithLanguage = useCallback((langKey: string, langCode: string) => {
|
|
const simplified = langCode.split('-')[0];
|
|
const translationKey = `TranslateLanguage${simplified.toUpperCase()}`;
|
|
const name = lang(translationKey);
|
|
if (name !== translationKey) {
|
|
return lang(langKey, name);
|
|
}
|
|
|
|
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
|
const translatedName = translatedNames.of(langCode)!;
|
|
return lang(`${langKey}Other`, translatedName);
|
|
}, [language, lang]);
|
|
|
|
const buttonText = useMemo(() => {
|
|
if (isTranslating) return lang('ShowOriginalButton');
|
|
|
|
return getTextWithLanguage('TranslateToButton', translationLanguage);
|
|
}, [translationLanguage, getTextWithLanguage, isTranslating, lang]);
|
|
|
|
const doNotTranslateText = useMemo(() => {
|
|
if (!detectedChatLanguage) return undefined;
|
|
|
|
return getTextWithLanguage('DoNotTranslateLanguage', detectedChatLanguage);
|
|
}, [getTextWithLanguage, detectedChatLanguage]);
|
|
|
|
const handleHide = useLastCallback(() => {
|
|
togglePeerTranslations({ chatId, isEnabled: false });
|
|
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
|
});
|
|
|
|
const handleChangeLanguage = useLastCallback(() => {
|
|
openChatLanguageModal({ chatId });
|
|
});
|
|
|
|
const handleDoNotTranslate = useLastCallback(() => {
|
|
if (!detectedChatLanguage) return;
|
|
|
|
setSettingOption({
|
|
doNotTranslate: [...doNotTranslate, detectedChatLanguage],
|
|
});
|
|
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
|
|
|
showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) });
|
|
});
|
|
|
|
useHotkeys(useMemo(() => ({
|
|
'Mod+F': handleHotkeySearchClick,
|
|
}), []));
|
|
|
|
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
|
return ({ onTrigger, isOpen }) => (
|
|
<Button
|
|
round
|
|
ripple={isRightColumnShown}
|
|
color="translucent"
|
|
size="smaller"
|
|
className={isOpen ? 'active' : ''}
|
|
onClick={onTrigger}
|
|
ariaLabel={lang('TranslateMessage')}
|
|
>
|
|
<Icon name="language" />
|
|
</Button>
|
|
);
|
|
}, [isRightColumnShown, lang]);
|
|
|
|
return (
|
|
<div className="HeaderActions">
|
|
{!isForForum && canTranslate && (
|
|
<DropdownMenu
|
|
className="stickers-more-menu with-menu-transitions"
|
|
trigger={MoreMenuButton}
|
|
positionX="right"
|
|
>
|
|
<MenuItem icon="language" onClick={handleTranslateClick}>
|
|
{buttonText}
|
|
</MenuItem>
|
|
<MenuItem icon="replace" onClick={handleChangeLanguage}>
|
|
{lang('Chat.Translate.Menu.To')}
|
|
</MenuItem>
|
|
<MenuSeparator />
|
|
{detectedChatLanguage
|
|
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
|
|
<MenuItem icon="close-circle" onClick={handleHide}>{lang('Hide')}</MenuItem>
|
|
</DropdownMenu>
|
|
)}
|
|
{!isMobile && (
|
|
<>
|
|
{canExpandActions && !shouldSendJoinRequest && (canSubscribe || shouldJoinToSend) && (
|
|
<Button
|
|
size="tiny"
|
|
ripple
|
|
fluid
|
|
onClick={handleSubscribeClick}
|
|
>
|
|
{lang(isChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
|
|
</Button>
|
|
)}
|
|
{canExpandActions && shouldSendJoinRequest && (
|
|
<Button
|
|
size="tiny"
|
|
ripple
|
|
fluid
|
|
onClick={handleSubscribeClick}
|
|
>
|
|
{lang('ChannelJoinRequest')}
|
|
</Button>
|
|
)}
|
|
{canExpandActions && canStartBot && (
|
|
<Button
|
|
size="tiny"
|
|
ripple
|
|
fluid
|
|
onClick={handleStartBot}
|
|
>
|
|
{lang('BotStart')}
|
|
</Button>
|
|
)}
|
|
{canExpandActions && canRestartBot && (
|
|
<Button
|
|
size="tiny"
|
|
ripple
|
|
fluid
|
|
onClick={handleRestartBot}
|
|
>
|
|
{lang('BotRestart')}
|
|
</Button>
|
|
)}
|
|
{canExpandActions && canUnblock && (
|
|
<Button
|
|
size="tiny"
|
|
ripple
|
|
fluid
|
|
onClick={handleUnblock}
|
|
>
|
|
{lang('Unblock')}
|
|
</Button>
|
|
)}
|
|
{canSearch && (
|
|
<Button
|
|
round
|
|
ripple={isRightColumnShown}
|
|
color="translucent"
|
|
size="smaller"
|
|
onClick={handleSearchClick}
|
|
ariaLabel={lang('Conversation.SearchPlaceholder')}
|
|
>
|
|
<Icon name="search" />
|
|
</Button>
|
|
)}
|
|
{canCall && (
|
|
<Button
|
|
round
|
|
color="translucent"
|
|
size="smaller"
|
|
onClick={handleRequestCall}
|
|
ariaLabel="Call"
|
|
>
|
|
<Icon name="phone" />
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
{!isForForum && Boolean(pendingJoinRequests) && (
|
|
<Button
|
|
round
|
|
className="badge-button"
|
|
ripple={isRightColumnShown}
|
|
color="translucent"
|
|
size="smaller"
|
|
onClick={handleJoinRequestsClick}
|
|
ariaLabel={isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}
|
|
>
|
|
<Icon name="user" />
|
|
<div className="badge">{pendingJoinRequests}</div>
|
|
</Button>
|
|
)}
|
|
<Button
|
|
ref={menuButtonRef}
|
|
className={isMenuOpen ? 'active' : ''}
|
|
round
|
|
ripple={!isMobile}
|
|
size="smaller"
|
|
color="translucent"
|
|
disabled={noMenu}
|
|
ariaLabel="More actions"
|
|
onClick={handleHeaderMenuOpen}
|
|
>
|
|
<Icon name="more" />
|
|
</Button>
|
|
{menuAnchor && (
|
|
<HeaderMenuContainer
|
|
chatId={chatId}
|
|
threadId={threadId}
|
|
isOpen={isMenuOpen}
|
|
anchor={menuAnchor}
|
|
withExtraActions={isMobile || !canExpandActions}
|
|
isChannel={isChannel}
|
|
canStartBot={canStartBot}
|
|
canSubscribe={canSubscribe}
|
|
canSearch={canSearch}
|
|
canCall={canCall}
|
|
canMute={canMute}
|
|
canViewStatistics={canViewStatistics}
|
|
canViewBoosts={canViewBoosts}
|
|
canViewMonetization={canViewMonetization}
|
|
canShowBoostModal={canShowBoostModal}
|
|
canLeave={canLeave}
|
|
canEnterVoiceChat={canEnterVoiceChat}
|
|
canCreateVoiceChat={canCreateVoiceChat}
|
|
pendingJoinRequests={pendingJoinRequests}
|
|
onJoinRequestsClick={handleJoinRequestsClick}
|
|
withForumActions={isForForum}
|
|
onSubscribeChannel={handleSubscribeClick}
|
|
onSearchClick={handleSearchClick}
|
|
onAsMessagesClick={handleAsMessagesClick}
|
|
onClose={handleHeaderMenuClose}
|
|
onCloseAnimationEnd={handleHeaderMenuHide}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, {
|
|
chatId, threadId, messageListType, isMobile,
|
|
}): StateProps => {
|
|
const chat = selectChat(global, chatId);
|
|
const isChannel = Boolean(chat && isChatChannel(chat));
|
|
const isSuperGroup = Boolean(chat && isChatSuperGroup(chat));
|
|
const language = selectLanguageCode(global);
|
|
const translationLanguage = selectTranslationLanguage(global);
|
|
const isPrivate = isUserId(chatId);
|
|
const { doNotTranslate } = global.settings.byKey;
|
|
|
|
if (!chat || chat.isRestricted || selectIsInSelectMode(global)) {
|
|
return {
|
|
noMenu: true,
|
|
language,
|
|
translationLanguage,
|
|
doNotTranslate,
|
|
};
|
|
}
|
|
|
|
const bot = selectBot(global, chatId);
|
|
const chatFullInfo = !isPrivate ? selectChatFullInfo(global, chatId) : undefined;
|
|
const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined;
|
|
const fullInfo = chatFullInfo || userFullInfo;
|
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
|
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
|
|
const isDiscussionThread = messageListType === 'thread' && threadId !== MAIN_THREAD_ID;
|
|
const isRightColumnShown = selectIsRightColumnShown(global, isMobile);
|
|
|
|
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
|
|
|
const isUserBlocked = isPrivate ? selectIsUserBlocked(global, chatId) : false;
|
|
const canRestartBot = Boolean(bot && isUserBlocked);
|
|
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
|
const canUnblock = isUserBlocked && !bot;
|
|
const canSubscribe = Boolean(
|
|
(isMainThread || chat.isForum) && (isChannel || isSuperGroup) && chat.isNotJoined,
|
|
);
|
|
const canSearch = isMainThread || isDiscussionThread;
|
|
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot && !chat.isSupport
|
|
&& !isAnonymousForwardsChat(chat.id);
|
|
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
|
|
const canLeave = isSavedDialog || (isMainThread && !canSubscribe);
|
|
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive;
|
|
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && !chat.isCallActive
|
|
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
|
const canViewStatistics = isMainThread && chatFullInfo?.canViewStatistics;
|
|
const canViewMonetization = isMainThread && chatFullInfo?.canViewMonetization;
|
|
const canViewBoosts = isMainThread
|
|
&& (isSuperGroup || isChannel) && (canViewStatistics || getHasAdminRight(chat, 'postStories'));
|
|
const canShowBoostModal = !canViewBoosts && (isSuperGroup || isChannel);
|
|
const pendingJoinRequests = isMainThread ? chatFullInfo?.requestsPending : undefined;
|
|
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
|
|
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
|
const noAnimation = !selectCanAnimateInterface(global);
|
|
|
|
const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId));
|
|
const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled;
|
|
const isAccountFrozen = selectIsCurrentUserFrozen(global);
|
|
|
|
return {
|
|
noMenu: false,
|
|
isChannel,
|
|
isRightColumnShown,
|
|
canStartBot,
|
|
canRestartBot,
|
|
canSubscribe,
|
|
canSearch,
|
|
canCall,
|
|
canMute,
|
|
canViewStatistics,
|
|
canViewMonetization,
|
|
canViewBoosts,
|
|
canShowBoostModal,
|
|
canLeave,
|
|
canEnterVoiceChat,
|
|
canCreateVoiceChat,
|
|
pendingJoinRequests,
|
|
shouldJoinToSend,
|
|
shouldSendJoinRequest,
|
|
noAnimation,
|
|
canTranslate,
|
|
isTranslating,
|
|
translationLanguage,
|
|
language,
|
|
doNotTranslate,
|
|
detectedChatLanguage: chat.detectedLanguage,
|
|
canUnblock,
|
|
isAccountFrozen,
|
|
};
|
|
},
|
|
)(HeaderActions));
|
|
|
|
function setFocusInSearchInput() {
|
|
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
|
|
searchInput?.focus();
|
|
}
|