From 8faa652eb6daf500d38e2cd6c17b5d6a95199c35 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 15 Jul 2024 15:50:50 +0200 Subject: [PATCH] Settings / Folders: Fix layout in Settings Folders Chats Picker component (#4668) --- src/components/common/Picker.tsx | 16 +- ...SettingsPrivacyVisibilityExceptionList.tsx | 1 + .../settings/folders/SettingsFolders.scss | 13 + .../folders/SettingsFoldersChatFilters.tsx | 111 +++++--- .../folders/SettingsFoldersChatsPicker.scss | 63 ----- .../folders/SettingsFoldersChatsPicker.tsx | 262 ------------------ .../settings/folders/SettingsFoldersEdit.tsx | 12 +- src/types/index.ts | 3 +- src/util/objects/customPeer.ts | 70 +++++ 9 files changed, 181 insertions(+), 370 deletions(-) delete mode 100644 src/components/left/settings/folders/SettingsFoldersChatsPicker.scss delete mode 100644 src/components/left/settings/folders/SettingsFoldersChatsPicker.tsx diff --git a/src/components/common/Picker.tsx b/src/components/common/Picker.tsx index fac77a002..99022348a 100644 --- a/src/components/common/Picker.tsx +++ b/src/components/common/Picker.tsx @@ -37,6 +37,7 @@ type OwnProps = { lockedUnselectedSubtitle?: string; filterValue?: string; filterPlaceholder?: string; + categoryPlaceholderKey?: string; notFoundText?: string; searchInputId?: string; isLoading?: boolean; @@ -65,6 +66,7 @@ const Picker: FC = ({ categories, itemIds, selectedCategories, + categoryPlaceholderKey, selectedIds, filterValue, filterPlaceholder, @@ -171,7 +173,9 @@ const Picker: FC = ({ onFilterChange?.(value); }); - const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, sortedItemIds, Boolean(filterValue)); + const [viewportIds, getMore] = useInfiniteScroll( + onLoadMore, sortedItemIds, Boolean(filterValue), + ); const lang = useOldLang(); @@ -247,13 +251,15 @@ const Picker: FC = ({ return (
{Boolean(categories?.length) && ( -
{lang('PrivacyUserTypes')}
+ <> + {categoryPlaceholderKey &&
{lang(categoryPlaceholderKey)}
} + {categories?.map((category) => renderItem(category.type, true))} +
{lang('FilterChats')}
+ )} - {categories?.map((category) => renderItem(category.type, true))} -
{lang('FilterChats')}
); - }, [categories, lang, renderItem]); + }, [categories, categoryPlaceholderKey, lang, renderItem]); return (
diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index 4a6b8ad5d..095bfa618 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -146,6 +146,7 @@ const SettingsPrivacyVisibilityExceptionList: FC = ({ selectedCategories={newSelectedCategoryTypes} filterValue={searchQuery} filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')} + categoryPlaceholderKey="PrivacyUserTypes" searchInputId="new-group-picker-search" isSearchable onSelectedIdsChange={handleSelectedContactIdsChange} diff --git a/src/components/left/settings/folders/SettingsFolders.scss b/src/components/left/settings/folders/SettingsFolders.scss index 76969bf2f..7dffb44ae 100644 --- a/src/components/left/settings/folders/SettingsFolders.scss +++ b/src/components/left/settings/folders/SettingsFolders.scss @@ -94,3 +94,16 @@ .settings-sortable-item .multiline-item { padding-inline-end: 3rem; } + +.settings-folders-chat-list { + padding: 0.5rem; +} + +.picker-category-title { + padding-top: 0.75rem; +} + +.down { + font-size: 1.5rem; + margin-right: 0.5rem; +} diff --git a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx index cc6f7c8f5..2fbed0f23 100644 --- a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx +++ b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx @@ -1,25 +1,31 @@ import type { FC } from '../../../../lib/teact/teact'; -import React, { memo, useCallback, useMemo } from '../../../../lib/teact/teact'; -import { getGlobal } from '../../../../global'; +import React, { + memo, useEffect, useMemo, useState, +} from '../../../../lib/teact/teact'; +import { getActions, getGlobal, withGlobal } from '../../../../global'; -import type { - FolderEditDispatch, - FoldersState, -} from '../../../../hooks/reducers/useFoldersReducer'; +import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../../config'; import { filterChatsByName } from '../../../../global/helpers'; +import { selectCurrentLimit } from '../../../../global/selectors/limits'; import { unique } from '../../../../util/iteratees'; +import { CUSTOM_PEER_EXCLUDED_CHAT_TYPES, CUSTOM_PEER_INCLUDED_CHAT_TYPES } from '../../../../util/objects/customPeer'; -import { - selectChatFilters, -} from '../../../../hooks/reducers/useFoldersReducer'; +import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer'; import { useFolderManagerForOrderedIds } from '../../../../hooks/useFolderManager'; import useHistoryBack from '../../../../hooks/useHistoryBack'; +import useLastCallback from '../../../../hooks/useLastCallback'; import useOldLang from '../../../../hooks/useOldLang'; +import Icon from '../../../common/icons/Icon'; +import Picker from '../../../common/Picker'; +import FloatingActionButton from '../../../ui/FloatingActionButton'; import Loading from '../../../ui/Loading'; -import SettingsFoldersChatsPicker from './SettingsFoldersChatsPicker'; + +type StateProps = { + maxChats: number; +}; type OwnProps = { mode: 'included' | 'excluded'; @@ -30,44 +36,62 @@ type OwnProps = { onSaveFilter: VoidFunction; }; -const SettingsFoldersChatFilters: FC = ({ +const SettingsFoldersChatFilters: FC = ({ mode, state, dispatch, isActive, onReset, onSaveFilter, + maxChats, }) => { + const lang = useOldLang(); + + const { openLimitReachedModal } = getActions(); + const { chatFilter } = state; const { selectedChatIds, selectedChatTypes } = selectChatFilters(state, mode, true); + const chatTypes = mode === 'included' ? CUSTOM_PEER_INCLUDED_CHAT_TYPES : CUSTOM_PEER_EXCLUDED_CHAT_TYPES; - const lang = useOldLang(); + const [isTouched, setIsTouched] = useState(false); const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID); const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID); const shouldHideChatTypes = state.folder.isChatList; + useEffect(() => { + if (!isActive) { + setIsTouched(false); + } + }, [isActive]); + const displayedIds = useMemo(() => { // No need for expensive global updates on chats, so we avoid them const chatsById = getGlobal().chats.byId; const chatIds = [...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []]; return unique([ - ...selectedChatIds, ...filterChatsByName(lang, chatIds, chatsById, chatFilter), ]); - }, [folderAllOrderedIds, folderArchivedOrderedIds, selectedChatIds, lang, chatFilter]); + }, [folderAllOrderedIds, folderArchivedOrderedIds, lang, chatFilter]); - const handleFilterChange = useCallback((newFilter: string) => { + const handleFilterChange = useLastCallback((newFilter: string) => { dispatch({ type: 'setChatFilter', payload: newFilter, }); - }, [dispatch]); + setIsTouched(true); + }); - const handleSelectedIdsChange = useCallback((ids: string[]) => { + const handleSelectedIdsChange = useLastCallback((ids: string[]) => { if (mode === 'included') { + if (ids.length >= maxChats) { + openLimitReachedModal({ + limit: 'dialogFiltersChats', + }); + return; + } dispatch({ type: 'setIncludeFilters', payload: { ...state.includeFilters, includedChatIds: ids }, @@ -78,9 +102,10 @@ const SettingsFoldersChatFilters: FC = ({ payload: { ...state.excludeFilters, excludedChatIds: ids }, }); } - }, [mode, state, dispatch]); + setIsTouched(true); + }); - const handleSelectedChatTypesChange = useCallback((keys: string[]) => { + const handleSelectedChatTypesChange = useLastCallback((keys: string[]) => { const newFilters: Record = {}; keys.forEach((key) => { newFilters[key] = true; @@ -103,7 +128,7 @@ const SettingsFoldersChatFilters: FC = ({ }, }); } - }, [mode, selectedChatIds, dispatch]); + }); useHistoryBack({ isActive, @@ -115,20 +140,38 @@ const SettingsFoldersChatFilters: FC = ({ } return ( - +
+ + + + + +
); }; -export default memo(SettingsFoldersChatFilters); +export default memo(withGlobal( + (global): StateProps => { + return { + maxChats: selectCurrentLimit(global, 'dialogFiltersChats'), + }; + }, +)(SettingsFoldersChatFilters)); diff --git a/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss b/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss deleted file mode 100644 index 9384d8bb8..000000000 --- a/src/components/left/settings/folders/SettingsFoldersChatsPicker.scss +++ /dev/null @@ -1,63 +0,0 @@ -.SettingsFoldersChatsPicker { - height: calc(100% - var(--header-height)); - - .picker-header { - box-shadow: 0 0 2px var(--color-default-shadow); - - .max-items-reached { - margin-bottom: 0.5rem; - margin-left: 0.5rem; - flex-grow: 1; - color: var(--color-text-secondary); - } - } - - .picker-list { - padding: 0 0.5rem 0.5rem; - - .no-results { - height: 10rem; - } - } - - .ListItem.picker-list-item { - &.chat-type-item .ListItem-button { - padding: 0.875rem 0.75rem; - } - - &.chat-item .ListItem-button { - padding: 0.5rem 0.75rem; - } - - .Checkbox { - margin-left: auto; - padding-left: 3.25rem; - } - - .chat-type { - font-size: 1rem; - font-weight: 400; - margin: 0; - } - - &[dir="rtl"] { - .Checkbox { - margin-left: 0; - margin-right: auto; - padding-left: 0; - padding-right: 3.25rem; - } - } - } - - .settings-item-header { - margin-left: 0.75rem; - margin-top: 1rem; - margin-bottom: 0.5rem; - } - - .picker-list-divider { - margin: 0.5rem -0.5rem 0; - border-bottom: 1px solid var(--color-borders); - } -} diff --git a/src/components/left/settings/folders/SettingsFoldersChatsPicker.tsx b/src/components/left/settings/folders/SettingsFoldersChatsPicker.tsx deleted file mode 100644 index a23e3703a..000000000 --- a/src/components/left/settings/folders/SettingsFoldersChatsPicker.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import type { FC } from '../../../../lib/teact/teact'; -import React, { - memo, useCallback, useEffect, useRef, useState, -} from '../../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../../global'; - -import type { FolderChatType } from '../../../../hooks/reducers/useFoldersReducer'; - -import { requestMutation } from '../../../../lib/fasterdom/fasterdom'; -import { isUserId } from '../../../../global/helpers'; -import { selectCurrentLimit } from '../../../../global/selectors/limits'; -import buildClassName from '../../../../util/buildClassName'; - -import { - EXCLUDED_CHAT_TYPES, - INCLUDED_CHAT_TYPES, -} from '../../../../hooks/reducers/useFoldersReducer'; -import useInfiniteScroll from '../../../../hooks/useInfiniteScroll'; -import useOldLang from '../../../../hooks/useOldLang'; - -import GroupChatInfo from '../../../common/GroupChatInfo'; -import PickerSelectedItem from '../../../common/PickerSelectedItem'; -import PrivateChatInfo from '../../../common/PrivateChatInfo'; -import Checkbox from '../../../ui/Checkbox'; -import FloatingActionButton from '../../../ui/FloatingActionButton'; -import InfiniteScroll from '../../../ui/InfiniteScroll'; -import InputText from '../../../ui/InputText'; -import ListItem from '../../../ui/ListItem'; -import Loading from '../../../ui/Loading'; - -import '../../../common/Picker.scss'; -import './SettingsFoldersChatsPicker.scss'; - -type OwnProps = { - mode: 'included' | 'excluded'; - chatIds: string[]; - selectedIds: string[]; - selectedChatTypes: string[]; - filterValue?: string; - shouldHideChatTypes?: boolean; - onSelectedIdsChange: (ids: string[]) => void; - onSelectedChatTypesChange: (types: string[]) => void; - onFilterChange: (value: string) => void; - onSaveFilter: VoidFunction; - isActive?: boolean; -}; - -// Focus slows down animation, also it breaks transition layout in Chrome -const FOCUS_DELAY_MS = 500; - -const MAX_FULL_ITEMS = 10; -const ALWAYS_FULL_ITEMS_COUNT = 5; - -type StateProps = { - maxChats: number; -}; - -const SettingsFoldersChatsPicker: FC = ({ - mode, - chatIds, - selectedIds, - selectedChatTypes, - filterValue, - shouldHideChatTypes, - onSelectedIdsChange, - onSelectedChatTypesChange, - onFilterChange, - maxChats, - onSaveFilter, - isActive, -}) => { - const { openLimitReachedModal } = getActions(); - // eslint-disable-next-line no-null/no-null - const inputRef = useRef(null); - const chatTypes = mode === 'included' ? INCLUDED_CHAT_TYPES : EXCLUDED_CHAT_TYPES; - const shouldMinimize = selectedIds.length + selectedChatTypes.length > MAX_FULL_ITEMS; - const [isTouched, setIsTouched] = useState(false); - - useEffect(() => { - if (!isActive) { - setIsTouched(false); - } - }, [isActive]); - - useEffect(() => { - setTimeout(() => { - requestMutation(() => { - inputRef.current!.focus(); - }); - }, FOCUS_DELAY_MS); - }, []); - - const handleItemClick = useCallback((id: string) => { - const newSelectedIds = [...selectedIds]; - if (newSelectedIds.includes(id)) { - newSelectedIds.splice(newSelectedIds.indexOf(id), 1); - } else { - if (selectedIds.length >= maxChats && mode === 'included') { - openLimitReachedModal({ - limit: 'dialogFiltersChats', - }); - return; - } - newSelectedIds.push(id); - } - setIsTouched(true); - onSelectedIdsChange(newSelectedIds); - }, [selectedIds, onSelectedIdsChange, maxChats, mode, openLimitReachedModal]); - - const handleChatTypeClick = useCallback((key: FolderChatType['key']) => { - const newSelectedChatTypes = [...selectedChatTypes]; - if (newSelectedChatTypes.includes(key)) { - newSelectedChatTypes.splice(newSelectedChatTypes.indexOf(key), 1); - } else { - newSelectedChatTypes.push(key); - } - setIsTouched(true); - onSelectedChatTypesChange(newSelectedChatTypes); - }, [selectedChatTypes, onSelectedChatTypesChange]); - - const handleFilterChange = useCallback((e: React.ChangeEvent) => { - const { value } = e.currentTarget; - onFilterChange(value); - }, [onFilterChange]); - - const lang = useOldLang(); - - function renderSelectedChatType(key: string) { - const selectedType = chatTypes.find(({ key: typeKey }) => key === typeKey); - if (!selectedType) { - return undefined; - } - - return ( - - ); - } - - function renderChatType(type: FolderChatType) { - return ( - handleChatTypeClick(type.key)} - ripple - > - -

{lang(type.title)}

- -
- ); - } - - function renderItem(id: string) { - const isSelected = selectedIds.includes(id); - - return ( - handleItemClick(id)} - ripple - > - {isUserId(id) ? ( - - ) : ( - - )} - - - ); - } - - const [viewportIds, getMore] = useInfiniteScroll(undefined, chatIds, Boolean(filterValue)); - - return ( -
-
- {selectedChatTypes.map(renderSelectedChatType)} - {selectedIds.map((id, i) => ( - - ))} - -
- - {(!viewportIds || !viewportIds.length || viewportIds.includes(chatIds[0])) && ( -
- {!shouldHideChatTypes && ( - <> -

- {lang('FilterChatTypes')} -

- {chatTypes.map(renderChatType)} -
- - )} -

- {lang('FilterChats')} -

-
- )} - - {viewportIds?.length ? ( - viewportIds.map(renderItem) - ) : viewportIds && !viewportIds.length ? ( -

Sorry, nothing found.

- ) : ( - - )} - - - - - -
- ); -}; - -export default memo(withGlobal( - (global): StateProps => { - return { - maxChats: selectCurrentLimit(global, 'dialogFiltersChats'), - }; - }, -)(SettingsFoldersChatsPicker)); diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index d5ad6fd67..81d92d4da 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -28,11 +28,11 @@ import useOldLang from '../../../../hooks/useOldLang'; import AnimatedIcon from '../../../common/AnimatedIcon'; import GroupChatInfo from '../../../common/GroupChatInfo'; +import Icon from '../../../common/icons/Icon'; import PrivateChatInfo from '../../../common/PrivateChatInfo'; import FloatingActionButton from '../../../ui/FloatingActionButton'; import InputText from '../../../ui/InputText'; import ListItem from '../../../ui/ListItem'; -import ShowMoreButton from '../../../ui/ShowMoreButton'; import Spinner from '../../../ui/Spinner'; type OwnProps = { @@ -265,12 +265,14 @@ const SettingsFoldersEdit: FC = ({ ))} {(!isExpanded && leftChatsCount > 0) && ( - + > + + {lang('FilterShowMoreChats', leftChatsCount, 'i')} + )} ); diff --git a/src/types/index.ts b/src/types/index.ts index 180ef44ae..c543d7c6f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -483,7 +483,8 @@ export type InlineBotSettings = { cacheTime: number; }; -export type CustomPeerType = 'premium' | 'toBeDistributed'; +export type CustomPeerType = 'premium' | 'toBeDistributed' | 'contacts' | 'nonContacts' +| 'groups' | 'channels' | 'bots' | 'excludeMuted' | 'excludeArchived' | 'excludeRead'; export interface CustomPeer { isCustomPeer: true; diff --git a/src/util/objects/customPeer.ts b/src/util/objects/customPeer.ts index 9088a02e9..5f058af27 100644 --- a/src/util/objects/customPeer.ts +++ b/src/util/objects/customPeer.ts @@ -17,3 +17,73 @@ export const CUSTOM_PEER_TO_BE_DISTRIBUTED: UniqueCustomPeer = { avatarIcon: 'user', withPremiumGradient: true, }; + +export const CUSTOM_PEER_INCLUDED_CHAT_TYPES: UniqueCustomPeer[] = [ + { + isCustomPeer: true, + type: 'contacts', + titleKey: 'FilterContacts', + avatarIcon: 'user', + isAvatarSquare: true, + peerColorId: 5, + }, + { + isCustomPeer: true, + type: 'nonContacts', + titleKey: 'FilterNonContacts', + avatarIcon: 'non-contacts', + isAvatarSquare: true, + peerColorId: 4, + }, + { + isCustomPeer: true, + type: 'groups', + titleKey: 'FilterGroups', + avatarIcon: 'group', + isAvatarSquare: true, + peerColorId: 3, + }, + { + isCustomPeer: true, + type: 'channels', + titleKey: 'FilterChannels', + avatarIcon: 'channel', + isAvatarSquare: true, + peerColorId: 1, + }, + { + isCustomPeer: true, + type: 'bots', + titleKey: 'FilterBots', + avatarIcon: 'bots', + isAvatarSquare: true, + peerColorId: 6, + }, +]; + +export const CUSTOM_PEER_EXCLUDED_CHAT_TYPES: UniqueCustomPeer[] = [ + { + isCustomPeer: true, + type: 'excludeMuted', + titleKey: 'FilterMuted', + avatarIcon: 'mute', + isAvatarSquare: true, + peerColorId: 6, + }, + { + isCustomPeer: true, + type: 'excludeRead', + titleKey: 'FilterRead', + avatarIcon: 'readchats', + isAvatarSquare: true, + peerColorId: 4, + }, + { + isCustomPeer: true, + type: 'excludeArchived', + titleKey: 'FilterArchived', + avatarIcon: 'archive', + isAvatarSquare: true, + peerColorId: 5, + }, +];