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();
let content: string | undefined = '';
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
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) {
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) {
content = <i className={buildClassName(cn.icon, 'icon-reply-filled')} />;
content = <i className={buildClassName(cn.icon, 'icon-reply-filled')} aria-label={author} />;
} else if (blobUrl) {
content = (
<img
src={blobUrl}
className={buildClassName(cn.img, 'avatar-media', transitionClassNames)}
alt=""
alt={author}
decoding="async"
/>
);
@ -125,7 +126,12 @@ const Avatar: FC<OwnProps> = ({
const senderId = (user || chat) && (user || chat)!.id;
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}
</div>
);

View File

@ -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<StateProps> = ({
activeChatFolder,
shouldSkipHistoryAnimations,
leftColumnWidth,
currentUserId,
}) => {
const {
setGlobalSearchQuery,
@ -57,6 +59,7 @@ const LeftColumn: FC<StateProps> = ({
clearTwoFaError,
setLeftColumnWidth,
resetLeftColumnWidth,
openChat,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -255,25 +258,25 @@ const LeftColumn: FC<StateProps> = ({
[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));

View File

@ -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<OwnProps> = ({
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(() => {
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<OwnProps> = ({
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);

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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 (

View File

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

View File

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

View File

@ -1,24 +1,16 @@
import { useEffect } from '../../../lib/teact/teact';
import { IS_MAC_OS } from '../../../util/environment';
import getKeyFromEvent from '../../../util/getKeyFromEvent';
import { useHotkeys } from '../../../hooks/useHotkeys';
const useCopySelectedMessages = (isActive: boolean, copySelectedMessages: NoneToVoidFunction) => {
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;

View File

@ -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 {

View File

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

View File

@ -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;

View File

@ -165,9 +165,9 @@ const ListItem: FC<OwnProps> = ({
>
<div
className={buildClassName('ListItem-button', isTouched && 'active')}
role="button"
role={!isStatic ? 'button' : undefined}
ref={buttonRef}
tabIndex={0}
tabIndex={!isStatic ? 0 : undefined}
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : undefined}
onMouseDown={handleMouseDown}
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_PACK = 'android';
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 LIGHT_THEME_BG_COLOR = '#A2AF8E';
export const DARK_THEME_BG_COLOR = '#0F0F0F';

View File

@ -12,8 +12,6 @@ import {
ARCHIVED_FOLDER_ID,
TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
CHAT_LIST_LOAD_SLICE,
TIPS_USERNAME,
LOCALIZED_TIPS,
RE_TG_LINK,
SERVICE_NOTIFICATIONS_USER_ID,
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) => {
const listType = payload.listType as 'active' | 'archived';
const { onReplace } = payload;

View File

@ -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;
}

View File

@ -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' |

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 { 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;

View File

@ -1849,4 +1849,8 @@ export default {
key: 'ChannelVisibility.Forwarding.Disabled',
value: 'Restrict Forwarding',
},
'Settings.TipsUsername': {
key: 'Settings.TipsUsername',
value: 'TelegramTips',
},
} 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);
}
});
};
}