diff --git a/src/components/common/AllUsersAndChats.tsx b/src/components/common/AllUsersAndChats.tsx new file mode 100644 index 0000000000..48c4b9cf85 --- /dev/null +++ b/src/components/common/AllUsersAndChats.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Command } from 'cmdk'; +import { useCallback, useMemo } from '../../lib/teact/teact'; +import { getActions, getGlobal } from '../../global'; + +import type { ApiChat, ApiUser } from '../../api/types'; + +import { + getChatTitle, + getChatTypeString, + getMainUsername, getUserFullName, isDeletedUser, +} from '../../global/helpers'; +import { convertLayout } from '../../util/convertLayout'; +import { unique } from '../../util/iteratees'; +import renderText from './helpers/renderText'; + +import useLang from '../../hooks/useLang'; + +const AllUsersAndChats: React.FC< +{ close: () => void; searchQuery: string; topUserIds: string[] }> = ({ close, searchQuery, topUserIds }) => { + const global = getGlobal(); + const usersById: Record = global.users.byId; + const chatsById: Record = global.chats.byId; + const { openChat, addRecentlyFoundChatId } = getActions(); + const SEARCH_CLOSE_TIMEOUT_MS = 250; + + const lang = useLang(); + + function getGroupStatus(chat: ApiChat) { + const chatTypeString = lang(getChatTypeString(chat)); + const { membersCount } = chat; + + if (chat.isRestricted) { + return chatTypeString === 'Channel' ? 'channel is inaccessible' : 'group is inaccessible'; + } + + if (!membersCount) { + return chatTypeString; + } + + return chatTypeString === 'Channel' + ? lang('Subscribers', membersCount, 'i') + : lang('Members', membersCount, 'i'); + } + + const renderName = (id: string, isUser: boolean): { content: React.ReactNode; value: string } => { + const NBSP = '\u00A0'; + let content: React.ReactNode; + let value: string; + + if (isUser) { + const user = usersById[id] as ApiUser; + if (isDeletedUser(user)) { + return { content: undefined, value: '' }; + } + const name = getUserFullName(user) || NBSP; + const handle = getMainUsername(user) || NBSP; + const renderedName = renderText(name); + content = React.isValidElement(renderedName) ? renderedName : ( + + {name} + {handle !== NBSP ? `@${handle}` : ''} + + ); + value = `${name} ${handle !== NBSP ? handle : ''}`.trim(); + } else { + const chat = chatsById[id] as ApiChat; + const title = getChatTitle(lang, chat) || 'Unknown Chat'; + const groupStatus = getGroupStatus(chat); + content = ( + + {title} + {groupStatus} + + ); + value = title; + } + + return { content, value }; + }; + + const handleClick = useCallback((id: string) => { + openChat({ id, shouldReplaceHistory: true }); + setTimeout(() => addRecentlyFoundChatId({ id }), SEARCH_CLOSE_TIMEOUT_MS); + close(); + }, [openChat, addRecentlyFoundChatId, close]); + + const handeSelect = useCallback((id: string) => () => handleClick(id), [handleClick]); + + const ids = useMemo(() => { + const convertedSearchQuery = convertLayout(searchQuery).toLowerCase(); + const userAndChatIds = unique([...Object.keys(usersById), ...Object.keys(chatsById)]); + return userAndChatIds.filter((id) => { + if (topUserIds && topUserIds.slice(0, 3).includes(id)) { + return false; + } + const isUser = usersById.hasOwnProperty(id); + if (isUser) { + const user = usersById[id]; + if (isDeletedUser(user)) return false; + const name = getUserFullName(user) || ''; // Запасной вариант для 'undefined' + return name.toLowerCase().includes(searchQuery.toLowerCase()) + || name.toLowerCase().includes(convertedSearchQuery); + } else { + const chat = chatsById[id]; + const title = getChatTitle(lang, chat) || ''; // Запасной вариант для 'undefined' + return title.toLowerCase().includes(searchQuery.toLowerCase()) + || title.toLowerCase().includes(convertedSearchQuery); + } + }); + }, [usersById, chatsById, searchQuery, lang, topUserIds]); + + if (!searchQuery) { + // eslint-disable-next-line no-null/no-null + return null; + } + + return ( + + {ids.map((id) => { + const isUser = usersById.hasOwnProperty(id); + const { content, value } = renderName(id, isUser); + // eslint-disable-next-line no-null/no-null + if (!content) return null; + + return ( + + {content} + + ); + })} + + ); +}; + +export default AllUsersAndChats; diff --git a/src/components/main/CommandMenu.scss b/src/components/main/CommandMenu.scss index da00902058..563677b688 100644 --- a/src/components/main/CommandMenu.scss +++ b/src/components/main/CommandMenu.scss @@ -172,11 +172,61 @@ display: flex; align-items: center; justify-content: center; - height: 48px; + height: 0px; white-space: pre-wrap; color: var(--gray11); } +.global-search { + display: none; +} + +/* Показать элемент глобального поиска, когда нет результатов */ +[cmdk-empty] + .global-search { + content-visibility: auto; + + cursor: pointer; + background: none; + width: 100%; + border: none; + margin: 0; + height: 48px; + border-radius: 8px; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px !important; + color: var(--gray11); + user-select: none; + will-change: background, color; + transition: background 150ms ease, color 150ms ease; + transition-property: none; + + &[data-selected='true'] { + background: #ffffff14; + } + + &[data-disabled='true'] { + opacity: 0.4; + cursor: not-allowed; + } + + &:active { + transition-property: background; + background: var(--color-chat-active); + } + + & + [cmdk-item] { + margin-top: 4px; + } +} + +.global-search:hover, .global-search[data-selected='true'] { + /* Ваши стили для hover, например изменение фона */ + background: #ffffff14; /* Пример фона при наведении */ +} + .cmdk-backdrop { position: fixed; left: -100vw; @@ -215,3 +265,8 @@ margin-left: 0.625rem; color: #aaaaaa } + +.chat-status { +margin-left: 0.625rem; +color: #aaaaaa + } \ No newline at end of file diff --git a/src/components/main/CommandMenu.tsx b/src/components/main/CommandMenu.tsx index 14d5d624e4..d77f75edf6 100644 --- a/src/components/main/CommandMenu.tsx +++ b/src/components/main/CommandMenu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/self-closing-comp */ /* eslint-disable arrow-parens */ /* eslint-disable react/no-array-index-key */ /* eslint-disable @typescript-eslint/no-shadow */ @@ -26,6 +27,8 @@ import useArchiver from '../../hooks/useArchiver'; import useCommands from '../../hooks/useCommands'; import { useJune } from '../../hooks/useJune'; +import AllUsersAndChats from '../common/AllUsersAndChats'; + import './CommandMenu.scss'; const cmdkElement = document.getElementById('cmdk-root'); @@ -92,7 +95,7 @@ const SuggestedContacts: FC = ({ topUserIds, usersById, return ( - {topUserIds.map((userId) => { + {topUserIds.slice(0, 3).map((userId) => { // take the first 3 elements const { displayedName, valueString } = renderName(userId); return ( handleClick(userId)}> @@ -302,6 +305,7 @@ const CommandMenu: FC = ({ topUserIds, usersById }) => { const close = useCallback(() => { setOpen(false); setPages(['home']); + setInputValue(''); }, []); // Toggle the menu when ⌘K is pressed @@ -473,13 +477,12 @@ const CommandMenu: FC = ({ topUserIds, usersById }) => { onValueChange={handleInputChange} value={inputValue} onKeyDown={(e) => { - if (e.key === 'Backspace') { + if (e.key === 'Backspace' && inputValue === '') { handleBack(); } }} /> - No results found. {activePage === 'home' && ( = ({ topUserIds, usersById }) => { handleCreateFolder={handleCreateFolder} /> )} + + + );