Navigation: Support quick chat picker (#6635)

This commit is contained in:
Alexander Zinchuk 2026-02-22 23:42:42 +01:00
parent 184c2a88e8
commit 1140242b0e
12 changed files with 108 additions and 5 deletions

View File

@ -107,3 +107,4 @@ export { default as FrozenAccountModal } from '../components/modals/frozenAccoun
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal';

View File

@ -67,7 +67,9 @@
.picker-list {
overflow-x: hidden;
overflow-y: auto;
height: 100%;
padding-block: 0.125rem;
padding-inline: 0.5rem;
@include mixins.adapt-padding-to-scrollbar(0.5rem);

View File

@ -1,4 +1,6 @@
.root {
scroll-margin-block: 0.25rem;
position: relative;
overflow: hidden;
@ -9,7 +11,7 @@
min-height: 2.5rem;
padding: 0.25rem;
border-radius: var(--border-radius-default);
border-radius: 1.25rem;
line-height: 1.25;
color: var(--color-text);
@ -40,10 +42,14 @@
cursor: var(--custom-cursor, pointer);
@media (hover: hover) {
&:hover,
&:focus-visible {
&:hover {
background-color: var(--color-item-hover);
}
&:focus-visible {
z-index: 1;
outline: 2px solid var(--color-borders);
}
}
}

View File

@ -20,6 +20,7 @@ import {
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { resolveTransitionName } from '../../util/resolveTransitionName';
import { captureControlledSwipe } from '../../util/swipeController';
import { isComposerHasSelection } from '../middle/composer/helpers/selection';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
import { useHotkeys } from '../../hooks/useHotkeys';
@ -110,6 +111,7 @@ function LeftColumn({
openChat,
openLeftColumnContent,
openSettingsScreen,
openQuickChatPicker,
} = getActions();
const [contactsFilter, setContactsFilter] = useState<string>('');
@ -436,8 +438,16 @@ function LeftColumn({
openLeftColumnContent({ contentKey: LeftColumnContent.Settings });
});
const handleQuickChatPicker = useLastCallback((e: KeyboardEvent) => {
if (isComposerHasSelection()) return;
e.preventDefault();
openQuickChatPicker();
});
useHotkeys(useMemo(() => ({
'Mod+Shift+F': handleHotkeySearch,
'Mod+K': handleQuickChatPicker,
// https://support.mozilla.org/en-US/kb/take-screenshots-firefox
...(!IS_FIREFOX && {
'Mod+Shift+S': handleHotkeySavedMessages,

View File

@ -1,3 +1,5 @@
import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config';
const MAX_NESTING_PARENTS = 5;
export function isSelectionInsideInput(selectionRange: Range, inputId: string) {
@ -11,3 +13,14 @@ export function isSelectionInsideInput(selectionRange: Range, inputId: string) {
return Boolean(parentNode && parentNode.id === inputId);
}
export function isComposerHasSelection() {
const activeElement = document.activeElement;
const isComposerFocused = activeElement?.id === EDITABLE_INPUT_ID
|| activeElement?.id === EDITABLE_INPUT_MODAL_ID;
if (!isComposerFocused) return false;
const selection = window.getSelection();
return Boolean(selection && !selection.isCollapsed);
}

View File

@ -49,6 +49,7 @@ import PasskeyModal from './passkey/PasskeyModal.async';
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
import QuickChatPickerModal from './quickChatPicker/QuickChatPickerModal.async';
import QuickPreviewModal from './quickPreview/QuickPreviewModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import ReportModal from './reportModal/ReportModal.async';
@ -126,7 +127,8 @@ type ModalKey = keyof Pick<TabState,
'quickPreview' |
'storyStealthModal' |
'isPasskeyModalOpen' |
'birthdaySetupModal'
'birthdaySetupModal' |
'isQuickChatPickerOpen'
>;
type StateProps = {
@ -201,6 +203,7 @@ const MODALS: ModalRegistry = {
storyStealthModal: StealthModeModal,
isPasskeyModalOpen: PasskeyModal,
birthdaySetupModal: BirthdaySetupModal,
isQuickChatPickerOpen: QuickChatPickerModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './QuickChatPickerModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const QuickChatPickerModalAsync = (props: OwnProps) => {
const { modal } = props;
const QuickChatPickerModal = useModuleLoader(Bundles.Extra, 'QuickChatPickerModal', !modal);
return QuickChatPickerModal ? <QuickChatPickerModal {...props} /> : undefined;
};
export default QuickChatPickerModalAsync;

View File

@ -0,0 +1,36 @@
import { memo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import RecipientPicker from '../../common/RecipientPicker';
export type OwnProps = {
modal?: boolean;
};
const QuickChatPickerModal = ({
modal,
}: OwnProps) => {
const { closeQuickChatPicker, openChat } = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const handleSelectRecipient = useLastCallback((peerId: string) => {
openChat({ id: peerId });
closeQuickChatPicker();
});
return (
<RecipientPicker
isOpen={isOpen}
searchPlaceholder={lang('Search')}
onSelectRecipient={handleSelectRecipient}
onClose={closeQuickChatPicker}
/>
);
};
export default memo(QuickChatPickerModal);

View File

@ -951,3 +951,13 @@ addCallback((global: GlobalState) => {
prevIsScreenLocked = global.passcode.isScreenLocked;
prevBlurredTabsCount = blurredTabsCount;
});
addActionHandler('openQuickChatPicker', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
isQuickChatPickerOpen: true,
}, tabId);
});
addTabStateResetterAction('closeQuickChatPicker', 'isQuickChatPickerOpen');

View File

@ -2556,6 +2556,9 @@ export interface ActionPayloads {
openGiftRecipientPicker: WithTabId | undefined;
closeGiftRecipientPicker: WithTabId | undefined;
openQuickChatPicker: WithTabId | undefined;
closeQuickChatPicker: WithTabId | undefined;
openWebAppsCloseConfirmationModal: WithTabId | undefined;
closeWebAppsCloseConfirmationModal: ({

View File

@ -681,6 +681,8 @@ export type TabState = {
isGiftRecipientPickerOpen?: boolean;
isQuickChatPickerOpen?: boolean;
isFrozenAccountModalOpen?: boolean;
starsGiftingPickerModal?: {

View File

@ -41,6 +41,8 @@ const useKeyboardListNavigation = (
return;
}
e.preventDefault();
const focusedElement = document.activeElement;
const elementChildren = Array.from(itemSelector ? element.querySelectorAll(itemSelector) : element.children);
@ -59,7 +61,8 @@ const useKeyboardListNavigation = (
const item = elementChildren[newIndex] as HTMLElement;
if (item) {
setFocusedIndex(newIndex);
item.focus();
item.focus({ preventScroll: true });
item.scrollIntoView({ behavior: 'instant', block: 'nearest' });
}
});
};