Settings / Folders: Fix layout in Settings Folders Chats Picker component (#4668)

This commit is contained in:
Alexander Zinchuk 2024-07-15 15:50:50 +02:00
parent 0f2c670e0f
commit 8faa652eb6
9 changed files with 181 additions and 370 deletions

View File

@ -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<OwnProps> = ({
categories,
itemIds,
selectedCategories,
categoryPlaceholderKey,
selectedIds,
filterValue,
filterPlaceholder,
@ -171,7 +173,9 @@ const Picker: FC<OwnProps> = ({
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<OwnProps> = ({
return (
<div key="categories">
{Boolean(categories?.length) && (
<div className="picker-category-title">{lang('PrivacyUserTypes')}</div>
<>
{categoryPlaceholderKey && <div className="picker-category-title">{lang(categoryPlaceholderKey)}</div>}
{categories?.map((category) => renderItem(category.type, true))}
<div className="picker-category-title">{lang('FilterChats')}</div>
</>
)}
{categories?.map((category) => renderItem(category.type, true))}
<div className="picker-category-title">{lang('FilterChats')}</div>
</div>
);
}, [categories, lang, renderItem]);
}, [categories, categoryPlaceholderKey, lang, renderItem]);
return (
<div className={buildClassName('Picker', className)}>

View File

@ -146,6 +146,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
selectedCategories={newSelectedCategoryTypes}
filterValue={searchQuery}
filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')}
categoryPlaceholderKey="PrivacyUserTypes"
searchInputId="new-group-picker-search"
isSearchable
onSelectedIdsChange={handleSelectedContactIdsChange}

View File

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

View File

@ -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<OwnProps> = ({
const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
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<OwnProps> = ({
payload: { ...state.excludeFilters, excludedChatIds: ids },
});
}
}, [mode, state, dispatch]);
setIsTouched(true);
});
const handleSelectedChatTypesChange = useCallback((keys: string[]) => {
const handleSelectedChatTypesChange = useLastCallback((keys: string[]) => {
const newFilters: Record<string, boolean> = {};
keys.forEach((key) => {
newFilters[key] = true;
@ -103,7 +128,7 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
},
});
}
}, [mode, selectedChatIds, dispatch]);
});
useHistoryBack({
isActive,
@ -115,20 +140,38 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
}
return (
<SettingsFoldersChatsPicker
mode={mode}
chatIds={displayedIds}
selectedIds={selectedChatIds}
selectedChatTypes={selectedChatTypes}
filterValue={chatFilter}
shouldHideChatTypes={shouldHideChatTypes}
onSelectedIdsChange={handleSelectedIdsChange}
onSelectedChatTypesChange={handleSelectedChatTypesChange}
onFilterChange={handleFilterChange}
onSaveFilter={onSaveFilter}
isActive={isActive}
/>
<div className="Picker settings-folders-chat-list">
<Picker
categories={shouldHideChatTypes ? undefined : chatTypes}
itemIds={displayedIds}
selectedIds={selectedChatIds}
selectedCategories={selectedChatTypes}
filterValue={chatFilter}
filterPlaceholder={lang('Search')}
categoryPlaceholderKey="FilterChatTypes"
searchInputId="new-group-picker-search"
isSearchable
isRoundCheckbox
onSelectedIdsChange={handleSelectedIdsChange}
onSelectedCategoriesChange={handleSelectedChatTypesChange}
onFilterChange={handleFilterChange}
/>
<FloatingActionButton
isShown={isTouched}
onClick={onSaveFilter}
ariaLabel={lang('Save')}
>
<Icon name="check" />
</FloatingActionButton>
</div>
);
};
export default memo(SettingsFoldersChatFilters);
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
maxChats: selectCurrentLimit(global, 'dialogFiltersChats'),
};
},
)(SettingsFoldersChatFilters));

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<PickerSelectedItem
icon={selectedType.icon}
title={lang(selectedType.title)}
isMinimized={shouldMinimize}
canClose
onClick={handleChatTypeClick}
clickArg={selectedType.key}
/>
);
}
function renderChatType(type: FolderChatType) {
return (
<ListItem
key={type.key}
className="chat-item-clickable picker-list-item chat-type-item"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleChatTypeClick(type.key)}
ripple
>
<i className={buildClassName('icon', `icon-${type.icon}`)} />
<h3 className="chat-type" dir="auto">{lang(type.title)}</h3>
<Checkbox
label=""
checked={selectedChatTypes.includes(type.key)}
round
/>
</ListItem>
);
}
function renderItem(id: string) {
const isSelected = selectedIds.includes(id);
return (
<ListItem
key={id}
className="chat-item-clickable picker-list-item chat-item"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleItemClick(id)}
ripple
>
{isUserId(id) ? (
<PrivateChatInfo userId={id} />
) : (
<GroupChatInfo chatId={id} withChatType />
)}
<Checkbox
label=""
checked={isSelected}
round
/>
</ListItem>
);
}
const [viewportIds, getMore] = useInfiniteScroll(undefined, chatIds, Boolean(filterValue));
return (
<div className="Picker SettingsFoldersChatsPicker">
<div className="picker-header custom-scroll">
{selectedChatTypes.map(renderSelectedChatType)}
{selectedIds.map((id, i) => (
<PickerSelectedItem
peerId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
canClose
onClick={handleItemClick}
clickArg={id}
/>
))}
<InputText
ref={inputRef}
value={filterValue}
onChange={handleFilterChange}
placeholder={lang('Search')}
/>
</div>
<InfiniteScroll
className="picker-list custom-scroll fab-padding-bottom"
itemSelector=".chat-item"
items={viewportIds}
onLoadMore={getMore}
>
{(!viewportIds || !viewportIds.length || viewportIds.includes(chatIds[0])) && (
<div key="header">
{!shouldHideChatTypes && (
<>
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterChatTypes')}
</h4>
{chatTypes.map(renderChatType)}
<div className="picker-list-divider" />
</>
)}
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterChats')}
</h4>
</div>
)}
{viewportIds?.length ? (
viewportIds.map(renderItem)
) : viewportIds && !viewportIds.length ? (
<p className="no-results" key="no-results">Sorry, nothing found.</p>
) : (
<Loading key="loading" />
)}
</InfiniteScroll>
<FloatingActionButton
isShown={isTouched}
onClick={onSaveFilter}
ariaLabel={lang('Save')}
>
<i className="icon icon-check" />
</FloatingActionButton>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
maxChats: selectCurrentLimit(global, 'dialogFiltersChats'),
};
},
)(SettingsFoldersChatsPicker));

View File

@ -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<OwnProps & StateProps> = ({
</ListItem>
))}
{(!isExpanded && leftChatsCount > 0) && (
<ShowMoreButton
count={leftChatsCount}
itemName="chat"
<ListItem
key="load-more"
// eslint-disable-next-line react/jsx-no-bind
onClick={clickHandler}
/>
>
<Icon name="down" className="down" />
{lang('FilterShowMoreChats', leftChatsCount, 'i')}
</ListItem>
)}
</>
);

View File

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

View File

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