Support more hotkeys (#1137)
This commit is contained in:
parent
45a3064153
commit
a09b488846
@ -8,10 +8,11 @@ import ChatList from './main/ChatList';
|
|||||||
import './ArchivedChats.scss';
|
import './ArchivedChats.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
|
isActive: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArchivedChats: FC<OwnProps> = ({ onReset }) => {
|
const ArchivedChats: FC<OwnProps> = ({ isActive, onReset }) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +29,7 @@ const ArchivedChats: FC<OwnProps> = ({ onReset }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<h3>{lang('ArchivedChats')}</h3>
|
<h3>{lang('ArchivedChats')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ChatList folderType="archived" noChatsText="Archive is empty." />
|
<ChatList folderType="archived" noChatsText="Archive is empty." isActive={isActive} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import './LeftColumn.scss';
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
searchDate?: number;
|
searchDate?: number;
|
||||||
|
activeChatFolder: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
@ -45,6 +46,7 @@ const RESET_TRANSITION_DELAY_MS = 250;
|
|||||||
const LeftColumn: FC<StateProps & DispatchProps> = ({
|
const LeftColumn: FC<StateProps & DispatchProps> = ({
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchDate,
|
searchDate,
|
||||||
|
activeChatFolder,
|
||||||
setGlobalSearchQuery,
|
setGlobalSearchQuery,
|
||||||
setGlobalSearchChatId,
|
setGlobalSearchChatId,
|
||||||
resetChatCreation,
|
resetChatCreation,
|
||||||
@ -188,6 +190,11 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content === LeftColumnContent.ChatList && activeChatFolder === 0) {
|
||||||
|
setContent(LeftColumnContent.GlobalSearch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setContent(LeftColumnContent.ChatList);
|
setContent(LeftColumnContent.ChatList);
|
||||||
setContactsFilter('');
|
setContactsFilter('');
|
||||||
setGlobalSearchQuery({ query: '' });
|
setGlobalSearchQuery({ query: '' });
|
||||||
@ -197,7 +204,10 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLastResetTime(Date.now());
|
setLastResetTime(Date.now());
|
||||||
}, RESET_TRANSITION_DELAY_MS);
|
}, RESET_TRANSITION_DELAY_MS);
|
||||||
}, [content, setGlobalSearchQuery, setGlobalSearchChatId, setGlobalSearchDate, resetChatCreation, settingsScreen]);
|
}, [
|
||||||
|
content, activeChatFolder, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId, resetChatCreation,
|
||||||
|
settingsScreen,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSearchQuery = useCallback((query: string) => {
|
const handleSearchQuery = useCallback((query: string) => {
|
||||||
if (content === LeftColumnContent.Contacts) {
|
if (content === LeftColumnContent.Contacts) {
|
||||||
@ -213,8 +223,10 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
}, [content, setGlobalSearchQuery, searchQuery]);
|
}, [content, setGlobalSearchQuery, searchQuery]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => (content !== LeftColumnContent.ChatList ? captureEscKeyListener(() => handleReset()) : undefined),
|
() => (content !== LeftColumnContent.ChatList || activeChatFolder === 0
|
||||||
[content, handleReset],
|
? captureEscKeyListener(() => handleReset())
|
||||||
|
: undefined),
|
||||||
|
[activeChatFolder, content, handleReset],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -232,11 +244,12 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
renderCount={RENDER_COUNT}
|
renderCount={RENDER_COUNT}
|
||||||
activeKey={contentType}
|
activeKey={contentType}
|
||||||
>
|
>
|
||||||
{() => {
|
{(isActive) => {
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case ContentType.Archived:
|
case ContentType.Archived:
|
||||||
return (
|
return (
|
||||||
<ArchivedChats
|
<ArchivedChats
|
||||||
|
isActive={isActive}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -287,8 +300,16 @@ const LeftColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const { query, date } = global.globalSearch;
|
const {
|
||||||
return { searchQuery: query, searchDate: date };
|
globalSearch: {
|
||||||
|
query,
|
||||||
|
date,
|
||||||
|
},
|
||||||
|
chatFolders: {
|
||||||
|
activeChatFolder,
|
||||||
|
},
|
||||||
|
} = global;
|
||||||
|
return { searchQuery: query, searchDate: date, activeChatFolder };
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
'setGlobalSearchQuery', 'setGlobalSearchChatId', 'resetChatCreation', 'setGlobalSearchDate',
|
'setGlobalSearchQuery', 'setGlobalSearchChatId', 'resetChatCreation', 'setGlobalSearchDate',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
|
FC, memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -29,12 +29,15 @@ type StateProps = {
|
|||||||
notifySettings: NotifySettings;
|
notifySettings: NotifySettings;
|
||||||
notifyExceptions?: Record<number, NotifyException>;
|
notifyExceptions?: Record<number, NotifyException>;
|
||||||
orderedFolderIds?: number[];
|
orderedFolderIds?: number[];
|
||||||
|
activeChatFolder: number;
|
||||||
|
currentUserId?: number;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'loadChatFolders'>;
|
type DispatchProps = Pick<GlobalActions, 'loadChatFolders' | 'setActiveChatFolder' | 'openChat'>;
|
||||||
|
|
||||||
const INFO_THROTTLE = 3000;
|
const INFO_THROTTLE = 3000;
|
||||||
|
const SAVED_MESSAGES_HOTKEY = '0';
|
||||||
|
|
||||||
const ChatFolders: FC<StateProps & DispatchProps> = ({
|
const ChatFolders: FC<StateProps & DispatchProps> = ({
|
||||||
chatsById,
|
chatsById,
|
||||||
@ -43,16 +46,18 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
|
|||||||
notifySettings,
|
notifySettings,
|
||||||
notifyExceptions,
|
notifyExceptions,
|
||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
|
activeChatFolder,
|
||||||
|
currentUserId,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
loadChatFolders,
|
loadChatFolders,
|
||||||
|
setActiveChatFolder,
|
||||||
|
openChat,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const transitionRef = useRef<HTMLDivElement>(null);
|
const transitionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSyncTime) {
|
if (lastSyncTime) {
|
||||||
loadChatFolders();
|
loadChatFolders();
|
||||||
@ -101,8 +106,8 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
|
|||||||
}, [displayedFolders, folderCountersById, lang]);
|
}, [displayedFolders, folderCountersById, lang]);
|
||||||
|
|
||||||
const handleSwitchTab = useCallback((index: number) => {
|
const handleSwitchTab = useCallback((index: number) => {
|
||||||
setActiveTab(index);
|
setActiveChatFolder(index);
|
||||||
}, []);
|
}, [setActiveChatFolder]);
|
||||||
|
|
||||||
// Prevent `activeTab` pointing at non-existing folder after update
|
// Prevent `activeTab` pointing at non-existing folder after update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -110,10 +115,10 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTab >= folderTabs.length) {
|
if (activeChatFolder >= folderTabs.length) {
|
||||||
setActiveTab(0);
|
setActiveChatFolder(0);
|
||||||
}
|
}
|
||||||
}, [activeTab, folderTabs]);
|
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!transitionRef.current || !IS_TOUCH_ENV || !folderTabs || !folderTabs.length) {
|
if (!transitionRef.current || !IS_TOUCH_ENV || !folderTabs || !folderTabs.length) {
|
||||||
@ -123,48 +128,81 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
|
|||||||
return captureEvents(transitionRef.current, {
|
return captureEvents(transitionRef.current, {
|
||||||
onSwipe: ((e, direction) => {
|
onSwipe: ((e, direction) => {
|
||||||
if (direction === SwipeDirection.Left) {
|
if (direction === SwipeDirection.Left) {
|
||||||
setActiveTab(Math.min(activeTab + 1, folderTabs.length - 1));
|
setActiveChatFolder(Math.min(activeChatFolder + 1, folderTabs.length - 1));
|
||||||
} else if (direction === SwipeDirection.Right) {
|
} else if (direction === SwipeDirection.Right) {
|
||||||
setActiveTab(Math.max(0, activeTab - 1));
|
setActiveChatFolder(Math.max(0, activeChatFolder - 1));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}, [activeTab, folderTabs]);
|
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
|
||||||
|
|
||||||
const isNotInAllTabRef = useRef();
|
const isNotInAllTabRef = useRef();
|
||||||
isNotInAllTabRef.current = activeTab !== 0;
|
isNotInAllTabRef.current = activeChatFolder !== 0;
|
||||||
useEffect(() => captureEscKeyListener(() => {
|
useEffect(() => (isNotInAllTabRef.current ? captureEscKeyListener(() => {
|
||||||
if (isNotInAllTabRef.current) {
|
if (isNotInAllTabRef.current) {
|
||||||
setActiveTab(0);
|
setActiveChatFolder(0);
|
||||||
}
|
}
|
||||||
}), []);
|
}) : undefined), [activeChatFolder, setActiveChatFolder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.metaKey && e.code.startsWith('Digit') && folderTabs) {
|
||||||
|
const [, digit] = e.code.match(/Digit(\d)/) || [];
|
||||||
|
if (!digit) return;
|
||||||
|
|
||||||
|
if (digit === SAVED_MESSAGES_HOTKEY) {
|
||||||
|
openChat({ id: currentUserId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = Number(digit) - 1;
|
||||||
|
if (folder > folderTabs.length - 1) return;
|
||||||
|
|
||||||
|
setActiveChatFolder(folder);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown, true);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderPlaceholder, transitionClassNames,
|
shouldRender: shouldRenderPlaceholder, transitionClassNames,
|
||||||
} = useShowTransition(!orderedFolderIds, undefined, true);
|
} = useShowTransition(!orderedFolderIds, undefined, true);
|
||||||
|
|
||||||
function renderCurrentTab() {
|
function renderCurrentTab(isActive: boolean) {
|
||||||
const activeFolder = Object.values(chatFoldersById)
|
const activeFolder = Object.values(chatFoldersById)
|
||||||
.find(({ title }) => title === folderTabs![activeTab].title);
|
.find(({ title }) => title === folderTabs![activeChatFolder].title);
|
||||||
|
|
||||||
if (!activeFolder || activeTab === 0) {
|
if (!activeFolder || activeChatFolder === 0) {
|
||||||
return <ChatList folderType="all" />;
|
return <ChatList folderType="all" isActive={isActive} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ChatList folderType="folder" folderId={activeFolder.id} noChatsText={lang('FilterNoChatsToDisplay')} />;
|
return (
|
||||||
|
<ChatList
|
||||||
|
folderType="folder"
|
||||||
|
folderId={activeFolder.id}
|
||||||
|
noChatsText={lang('FilterNoChatsToDisplay')}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ChatFolders">
|
<div className="ChatFolders">
|
||||||
{folderTabs && folderTabs.length ? (
|
{folderTabs && folderTabs.length ? (
|
||||||
<TabList tabs={folderTabs} activeTab={activeTab} onSwitchTab={handleSwitchTab} />
|
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} />
|
||||||
) : shouldRenderPlaceholder ? (
|
) : shouldRenderPlaceholder ? (
|
||||||
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Transition
|
<Transition
|
||||||
ref={transitionRef}
|
ref={transitionRef}
|
||||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||||
activeKey={activeTab}
|
activeKey={activeChatFolder}
|
||||||
renderCount={folderTabs ? folderTabs.length : undefined}
|
renderCount={folderTabs ? folderTabs.length : undefined}
|
||||||
>
|
>
|
||||||
{renderCurrentTab}
|
{renderCurrentTab}
|
||||||
@ -181,7 +219,9 @@ export default memo(withGlobal(
|
|||||||
chatFolders: {
|
chatFolders: {
|
||||||
byId: chatFoldersById,
|
byId: chatFoldersById,
|
||||||
orderedIds: orderedFolderIds,
|
orderedIds: orderedFolderIds,
|
||||||
|
activeChatFolder,
|
||||||
},
|
},
|
||||||
|
currentUserId,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
@ -193,7 +233,13 @@ export default memo(withGlobal(
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
notifySettings: selectNotifySettings(global),
|
notifySettings: selectNotifySettings(global),
|
||||||
notifyExceptions: selectNotifyExceptions(global),
|
notifyExceptions: selectNotifyExceptions(global),
|
||||||
|
activeChatFolder,
|
||||||
|
currentUserId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['loadChatFolders']),
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
'loadChatFolders',
|
||||||
|
'setActiveChatFolder',
|
||||||
|
'openChat',
|
||||||
|
]),
|
||||||
)(ChatFolders));
|
)(ChatFolders));
|
||||||
|
|||||||
@ -28,6 +28,7 @@ type OwnProps = {
|
|||||||
folderType: 'all' | 'archived' | 'folder';
|
folderType: 'all' | 'archived' | 'folder';
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
noChatsText?: string;
|
noChatsText?: string;
|
||||||
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -43,7 +44,7 @@ type StateProps = {
|
|||||||
notifyExceptions?: Record<number, NotifyException>;
|
notifyExceptions?: Record<number, NotifyException>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages'>;
|
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat'>;
|
||||||
|
|
||||||
enum FolderTypeToListType {
|
enum FolderTypeToListType {
|
||||||
'all' = 'active',
|
'all' = 'active',
|
||||||
@ -54,6 +55,7 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
folderType,
|
folderType,
|
||||||
folderId,
|
folderId,
|
||||||
noChatsText = 'Chat list is empty.',
|
noChatsText = 'Chat list is empty.',
|
||||||
|
isActive,
|
||||||
chatFolder,
|
chatFolder,
|
||||||
chatsById,
|
chatsById,
|
||||||
usersById,
|
usersById,
|
||||||
@ -66,6 +68,7 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
notifyExceptions,
|
notifyExceptions,
|
||||||
loadMoreChats,
|
loadMoreChats,
|
||||||
preloadTopChatMessages,
|
preloadTopChatMessages,
|
||||||
|
openChat,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentListIds, currentPinnedIds] = useMemo(() => {
|
const [currentListIds, currentPinnedIds] = useMemo(() => {
|
||||||
return folderType === 'folder' && chatFolder
|
return folderType === 'folder' && chatFolder
|
||||||
@ -161,6 +164,49 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (isActive && orderedIds) {
|
||||||
|
if (e.ctrlKey && e.code.startsWith('Digit')) {
|
||||||
|
const [, digit] = e.code.match(/Digit(\d)/) || [];
|
||||||
|
if (!digit) return;
|
||||||
|
|
||||||
|
const position = Number(digit) - 1;
|
||||||
|
if (position > orderedIds.length - 1) return;
|
||||||
|
|
||||||
|
openChat({ id: orderedIds[position] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.altKey) {
|
||||||
|
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
|
||||||
|
if (!targetIndexDelta) return;
|
||||||
|
|
||||||
|
if (!currentChatId) {
|
||||||
|
e.preventDefault();
|
||||||
|
openChat({ id: orderedIds[0] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = orderedIds.indexOf(currentChatId);
|
||||||
|
|
||||||
|
if (position === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextId = orderedIds[position + targetIndexDelta];
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
openChat({ id: nextId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown, false);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className="chat-list custom-scroll"
|
className="chat-list custom-scroll"
|
||||||
@ -213,5 +259,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
notifyExceptions: selectNotifyExceptions(global),
|
notifyExceptions: selectNotifyExceptions(global),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['loadMoreChats', 'preloadTopChatMessages']),
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
'loadMoreChats',
|
||||||
|
'preloadTopChatMessages',
|
||||||
|
'openChat',
|
||||||
|
]),
|
||||||
)(ChatList));
|
)(ChatList));
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const LeftMain: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
<ConnectionState />
|
<ConnectionState />
|
||||||
<Transition name="zoom-fade" renderCount={TRANSITION_RENDER_COUNT} activeKey={content}>
|
<Transition name="zoom-fade" renderCount={TRANSITION_RENDER_COUNT} activeKey={content}>
|
||||||
{() => {
|
{(isActive) => {
|
||||||
switch (content) {
|
switch (content) {
|
||||||
case LeftColumnContent.ChatList:
|
case LeftColumnContent.ChatList:
|
||||||
return <ChatFolders />;
|
return <ChatFolders />;
|
||||||
@ -132,6 +132,7 @@ const LeftMain: FC<OwnProps & StateProps> = ({
|
|||||||
<LeftSearch
|
<LeftSearch
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
searchDate={searchDate}
|
searchDate={searchDate}
|
||||||
|
isActive={isActive}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -239,6 +239,7 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
inputId="telegram-search-input"
|
inputId="telegram-search-input"
|
||||||
|
parentContainerClassName="LeftSearch"
|
||||||
className={globalSearchChatId || searchDate ? 'with-picker-item' : ''}
|
className={globalSearchChatId || searchDate ? 'with-picker-item' : ''}
|
||||||
value={contactsFilter || searchQuery}
|
value={contactsFilter || searchQuery}
|
||||||
focused={isSearchFocused}
|
focused={isSearchFocused}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { pick } from '../../../util/iteratees';
|
|||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import { formatPastTimeShort } from '../../../util/dateFormat';
|
import { formatPastTimeShort } from '../../../util/dateFormat';
|
||||||
import useLang, { LangFn } from '../../../hooks/useLang';
|
import useLang, { LangFn } from '../../../hooks/useLang';
|
||||||
|
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import VerifiedIcon from '../../common/VerifiedIcon';
|
import VerifiedIcon from '../../common/VerifiedIcon';
|
||||||
@ -66,6 +67,8 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const buttonRef = useSelectWithEnter(handleClick);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -75,6 +78,7 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
className="ChatMessage chat-item-clickable"
|
className="ChatMessage chat-item-clickable"
|
||||||
ripple={!IS_MOBILE_SCREEN}
|
ripple={!IS_MOBILE_SCREEN}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
buttonRef={buttonRef}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
chat={chat}
|
chat={chat}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useState, useMemo,
|
FC, memo, useCallback, useState, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ import { GlobalSearchContent } from '../../../types';
|
|||||||
|
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { parseDateString } from '../../../util/dateFormat';
|
import { parseDateString } from '../../../util/dateFormat';
|
||||||
|
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import TabList from '../../ui/TabList';
|
import TabList from '../../ui/TabList';
|
||||||
@ -24,6 +25,7 @@ import './LeftSearch.scss';
|
|||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
searchDate?: number;
|
searchDate?: number;
|
||||||
|
isActive: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ const TRANSITION_RENDER_COUNT = Object.keys(GlobalSearchContent).length / 2;
|
|||||||
const LeftSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
const LeftSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchDate,
|
searchDate,
|
||||||
|
isActive,
|
||||||
currentContent = GlobalSearchContent.ChatList,
|
currentContent = GlobalSearchContent.ChatList,
|
||||||
chatId,
|
chatId,
|
||||||
setGlobalSearchContent,
|
setGlobalSearchContent,
|
||||||
@ -73,8 +76,12 @@ const LeftSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
setGlobalSearchDate({ date: value.getTime() / 1000 });
|
setGlobalSearchDate({ date: value.getTime() / 1000 });
|
||||||
}, [setGlobalSearchDate]);
|
}, [setGlobalSearchDate]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleKeyDown = useKeyboardListNavigation(containerRef, isActive, undefined, '.ListItem-button', true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LeftSearch">
|
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
|
||||||
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
|
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
|
||||||
<Transition
|
<Transition
|
||||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import React, { FC, memo } from '../../../lib/teact/teact';
|
import React, {
|
||||||
|
FC, memo,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiChat, ApiUser } from '../../../api/types';
|
import { ApiChat, ApiUser } from '../../../api/types';
|
||||||
@ -7,6 +9,7 @@ import useChatContextActions from '../../../hooks/useChatContextActions';
|
|||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import { isChatPrivate, getPrivateChatUserId } from '../../../modules/helpers';
|
import { isChatPrivate, getPrivateChatUserId } from '../../../modules/helpers';
|
||||||
import { selectChat, selectUser, selectIsChatPinned } from '../../../modules/selectors';
|
import { selectChat, selectUser, selectIsChatPinned } from '../../../modules/selectors';
|
||||||
|
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
|
||||||
|
|
||||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
import GroupChatInfo from '../../common/GroupChatInfo';
|
import GroupChatInfo from '../../common/GroupChatInfo';
|
||||||
@ -42,6 +45,12 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
handleDelete: openDeleteModal,
|
handleDelete: openDeleteModal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick(chatId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonRef = useSelectWithEnter(handleClick);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -49,8 +58,9 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
className="chat-item-clickable search-result"
|
className="chat-item-clickable search-result"
|
||||||
onClick={() => onClick(chatId)}
|
onClick={handleClick}
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
|
buttonRef={buttonRef}
|
||||||
>
|
>
|
||||||
{isChatPrivate(chatId) ? (
|
{isChatPrivate(chatId) ? (
|
||||||
<PrivateChatInfo userId={chatId} withUsername={withUsername} avatarSize="large" />
|
<PrivateChatInfo userId={chatId} withUsername={withUsername} avatarSize="large" />
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import searchWords from '../../util/searchWords';
|
|||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../util/iteratees';
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
@ -114,6 +115,14 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
setFilter(e.currentTarget.value);
|
setFilter(e.currentTarget.value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
||||||
|
if (viewportIds) {
|
||||||
|
setForwardChatId({ id: viewportIds[index] });
|
||||||
|
}
|
||||||
|
}, '.ListItem-button', true);
|
||||||
|
|
||||||
const modalHeader = (
|
const modalHeader = (
|
||||||
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<Button
|
<Button
|
||||||
@ -129,6 +138,7 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={lang('ForwardTo')}
|
placeholder={lang('ForwardTo')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -147,6 +157,8 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
items={viewportIds}
|
items={viewportIds}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
noScrollRestore={Boolean(filter)}
|
noScrollRestore={Boolean(filter)}
|
||||||
|
ref={containerRef}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
{viewportIds.map((id) => (
|
{viewportIds.map((id) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@ -50,7 +50,7 @@ type StateProps = {
|
|||||||
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
|
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'editLastMessage'>;
|
type DispatchProps = Pick<GlobalActions, 'editLastMessage' | 'replyToNextMessage'>;
|
||||||
|
|
||||||
const MAX_INPUT_HEIGHT = IS_MOBILE_SCREEN ? 256 : 416;
|
const MAX_INPUT_HEIGHT = IS_MOBILE_SCREEN ? 256 : 416;
|
||||||
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
|
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
|
||||||
@ -87,6 +87,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
noTabCapture,
|
noTabCapture,
|
||||||
messageSendKeyCombo,
|
messageSendKeyCombo,
|
||||||
editLastMessage,
|
editLastMessage,
|
||||||
|
replyToNextMessage,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
@ -226,6 +227,16 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
e.target.removeEventListener('keyup', handleKeyUp);
|
e.target.removeEventListener('keyup', handleKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.metaKey) {
|
||||||
|
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
|
||||||
|
if (targetIndexDelta) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
replyToNextMessage({ targetIndexDelta });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
if (
|
if (
|
||||||
!(IS_IOS || IS_ANDROID)
|
!(IS_IOS || IS_ANDROID)
|
||||||
@ -239,7 +250,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
closeTextFormatter();
|
closeTextFormatter();
|
||||||
onSend();
|
onSend();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowUp' && !html.length) {
|
} else if (e.key === 'ArrowUp' && !html.length && !e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
editLastMessage();
|
editLastMessage();
|
||||||
} else {
|
} else {
|
||||||
@ -391,5 +402,5 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
noTabCapture: global.isPollModalOpen || global.payment.isPaymentModalOpen,
|
noTabCapture: global.isPollModalOpen || global.payment.isPaymentModalOpen,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['editLastMessage']),
|
(setGlobal, actions): DispatchProps => pick(actions, ['editLastMessage', 'replyToNextMessage']),
|
||||||
)(MessageInput));
|
)(MessageInput));
|
||||||
|
|||||||
@ -197,6 +197,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
parentContainerClassName="RightSearch"
|
||||||
value={messageSearchQuery}
|
value={messageSearchQuery}
|
||||||
onChange={handleMessageSearchQueryChange}
|
onChange={handleMessageSearchQueryChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import React, { FC, useMemo, memo } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
FC, useMemo, memo, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
|
import { getGlobal, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMessage, ApiUser, ApiChat } from '../../api/types';
|
import { ApiMessage, ApiUser, ApiChat } from '../../api/types';
|
||||||
@ -20,6 +22,7 @@ import renderText from '../common/helpers/renderText';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import { orderBy, pick } from '../../util/iteratees';
|
import { orderBy, pick } from '../../util/iteratees';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
|
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||||
|
|
||||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||||
import ListItem from '../ui/ListItem';
|
import ListItem from '../ui/ListItem';
|
||||||
@ -122,6 +125,14 @@ const RightSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
|
||||||
|
if (foundResults && foundResults[index]) {
|
||||||
|
foundResults[index].onClick();
|
||||||
|
}
|
||||||
|
}, '.ListItem-button', true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className="RightSearch custom-scroll"
|
className="RightSearch custom-scroll"
|
||||||
@ -129,6 +140,8 @@ const RightSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
preloadBackwards={0}
|
preloadBackwards={0}
|
||||||
onLoadMore={searchTextMessagesLocal}
|
onLoadMore={searchTextMessagesLocal}
|
||||||
noFastList
|
noFastList
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<p className="helper-text" dir="auto">
|
<p className="helper-text" dir="auto">
|
||||||
{!query ? (
|
{!query ? (
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type OwnProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onLoadMore?: ({ direction }: { direction: LoadMoreDirection }) => void;
|
onLoadMore?: ({ direction }: { direction: LoadMoreDirection }) => void;
|
||||||
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
|
||||||
items?: any[];
|
items?: any[];
|
||||||
itemSelector?: string;
|
itemSelector?: string;
|
||||||
preloadBackwards?: number;
|
preloadBackwards?: number;
|
||||||
@ -33,6 +34,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
className,
|
className,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
onKeyDown,
|
||||||
items,
|
items,
|
||||||
itemSelector = DEFAULT_LIST_SELECTOR,
|
itemSelector = DEFAULT_LIST_SELECTOR,
|
||||||
preloadBackwards = DEFAULT_PRELOAD_BACKWARDS,
|
preloadBackwards = DEFAULT_PRELOAD_BACKWARDS,
|
||||||
@ -205,7 +207,13 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
}, [loadMoreBackwards, loadMoreForwards, onScroll, sensitiveArea]);
|
}, [loadMoreBackwards, loadMoreForwards, onScroll, sensitiveArea]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={className} onScroll={handleScroll} teactFastList={!noFastList}>
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
teactFastList={!noFastList}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type MenuItemContextAction = {
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
|
buttonRef?: RefObject<HTMLDivElement>;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
@ -43,6 +44,7 @@ type OwnProps = {
|
|||||||
const ListItem: FC<OwnProps> = (props) => {
|
const ListItem: FC<OwnProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
ref,
|
ref,
|
||||||
|
buttonRef,
|
||||||
icon,
|
icon,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
@ -141,6 +143,7 @@ const ListItem: FC<OwnProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className="ListItem-button"
|
className="ListItem-button"
|
||||||
role="button"
|
role="button"
|
||||||
|
ref={buttonRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={!inactive && IS_TOUCH_ENV ? handleClick : undefined}
|
onClick={!inactive && IS_TOUCH_ENV ? handleClick : undefined}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
import React, {
|
import React, {
|
||||||
FC, useRef, useEffect, memo,
|
FC, useRef, useEffect, memo, useCallback,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
@ -15,6 +15,7 @@ import './SearchInput.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: RefObject<HTMLInputElement>;
|
ref?: RefObject<HTMLInputElement>;
|
||||||
children?: any;
|
children?: any;
|
||||||
|
parentContainerClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -32,6 +33,7 @@ type OwnProps = {
|
|||||||
const SearchInput: FC<OwnProps> = ({
|
const SearchInput: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
children,
|
children,
|
||||||
|
parentContainerClassName,
|
||||||
value,
|
value,
|
||||||
inputId,
|
inputId,
|
||||||
className,
|
className,
|
||||||
@ -86,6 +88,15 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
const element = document.querySelector(`.${parentContainerClassName} .ListItem-button`) as HTMLElement;
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [parentContainerClassName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName('SearchInput', className, isInputFocused && 'has-focus')}
|
className={buildClassName('SearchInput', className, isInputFocused && 'has-focus')}
|
||||||
@ -104,6 +115,7 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<i className="icon-search" />
|
<i className="icon-search" />
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
chatFolders: {
|
chatFolders: {
|
||||||
byId: {},
|
byId: {},
|
||||||
|
activeChatFolder: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
fileUploads: {
|
fileUploads: {
|
||||||
|
|||||||
@ -149,6 +149,7 @@ export type GlobalState = {
|
|||||||
orderedIds?: number[];
|
orderedIds?: number[];
|
||||||
byId: Record<number, ApiChatFolder>;
|
byId: Record<number, ApiChatFolder>;
|
||||||
recommended?: ApiChatFolder[];
|
recommended?: ApiChatFolder[];
|
||||||
|
activeChatFolder: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
focusedMessage?: {
|
focusedMessage?: {
|
||||||
@ -404,14 +405,14 @@ export type ActionTypes = (
|
|||||||
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
|
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
|
||||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||||
'loadProfilePhotos' | 'loadMoreMembers' |
|
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' |
|
||||||
// messages
|
// messages
|
||||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||||
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
||||||
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'loadMessageLink' | 'toggleMessageWebPage' |
|
'loadMessageLink' | 'toggleMessageWebPage' | 'replyToNextMessage' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||||
// poll result
|
// poll result
|
||||||
|
|||||||
@ -4,13 +4,21 @@ import { useState, useCallback, useEffect } from '../lib/teact/teact';
|
|||||||
export default (
|
export default (
|
||||||
elementRef: RefObject<HTMLElement>,
|
elementRef: RefObject<HTMLElement>,
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
onSelectWithEnter?: () => void,
|
onSelectWithEnter?: (index: number) => void,
|
||||||
|
itemSelector?: string,
|
||||||
|
noCaptureFocus?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocusedIndex(-1);
|
setFocusedIndex(-1);
|
||||||
}, [isOpen]);
|
|
||||||
|
const element = elementRef.current;
|
||||||
|
if (isOpen && element && !noCaptureFocus) {
|
||||||
|
element.tabIndex = -1;
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}, [elementRef, isOpen, noCaptureFocus]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<any>) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<any>) => {
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
@ -20,7 +28,7 @@ export default (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 13 && onSelectWithEnter) {
|
if (e.keyCode === 13 && onSelectWithEnter) {
|
||||||
onSelectWithEnter();
|
onSelectWithEnter(focusedIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +37,7 @@ export default (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const focusedElement = document.activeElement;
|
const focusedElement = document.activeElement;
|
||||||
const elementChildren = Array.from(element.children);
|
const elementChildren = Array.from(itemSelector ? element.querySelectorAll(itemSelector) : element.children);
|
||||||
|
|
||||||
let newIndex = (focusedElement && elementChildren.indexOf(focusedElement)) || focusedIndex;
|
let newIndex = (focusedElement && elementChildren.indexOf(focusedElement)) || focusedIndex;
|
||||||
|
|
||||||
@ -48,7 +56,7 @@ export default (
|
|||||||
setFocusedIndex(newIndex);
|
setFocusedIndex(newIndex);
|
||||||
item.focus();
|
item.focus();
|
||||||
}
|
}
|
||||||
}, [focusedIndex, elementRef, onSelectWithEnter]);
|
}, [elementRef, onSelectWithEnter, itemSelector, focusedIndex]);
|
||||||
|
|
||||||
return handleKeyDown;
|
return handleKeyDown;
|
||||||
};
|
};
|
||||||
|
|||||||
25
src/hooks/useSelectWithEnter.ts
Normal file
25
src/hooks/useSelectWithEnter.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
onSelect: NoneToVoidFunction,
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
const isFocused = buttonRef.current === document.activeElement;
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
}, [onSelect]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
|
return buttonRef;
|
||||||
|
};
|
||||||
@ -664,6 +664,17 @@ addReducer('unlinkDiscussionGroup', (global, actions, payload) => {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
addReducer('setActiveChatFolder', (global, actions, payload) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
chatFolders: {
|
||||||
|
...global.chatFolders,
|
||||||
|
activeChatFolder: payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('loadMoreMembers', (global) => {
|
addReducer('loadMoreMembers', (global) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const { chatId } = selectCurrentMessageList(global) || {};
|
const { chatId } = selectCurrentMessageList(global) || {};
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
selectAllowedMessageActions,
|
selectAllowedMessageActions,
|
||||||
selectMessageIdsByGroupId,
|
selectMessageIdsByGroupId,
|
||||||
selectForwardedMessageIdsByGroupId,
|
selectForwardedMessageIdsByGroupId, selectIsViewportNewest, selectReplyingToId,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { findLast } from '../../../util/iteratees';
|
import { findLast } from '../../../util/iteratees';
|
||||||
|
|
||||||
@ -83,6 +83,48 @@ addReducer('editLastMessage', (global) => {
|
|||||||
return replaceThreadParam(global, chatId, threadId, 'editingId', lastOwnEditableMessageId);
|
return replaceThreadParam(global, chatId, threadId, 'editingId', lastOwnEditableMessageId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('replyToNextMessage', (global, actions, payload) => {
|
||||||
|
const { targetIndexDelta } = payload;
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
||||||
|
if (!chatId || !threadId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatMessages = selectChatMessages(global, chatId);
|
||||||
|
const viewportIds = selectViewportIds(global, chatId, threadId);
|
||||||
|
if (!chatMessages || !viewportIds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
||||||
|
const isLatest = selectIsViewportNewest(global, chatId, threadId);
|
||||||
|
|
||||||
|
let messageId: number | undefined;
|
||||||
|
|
||||||
|
if (!isLatest || !replyingToId) {
|
||||||
|
if (threadId === MAIN_THREAD_ID) {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
messageId = chat && chat.lastMessage ? chat.lastMessage.id : undefined;
|
||||||
|
} else {
|
||||||
|
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||||
|
|
||||||
|
messageId = threadInfo ? threadInfo.lastMessageId : undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const chatMessageKeys = Object.keys(chatMessages);
|
||||||
|
const indexOfCurrent = chatMessageKeys.indexOf(replyingToId.toString());
|
||||||
|
const newIndex = indexOfCurrent + targetIndexDelta;
|
||||||
|
messageId = newIndex <= chatMessageKeys.length + 1 && newIndex >= 0
|
||||||
|
? Number(chatMessageKeys[newIndex])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
actions.setReplyingToId({ messageId });
|
||||||
|
actions.focusMessage({
|
||||||
|
chatId, threadId, messageId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('openMediaViewer', (global, actions, payload) => {
|
addReducer('openMediaViewer', (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin,
|
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user