From 67da677b1bb29822c7207313cc0383bf4a54b10e Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 26 Apr 2022 17:08:44 +0200 Subject: [PATCH] Various UI fixes (#1840) --- src/components/common/Avatar.tsx | 16 ++-- src/components/left/LeftColumn.tsx | 44 +++++---- src/components/left/main/ChatList.tsx | 30 ++++--- src/components/left/main/LeftMainHeader.tsx | 5 +- src/components/middle/HeaderActions.tsx | 31 +++---- src/components/middle/MiddleHeader.scss | 1 + src/components/middle/composer/Composer.tsx | 26 +++--- .../middle/hooks/useCopySelectedMessages.ts | 24 ++--- src/components/middle/message/Invoice.scss | 10 ++- src/components/middle/message/Invoice.tsx | 2 +- .../right/management/Management.scss | 5 +- src/components/ui/ListItem.tsx | 4 +- src/config.ts | 2 - src/global/actions/api/chats.ts | 12 --- src/global/helpers/users.ts | 5 ++ src/global/types.ts | 2 +- src/hooks/useHotkeys.ts | 31 +++++++ src/hooks/useNativeCopySelectedMessages.ts | 26 ++---- src/util/fallbackLangPack.ts | 4 + src/util/parseHotkey.ts | 90 +++++++++++++++++++ 20 files changed, 247 insertions(+), 123 deletions(-) create mode 100644 src/hooks/useHotkeys.ts create mode 100644 src/util/parseHotkey.ts diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 1047d7684..b84f62b47 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -76,19 +76,20 @@ const Avatar: FC = ({ const lang = useLang(); let content: string | undefined = ''; + const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text); if (isSavedMessages) { - content = ; + content = ; } else if (isDeleted) { - content = ; + content = ; } else if (isReplies) { - content = ; + content = ; } else if (blobUrl) { content = ( ); @@ -125,7 +126,12 @@ const Avatar: FC = ({ const senderId = (user || chat) && (user || chat)!.id; return ( -
+
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
); diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index a372d7e75..2df0b76bd 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -5,11 +5,11 @@ import { getActions, withGlobal } from '../../global'; 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 getKeyFromEvent from '../../util/getKeyFromEvent'; import useFoldersReducer from '../../hooks/reducers/useFoldersReducer'; import { useResize } from '../../hooks/useResize'; +import { useHotkeys } from '../../hooks/useHotkeys'; import Transition from '../ui/Transition'; import LeftMain from './main/LeftMain'; @@ -25,6 +25,7 @@ type StateProps = { activeChatFolder: number; shouldSkipHistoryAnimations?: boolean; leftColumnWidth?: number; + currentUserId?: string; }; enum ContentType { @@ -47,6 +48,7 @@ const LeftColumn: FC = ({ activeChatFolder, shouldSkipHistoryAnimations, leftColumnWidth, + currentUserId, }) => { const { setGlobalSearchQuery, @@ -57,6 +59,7 @@ const LeftColumn: FC = ({ clearTwoFaError, setLeftColumnWidth, resetLeftColumnWidth, + openChat, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -255,25 +258,25 @@ const LeftColumn: FC = ({ [activeChatFolder, content, handleReset], ); - useEffect(() => { + const handleHotkeySearch = useCallback((e: KeyboardEvent) => { if (content === LeftColumnContent.GlobalSearch) { - return undefined; + return; } - function handleKeyDown(e: KeyboardEvent) { - if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.shiftKey && getKeyFromEvent(e) === 'f') { - e.preventDefault(); - setContent(LeftColumnContent.GlobalSearch); - } - } - - document.addEventListener('keydown', handleKeyDown, false); - - return () => { - document.removeEventListener('keydown', handleKeyDown, false); - }; + e.preventDefault(); + setContent(LeftColumnContent.GlobalSearch); }, [content]); + const handleHotkeySavedMessages = useCallback((e: KeyboardEvent) => { + e.preventDefault(); + openChat({ id: currentUserId }); + }, [currentUserId, openChat]); + + useHotkeys([ + ['mod+shift+F', handleHotkeySearch], + ['mod+shift+S', handleHotkeySavedMessages], + ]); + useEffect(() => { clearTwoFaError(); @@ -386,9 +389,16 @@ export default memo(withGlobal( }, shouldSkipHistoryAnimations, leftColumnWidth, + currentUserId, } = global; + return { - searchQuery: query, searchDate: date, activeChatFolder, shouldSkipHistoryAnimations, leftColumnWidth, + searchQuery: query, + searchDate: date, + activeChatFolder, + shouldSkipHistoryAnimations, + leftColumnWidth, + currentUserId, }; }, )(LeftColumn)); diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 9eca97bcd..f67f54066 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -19,6 +19,7 @@ import usePrevious from '../../../hooks/usePrevious'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useChatAnimationType } from './hooks'; +import { HotkeyItem, useHotkeys } from '../../../hooks/useHotkeys'; import InfiniteScroll from '../../ui/InfiniteScroll'; import Loading from '../../ui/Loading'; @@ -74,14 +75,29 @@ const ChatList: FC = ({ const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE); - // Support + and + to navigate between chats + // Support + 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 + to navigate between chats useEffect(() => { - if (!isActive || !orderedIds) { + if (!isActive || !orderedIds || !IS_PWA) { return undefined; } 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)/) || []; if (!digit) return; @@ -90,14 +106,6 @@ const ChatList: FC = ({ 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); diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 5acb466c2..7e35b3015 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -96,7 +96,6 @@ const LeftMainHeader: FC = ({ }) => { const { openChat, - openTipsChat, setGlobalSearchDate, setSettingOption, setGlobalSearchChatId, @@ -196,8 +195,8 @@ const LeftMainHeader: FC = ({ }, []); const handleOpenTipsChat = useCallback(() => { - openTipsChat({ langCode: lang.code }); - }, [lang.code, openTipsChat]); + openChatByUsername({ username: lang('Settings.TipsUsername') }); + }, [lang, openChatByUsername]); const isSearchFocused = ( Boolean(globalSearchChatId) diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index 9d5c6b22e..b1bd40f53 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -4,7 +4,6 @@ import React, { useRef, useCallback, useState, - useEffect, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; @@ -13,9 +12,8 @@ import { MAIN_THREAD_ID } from '../../api/types'; import { IAnchorPosition, ManagementScreens } from '../../types'; 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'; -import getKeyFromEvent from '../../util/getKeyFromEvent'; import { isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId, } from '../../global/helpers'; @@ -29,6 +27,7 @@ import { selectIsRightColumnShown, } from '../../global/selectors'; import useLang from '../../hooks/useLang'; +import { useHotkeys } from '../../hooks/useHotkeys'; import Button from '../ui/Button'; import HeaderMenuContainer from './HeaderMenuContainer.async'; @@ -143,27 +142,19 @@ const HeaderActions: FC = ({ requestCall({ userId: chatId }); } - useEffect(() => { - if (!canSearch) { - return undefined; + const handleHotkeySearchClick = useCallback((e: KeyboardEvent) => { + if (!canSearch || !IS_PWA || e.shiftKey) { + return; } - function handleKeyDown(e: KeyboardEvent) { - if ( - 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); - }; + e.preventDefault(); + handleSearchClick(); }, [canSearch, handleSearchClick]); + useHotkeys([ + ['meta+F', handleHotkeySearchClick], + ]); + const lang = useLang(); return ( diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index efcb267d6..660b2ffff 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -253,6 +253,7 @@ cursor: pointer; display: flex; align-items: center; + user-select: none; .info { display: flex; diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 2dcdc5c53..4f5b80fe3 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -1149,19 +1149,6 @@ const Composer: FC = ({ {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} )} - - = ({ onClose={closeBotCommandMenu} /> )} + + { - useEffect(() => { - function handleCopy(e: KeyboardEvent) { - if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && getKeyFromEvent(e) === 'c') { - e.preventDefault(); - copySelectedMessages(); - } + function handleCopy(e: KeyboardEvent) { + if (!isActive) { + return; } - if (isActive) { - document.addEventListener('keydown', handleCopy, false); - } + e.preventDefault(); + copySelectedMessages(); + } - return () => { - document.removeEventListener('keydown', handleCopy, false); - }; - }, [copySelectedMessages, isActive]); + useHotkeys([['meta+C', handleCopy]]); }; export default useCopySelectedMessages; diff --git a/src/components/middle/message/Invoice.scss b/src/components/middle/message/Invoice.scss index 086b59fd7..37645a1ff 100644 --- a/src/components/middle/message/Invoice.scss +++ b/src/components/middle/message/Invoice.scss @@ -6,9 +6,12 @@ .description { position: relative; + margin-top: 0.5rem; &.has-image { - margin: 1rem -0.5rem -0.375rem; + .content-inner:not(.forwarded-message) & { + margin: 0.5rem -0.5rem -0.375rem; + } .invoice-image { width: 100%; @@ -16,6 +19,11 @@ object-fit: cover; border-bottom-left-radius: var(--border-bottom-left-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 { diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index dc4542f48..51b11584a 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -72,7 +72,7 @@ const Invoice: FC = ({

{renderText(title)}

)} {text && ( -

{renderText(text, ['emoji', 'br'])}

+
{renderText(text, ['emoji', 'br'])}
)}
{photoUrl && ( diff --git a/src/components/right/management/Management.scss b/src/components/right/management/Management.scss index d1eaf0669..e7b67d7fc 100644 --- a/src/components/right/management/Management.scss +++ b/src/components/right/management/Management.scss @@ -163,7 +163,10 @@ &__filter { 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; flex-flow: row wrap; flex-shrink: 0; diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index f189c7b4d..b038b3b94 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -165,9 +165,9 @@ const ListItem: FC = ({ >
{ } }); -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) => { const listType = payload.listType as 'active' | 'archived'; const { onReplace } = payload; diff --git a/src/global/helpers/users.ts b/src/global/helpers/users.ts index 2b9a30cc4..5d7204e36 100644 --- a/src/global/helpers/users.ts +++ b/src/global/helpers/users.ts @@ -6,6 +6,7 @@ import { orderBy } from '../../util/iteratees'; import { LangFn } from '../../hooks/useLang'; import { getServerTime } from '../../util/serverTime'; import { prepareSearchWordsForNeedle } from '../../util/searchWords'; +import { formatPhoneNumber } from '../../util/phoneNumber'; const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6]; @@ -54,6 +55,10 @@ export function getUserFullName(user?: ApiUser) { return user.lastName; } + if (user.phoneNumber) { + return `+${formatPhoneNumber(user.phoneNumber)}`; + } + break; } diff --git a/src/global/types.ts b/src/global/types.ts index e8566ae1b..6d06f4842 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -818,7 +818,7 @@ export type NonTypedActionNames = ( 'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' | // chats 'preloadTopChatMessages' | 'loadAllChats' | 'openChatWithInfo' | 'openLinkedChat' | - 'openSupportChat' | 'openTipsChat' | 'focusMessageInComments' | 'openChatByPhoneNumber' | + 'openSupportChat' | 'focusMessageInComments' | 'openChatByPhoneNumber' | 'loadChatSettings' | 'loadFullChat' | 'loadTopChats' | 'requestChatUpdate' | 'updateChatMutedState' | 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | diff --git a/src/hooks/useHotkeys.ts b/src/hooks/useHotkeys.ts new file mode 100644 index 000000000..f3dd53617 --- /dev/null +++ b/src/hooks/useHotkeys.ts @@ -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]); +} diff --git a/src/hooks/useNativeCopySelectedMessages.ts b/src/hooks/useNativeCopySelectedMessages.ts index b93c345ce..5ad87beee 100644 --- a/src/hooks/useNativeCopySelectedMessages.ts +++ b/src/hooks/useNativeCopySelectedMessages.ts @@ -1,27 +1,17 @@ -import { useEffect } from '../lib/teact/teact'; -import { IS_MAC_OS } from '../util/environment'; -import getKeyFromEvent from '../util/getKeyFromEvent'; +import { useHotkeys } from './useHotkeys'; import getMessageIdsForSelectedText from '../util/getMessageIdsForSelectedText'; const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { messageIds?: number[] }) => void) => { - useEffect(() => { - function handleCopy(e: KeyboardEvent) { - if (((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && getKeyFromEvent(e) === 'c') { - const messageIds = getMessageIdsForSelectedText(); + function handleCopy(e: KeyboardEvent) { + const messageIds = getMessageIdsForSelectedText(); - if (messageIds && messageIds.length > 0) { - e.preventDefault(); - copyMessagesByIds({ messageIds }); - } - } + if (messageIds && messageIds.length > 0) { + e.preventDefault(); + copyMessagesByIds({ messageIds }); } + } - document.addEventListener('keydown', handleCopy, false); - - return () => { - document.removeEventListener('keydown', handleCopy, false); - }; - }, [copyMessagesByIds]); + useHotkeys([['meta+C', handleCopy]]); }; export default useNativeCopySelectedMessages; diff --git a/src/util/fallbackLangPack.ts b/src/util/fallbackLangPack.ts index f3a04004d..ace313981 100644 --- a/src/util/fallbackLangPack.ts +++ b/src/util/fallbackLangPack.ts @@ -1849,4 +1849,8 @@ export default { key: 'ChannelVisibility.Forwarding.Disabled', value: 'Restrict Forwarding', }, + 'Settings.TipsUsername': { + key: 'Settings.TipsUsername', + value: 'TelegramTips', + }, } as ApiLangPack; diff --git a/src/util/parseHotkey.ts b/src/util/parseHotkey.ts new file mode 100644 index 000000000..0dcee34ce --- /dev/null +++ b/src/util/parseHotkey.ts @@ -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) => 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) => { + hotkeys.forEach(([hotkey, handler]) => { + if (getHotkeyMatcher(hotkey)(event.nativeEvent)) { + event.preventDefault(); + handler(event); + } + }); + }; +}