Various UI fixes (#1840)

This commit is contained in:
Alexander Zinchuk 2022-04-26 17:08:44 +02:00
parent 2a9b516142
commit 67da677b1b
20 changed files with 247 additions and 123 deletions

View File

@ -76,19 +76,20 @@ const Avatar: FC<OwnProps> = ({
const lang = useLang(); const lang = useLang();
let content: string | undefined = ''; let content: string | undefined = '';
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
if (isSavedMessages) { if (isSavedMessages) {
content = <i className={buildClassName(cn.icon, 'icon-avatar-saved-messages')} />; content = <i className={buildClassName(cn.icon, 'icon-avatar-saved-messages')} aria-label={author} />;
} else if (isDeleted) { } else if (isDeleted) {
content = <i className={buildClassName(cn.icon, 'icon-avatar-deleted-account')} />; content = <i className={buildClassName(cn.icon, 'icon-avatar-deleted-account')} aria-label={author} />;
} else if (isReplies) { } else if (isReplies) {
content = <i className={buildClassName(cn.icon, 'icon-reply-filled')} />; content = <i className={buildClassName(cn.icon, 'icon-reply-filled')} aria-label={author} />;
} else if (blobUrl) { } else if (blobUrl) {
content = ( content = (
<img <img
src={blobUrl} src={blobUrl}
className={buildClassName(cn.img, 'avatar-media', transitionClassNames)} className={buildClassName(cn.img, 'avatar-media', transitionClassNames)}
alt="" alt={author}
decoding="async" decoding="async"
/> />
); );
@ -125,7 +126,12 @@ const Avatar: FC<OwnProps> = ({
const senderId = (user || chat) && (user || chat)!.id; const senderId = (user || chat) && (user || chat)!.id;
return ( return (
<div className={fullClassName} onClick={handleClick} data-test-sender-id={IS_TEST ? senderId : undefined}> <div
className={fullClassName}
onClick={handleClick}
data-test-sender-id={IS_TEST ? senderId : undefined}
aria-label={typeof content === 'string' ? author : undefined}
>
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content} {typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
</div> </div>
); );

View File

@ -5,11 +5,11 @@ import { getActions, withGlobal } from '../../global';
import { LeftColumnContent, SettingsScreens } from '../../types'; import { LeftColumnContent, SettingsScreens } from '../../types';
import { IS_MAC_OS, LAYERS_ANIMATION_NAME } from '../../util/environment'; import { LAYERS_ANIMATION_NAME } from '../../util/environment';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import getKeyFromEvent from '../../util/getKeyFromEvent';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer'; import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import { useResize } from '../../hooks/useResize'; import { useResize } from '../../hooks/useResize';
import { useHotkeys } from '../../hooks/useHotkeys';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import LeftMain from './main/LeftMain'; import LeftMain from './main/LeftMain';
@ -25,6 +25,7 @@ type StateProps = {
activeChatFolder: number; activeChatFolder: number;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
leftColumnWidth?: number; leftColumnWidth?: number;
currentUserId?: string;
}; };
enum ContentType { enum ContentType {
@ -47,6 +48,7 @@ const LeftColumn: FC<StateProps> = ({
activeChatFolder, activeChatFolder,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
leftColumnWidth, leftColumnWidth,
currentUserId,
}) => { }) => {
const { const {
setGlobalSearchQuery, setGlobalSearchQuery,
@ -57,6 +59,7 @@ const LeftColumn: FC<StateProps> = ({
clearTwoFaError, clearTwoFaError,
setLeftColumnWidth, setLeftColumnWidth,
resetLeftColumnWidth, resetLeftColumnWidth,
openChat,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -255,25 +258,25 @@ const LeftColumn: FC<StateProps> = ({
[activeChatFolder, content, handleReset], [activeChatFolder, content, handleReset],
); );
useEffect(() => { const handleHotkeySearch = useCallback((e: KeyboardEvent) => {
if (content === LeftColumnContent.GlobalSearch) { if (content === LeftColumnContent.GlobalSearch) {
return undefined; return;
} }
function handleKeyDown(e: KeyboardEvent) { e.preventDefault();
if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.shiftKey && getKeyFromEvent(e) === 'f') { setContent(LeftColumnContent.GlobalSearch);
e.preventDefault();
setContent(LeftColumnContent.GlobalSearch);
}
}
document.addEventListener('keydown', handleKeyDown, false);
return () => {
document.removeEventListener('keydown', handleKeyDown, false);
};
}, [content]); }, [content]);
const handleHotkeySavedMessages = useCallback((e: KeyboardEvent) => {
e.preventDefault();
openChat({ id: currentUserId });
}, [currentUserId, openChat]);
useHotkeys([
['mod+shift+F', handleHotkeySearch],
['mod+shift+S', handleHotkeySavedMessages],
]);
useEffect(() => { useEffect(() => {
clearTwoFaError(); clearTwoFaError();
@ -386,9 +389,16 @@ export default memo(withGlobal(
}, },
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
leftColumnWidth, leftColumnWidth,
currentUserId,
} = global; } = global;
return { return {
searchQuery: query, searchDate: date, activeChatFolder, shouldSkipHistoryAnimations, leftColumnWidth, searchQuery: query,
searchDate: date,
activeChatFolder,
shouldSkipHistoryAnimations,
leftColumnWidth,
currentUserId,
}; };
}, },
)(LeftColumn)); )(LeftColumn));

View File

@ -19,6 +19,7 @@ import usePrevious from '../../../hooks/usePrevious';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
import { useChatAnimationType } from './hooks'; import { useChatAnimationType } from './hooks';
import { HotkeyItem, useHotkeys } from '../../../hooks/useHotkeys';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
@ -74,14 +75,29 @@ const ChatList: FC<OwnProps> = ({
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE); const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
// Support <Cmd>+<Digit> and <Alt>+<Up/Down> to navigate between chats // Support <Alt>+<Up/Down> to navigate between chats
const hotkeys: HotkeyItem[] = [];
if (isActive && orderedIds?.length) {
hotkeys.push(['alt+ArrowUp', (e: KeyboardEvent) => {
e.preventDefault();
openNextChat({ targetIndexDelta: -1, orderedIds });
}]);
hotkeys.push(['alt+ArrowDown', (e: KeyboardEvent) => {
e.preventDefault();
openNextChat({ targetIndexDelta: 1, orderedIds });
}]);
}
useHotkeys(hotkeys);
// Support <Cmd>+<Digit> to navigate between chats
useEffect(() => { useEffect(() => {
if (!isActive || !orderedIds) { if (!isActive || !orderedIds || !IS_PWA) {
return undefined; return undefined;
} }
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
if (IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) { if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) {
const [, digit] = e.code.match(/Digit(\d)/) || []; const [, digit] = e.code.match(/Digit(\d)/) || [];
if (!digit) return; if (!digit) return;
@ -90,14 +106,6 @@ const ChatList: FC<OwnProps> = ({
openChat({ id: orderedIds![position], shouldReplaceHistory: true }); openChat({ id: orderedIds![position], shouldReplaceHistory: true });
} }
if (e.altKey) {
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
if (!targetIndexDelta) return;
e.preventDefault();
openNextChat({ targetIndexDelta, orderedIds });
}
} }
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);

View File

@ -96,7 +96,6 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { const {
openChat, openChat,
openTipsChat,
setGlobalSearchDate, setGlobalSearchDate,
setSettingOption, setSettingOption,
setGlobalSearchChatId, setGlobalSearchChatId,
@ -196,8 +195,8 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
}, []); }, []);
const handleOpenTipsChat = useCallback(() => { const handleOpenTipsChat = useCallback(() => {
openTipsChat({ langCode: lang.code }); openChatByUsername({ username: lang('Settings.TipsUsername') });
}, [lang.code, openTipsChat]); }, [lang, openChatByUsername]);
const isSearchFocused = ( const isSearchFocused = (
Boolean(globalSearchChatId) Boolean(globalSearchChatId)

View File

@ -4,7 +4,6 @@ import React, {
useRef, useRef,
useCallback, useCallback,
useState, useState,
useEffect,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
@ -13,9 +12,8 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { IAnchorPosition, ManagementScreens } from '../../types'; import { IAnchorPosition, ManagementScreens } from '../../types';
import { import {
ARE_CALLS_SUPPORTED, IS_MAC_OS, IS_PWA, IS_SINGLE_COLUMN_LAYOUT, ARE_CALLS_SUPPORTED, IS_PWA, IS_SINGLE_COLUMN_LAYOUT,
} from '../../util/environment'; } from '../../util/environment';
import getKeyFromEvent from '../../util/getKeyFromEvent';
import { import {
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId, isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
@ -29,6 +27,7 @@ import {
selectIsRightColumnShown, selectIsRightColumnShown,
} from '../../global/selectors'; } from '../../global/selectors';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { useHotkeys } from '../../hooks/useHotkeys';
import Button from '../ui/Button'; import Button from '../ui/Button';
import HeaderMenuContainer from './HeaderMenuContainer.async'; import HeaderMenuContainer from './HeaderMenuContainer.async';
@ -143,27 +142,19 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
requestCall({ userId: chatId }); requestCall({ userId: chatId });
} }
useEffect(() => { const handleHotkeySearchClick = useCallback((e: KeyboardEvent) => {
if (!canSearch) { if (!canSearch || !IS_PWA || e.shiftKey) {
return undefined; return;
} }
function handleKeyDown(e: KeyboardEvent) { e.preventDefault();
if ( handleSearchClick();
IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && !e.shiftKey && getKeyFromEvent(e) === 'f'
) {
e.preventDefault();
handleSearchClick();
}
}
document.addEventListener('keydown', handleKeyDown, false);
return () => {
document.removeEventListener('keydown', handleKeyDown, false);
};
}, [canSearch, handleSearchClick]); }, [canSearch, handleSearchClick]);
useHotkeys([
['meta+F', handleHotkeySearchClick],
]);
const lang = useLang(); const lang = useLang();
return ( return (

View File

@ -253,6 +253,7 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
user-select: none;
.info { .info {
display: flex; display: flex;

View File

@ -1149,19 +1149,6 @@ const Composer: FC<OwnProps & StateProps> = ({
{formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)}
</span> </span>
)} )}
<StickerTooltip
chatId={chatId}
threadId={threadId}
isOpen={isStickerTooltipOpen}
onStickerSelect={handleStickerSelect}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
addRecentEmoji={addRecentEmoji}
/>
<AttachMenu <AttachMenu
chatId={chatId} chatId={chatId}
isButtonVisible={!activeVoiceRecording && !editingMessage} isButtonVisible={!activeVoiceRecording && !editingMessage}
@ -1188,6 +1175,19 @@ const Composer: FC<OwnProps & StateProps> = ({
onClose={closeBotCommandMenu} onClose={closeBotCommandMenu}
/> />
)} )}
<StickerTooltip
chatId={chatId}
threadId={threadId}
isOpen={isStickerTooltipOpen}
onStickerSelect={handleStickerSelect}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
addRecentEmoji={addRecentEmoji}
/>
<SymbolMenu <SymbolMenu
chatId={chatId} chatId={chatId}
threadId={threadId} threadId={threadId}

View File

@ -1,24 +1,16 @@
import { useEffect } from '../../../lib/teact/teact'; import { useHotkeys } from '../../../hooks/useHotkeys';
import { IS_MAC_OS } from '../../../util/environment';
import getKeyFromEvent from '../../../util/getKeyFromEvent';
const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneToVoidFunction) => { const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneToVoidFunction) => {
useEffect(() => { function handleCopy(e: KeyboardEvent) {
function handleCopy(e: KeyboardEvent) { if (!isActive) {
if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && getKeyFromEvent(e) === 'c') { return;
e.preventDefault();
copySelectedMessages();
}
} }
if (isActive) { e.preventDefault();
document.addEventListener('keydown', handleCopy, false); copySelectedMessages();
} }
return () => { useHotkeys([['meta+C', handleCopy]]);
document.removeEventListener('keydown', handleCopy, false);
};
}, [copySelectedMessages, isActive]);
}; };
export default useCopySelectedMessages; export default useCopySelectedMessages;

View File

@ -6,9 +6,12 @@
.description { .description {
position: relative; position: relative;
margin-top: 0.5rem;
&.has-image { &.has-image {
margin: 1rem -0.5rem -0.375rem; .content-inner:not(.forwarded-message) & {
margin: 0.5rem -0.5rem -0.375rem;
}
.invoice-image { .invoice-image {
width: 100%; width: 100%;
@ -16,6 +19,11 @@
object-fit: cover; object-fit: cover;
border-bottom-left-radius: var(--border-bottom-left-radius); border-bottom-left-radius: var(--border-bottom-left-radius);
border-bottom-right-radius: var(--border-bottom-right-radius); border-bottom-right-radius: var(--border-bottom-right-radius);
.forwarded-message & {
border-top-left-radius: var(--border-top-left-radius);
border-top-right-radius: var(--border-top-right-radius);
}
} }
.description-text { .description-text {

View File

@ -72,7 +72,7 @@ const Invoice: FC<OwnProps> = ({
<p className="title">{renderText(title)}</p> <p className="title">{renderText(title)}</p>
)} )}
{text && ( {text && (
<p>{renderText(text, ['emoji', 'br'])}</p> <div>{renderText(text, ['emoji', 'br'])}</div>
)} )}
<div className={`description ${photoUrl ? 'has-image' : ''}`}> <div className={`description ${photoUrl ? 'has-image' : ''}`}>
{photoUrl && ( {photoUrl && (

View File

@ -163,7 +163,10 @@
&__filter { &__filter {
padding: 0 1rem 0.25rem 0.75rem; padding: 0 1rem 0.25rem 0.75rem;
border-bottom: 1px solid var(--color-borders); background-color: var(--color-background);
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
margin-bottom: 0.625rem;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
flex-shrink: 0; flex-shrink: 0;

View File

@ -165,9 +165,9 @@ const ListItem: FC<OwnProps> = ({
> >
<div <div
className={buildClassName('ListItem-button', isTouched && 'active')} className={buildClassName('ListItem-button', isTouched && 'active')}
role="button" role={!isStatic ? 'button' : undefined}
ref={buttonRef} ref={buttonRef}
tabIndex={0} tabIndex={!isStatic ? 0 : undefined}
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : undefined} onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : undefined}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onContextMenu={(!inactive && contextActions) ? handleContextMenu : undefined} onContextMenu={(!inactive && contextActions) ? handleContextMenu : undefined}

View File

@ -193,8 +193,6 @@ export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE;
export const DEFAULT_LANG_CODE = 'en'; export const DEFAULT_LANG_CODE = 'en';
export const DEFAULT_LANG_PACK = 'android'; export const DEFAULT_LANG_PACK = 'android';
export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const; export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const;
export const TIPS_USERNAME = 'TelegramTips';
export const LOCALIZED_TIPS = ['ar', 'pt-br', 'id', 'it', 'ko', 'ms', 'pl', 'es', 'tr'];
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time'; export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
export const LIGHT_THEME_BG_COLOR = '#A2AF8E'; export const LIGHT_THEME_BG_COLOR = '#A2AF8E';
export const DARK_THEME_BG_COLOR = '#0F0F0F'; export const DARK_THEME_BG_COLOR = '#0F0F0F';

View File

@ -12,8 +12,6 @@ import {
ARCHIVED_FOLDER_ID, ARCHIVED_FOLDER_ID,
TOP_CHAT_MESSAGES_PRELOAD_LIMIT, TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
CHAT_LIST_LOAD_SLICE, CHAT_LIST_LOAD_SLICE,
TIPS_USERNAME,
LOCALIZED_TIPS,
RE_TG_LINK, RE_TG_LINK,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
TMP_CHAT_ID, ALL_FOLDER_ID, DEBUG, TMP_CHAT_ID, ALL_FOLDER_ID, DEBUG,
@ -153,16 +151,6 @@ addActionHandler('openSupportChat', async (global, actions) => {
} }
}); });
addActionHandler('openTipsChat', (global, actions, payload) => {
const { langCode } = payload;
const usernamePostfix = langCode === 'pt-br'
? 'BR'
: LOCALIZED_TIPS.includes(langCode) ? (langCode as string).toUpperCase() : '';
actions.openChatByUsername({ username: `${TIPS_USERNAME}${usernamePostfix}` });
});
addActionHandler('loadAllChats', async (global, actions, payload) => { addActionHandler('loadAllChats', async (global, actions, payload) => {
const listType = payload.listType as 'active' | 'archived'; const listType = payload.listType as 'active' | 'archived';
const { onReplace } = payload; const { onReplace } = payload;

View File

@ -6,6 +6,7 @@ import { orderBy } from '../../util/iteratees';
import { LangFn } from '../../hooks/useLang'; import { LangFn } from '../../hooks/useLang';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
import { prepareSearchWordsForNeedle } from '../../util/searchWords'; import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { formatPhoneNumber } from '../../util/phoneNumber';
const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6]; const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6];
@ -54,6 +55,10 @@ export function getUserFullName(user?: ApiUser) {
return user.lastName; return user.lastName;
} }
if (user.phoneNumber) {
return `+${formatPhoneNumber(user.phoneNumber)}`;
}
break; break;
} }

View File

@ -818,7 +818,7 @@ export type NonTypedActionNames = (
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' | 'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
// chats // chats
'preloadTopChatMessages' | 'loadAllChats' | 'openChatWithInfo' | 'openLinkedChat' | 'preloadTopChatMessages' | 'loadAllChats' | 'openChatWithInfo' | 'openLinkedChat' |
'openSupportChat' | 'openTipsChat' | 'focusMessageInComments' | 'openChatByPhoneNumber' | 'openSupportChat' | 'focusMessageInComments' | 'openChatByPhoneNumber' |
'loadChatSettings' | 'loadFullChat' | 'loadTopChats' | 'requestChatUpdate' | 'updateChatMutedState' | 'loadChatSettings' | 'loadFullChat' | 'loadTopChats' | 'requestChatUpdate' | 'updateChatMutedState' |
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |

31
src/hooks/useHotkeys.ts Normal file
View File

@ -0,0 +1,31 @@
// Original source from Mantine
// https://github.com/mantinedev/mantine/blob/master/src/mantine-hooks/src/use-hotkeys/
import { useEffect } from '../lib/teact/teact';
import { getHotkeyHandler, getHotkeyMatcher } from '../util/parseHotkey';
export { getHotkeyHandler };
export type HotkeyItem = [string, (event: KeyboardEvent) => void];
function shouldFireEvent(event: KeyboardEvent) {
if (event.target instanceof HTMLElement) {
return !['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName);
}
return true;
}
export function useHotkeys(hotkeys: HotkeyItem[]) {
useEffect(() => {
const keydownListener = (event: KeyboardEvent) => {
hotkeys.forEach(([hotkey, handler]) => {
if (getHotkeyMatcher(hotkey)(event) && shouldFireEvent(event)) {
handler(event);
}
});
};
document.documentElement.addEventListener('keydown', keydownListener);
return () => document.documentElement.removeEventListener('keydown', keydownListener);
}, [hotkeys]);
}

View File

@ -1,27 +1,17 @@
import { useEffect } from '../lib/teact/teact'; import { useHotkeys } from './useHotkeys';
import { IS_MAC_OS } from '../util/environment';
import getKeyFromEvent from '../util/getKeyFromEvent';
import getMessageIdsForSelectedText from '../util/getMessageIdsForSelectedText'; import getMessageIdsForSelectedText from '../util/getMessageIdsForSelectedText';
const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { messageIds?: number[] }) => void) => { const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { messageIds?: number[] }) => void) => {
useEffect(() => { function handleCopy(e: KeyboardEvent) {
function handleCopy(e: KeyboardEvent) { const messageIds = getMessageIdsForSelectedText();
if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && getKeyFromEvent(e) === 'c') {
const messageIds = getMessageIdsForSelectedText();
if (messageIds && messageIds.length > 0) { if (messageIds && messageIds.length > 0) {
e.preventDefault(); e.preventDefault();
copyMessagesByIds({ messageIds }); copyMessagesByIds({ messageIds });
}
}
} }
}
document.addEventListener('keydown', handleCopy, false); useHotkeys([['meta+C', handleCopy]]);
return () => {
document.removeEventListener('keydown', handleCopy, false);
};
}, [copyMessagesByIds]);
}; };
export default useNativeCopySelectedMessages; export default useNativeCopySelectedMessages;

View File

@ -1849,4 +1849,8 @@ export default {
key: 'ChannelVisibility.Forwarding.Disabled', key: 'ChannelVisibility.Forwarding.Disabled',
value: 'Restrict Forwarding', value: 'Restrict Forwarding',
}, },
'Settings.TipsUsername': {
key: 'Settings.TipsUsername',
value: 'TelegramTips',
},
} as ApiLangPack; } as ApiLangPack;

90
src/util/parseHotkey.ts Normal file
View File

@ -0,0 +1,90 @@
// Original source from Mantine
// https://github.com/mantinedev/mantine/blob/master/src/mantine-hooks/src/use-hotkeys/parse-hotkey.ts
export type KeyboardModifiers = {
alt: boolean;
ctrl: boolean;
meta: boolean;
mod: boolean;
shift: boolean;
};
export type Hotkey = KeyboardModifiers & {
key?: string;
};
type HotkeyItem = [string, (event: React.KeyboardEvent<HTMLElement>) => void];
type CheckHotkeyMatch = (event: KeyboardEvent) => boolean;
export function parseHotkey(hotkey: string): Hotkey {
const keys = hotkey
.toLowerCase()
.split('+')
.map((part) => part.trim());
const modifiers: KeyboardModifiers = {
alt: keys.includes('alt'),
ctrl: keys.includes('ctrl'),
meta: keys.includes('meta'),
mod: keys.includes('mod'),
shift: keys.includes('shift'),
};
const reservedKeys = ['alt', 'ctrl', 'meta', 'shift', 'mod'];
const freeKey = keys.find((key) => !reservedKeys.includes(key));
return {
...modifiers,
key: freeKey,
};
}
function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean {
const {
alt, ctrl, meta, mod, shift, key,
} = hotkey;
const {
altKey, ctrlKey, metaKey, shiftKey, key: pressedKey,
} = event;
if (alt !== altKey) {
return false;
}
if (mod) {
if (!ctrlKey && !metaKey) {
return false;
}
} else {
if (ctrl !== ctrlKey) {
return false;
}
if (meta !== metaKey) {
return false;
}
}
if (shift !== shiftKey) {
return false;
}
return Boolean(key
&& (pressedKey.toLowerCase() === key.toLowerCase()
|| event.code.replace('Key', '').toLowerCase() === key.toLowerCase()));
}
export function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch {
return (event) => isExactHotkey(parseHotkey(hotkey), event);
}
export function getHotkeyHandler(hotkeys: HotkeyItem[]) {
return (event: React.KeyboardEvent<HTMLElement>) => {
hotkeys.forEach(([hotkey, handler]) => {
if (getHotkeyMatcher(hotkey)(event.nativeEvent)) {
event.preventDefault();
handler(event);
}
});
};
}