mirror of
https://github.com/danog/telegram-tt.git
synced 2024-11-26 20:34:44 +01:00
Settings / Folders: Support re-ordering (#1973)
This commit is contained in:
parent
3493f247eb
commit
6445847af0
@ -831,6 +831,12 @@ export async function deleteChatFolder(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export function sortChatFolders(ids: number[]) {
|
||||
return invokeRequest(new GramJs.messages.UpdateDialogFiltersOrder({
|
||||
order: ids,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function toggleDialogUnread({
|
||||
chat, hasUnreadMark,
|
||||
}: {
|
||||
|
@ -15,7 +15,7 @@ export {
|
||||
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
||||
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
||||
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
||||
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
||||
fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders,
|
||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||
|
Binary file not shown.
Binary file not shown.
@ -76,3 +76,15 @@
|
||||
width: calc(100% + 2rem);
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.settings-sortable-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-sortable-container .draggable-knob {
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.settings-sortable-item .multiline-item {
|
||||
padding-inline-end: 3rem;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useCallback, useEffect,
|
||||
memo, useMemo, useCallback, useEffect, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
@ -21,6 +21,7 @@ import ListItem from '../../../ui/ListItem';
|
||||
import Button from '../../../ui/Button';
|
||||
import Loading from '../../../ui/Loading';
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import Draggable from '../../../ui/Draggable';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
@ -30,13 +31,20 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
orderedFolderIds?: number[];
|
||||
folderIds?: number[];
|
||||
foldersById: Record<number, ApiChatFolder>;
|
||||
recommendedChatFolders?: ApiChatFolder[];
|
||||
maxFolders: number;
|
||||
isPremium?: boolean;
|
||||
};
|
||||
|
||||
type SortState = {
|
||||
orderedFolderIds?: number[];
|
||||
dragOrderIds?: number[];
|
||||
draggedIndex?: number;
|
||||
};
|
||||
|
||||
const FOLDER_HEIGHT_PX = 68;
|
||||
const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
|
||||
|
||||
const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
@ -44,7 +52,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
onCreateFolder,
|
||||
onEditFolder,
|
||||
onReset,
|
||||
orderedFolderIds,
|
||||
folderIds,
|
||||
foldersById,
|
||||
isPremium,
|
||||
recommendedChatFolders,
|
||||
@ -55,8 +63,15 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
addChatFolder,
|
||||
openLimitReachedModal,
|
||||
openDeleteChatFolderModal,
|
||||
sortChatFolders,
|
||||
} = getActions();
|
||||
|
||||
const [state, setState] = useState<SortState>({
|
||||
orderedFolderIds: folderIds,
|
||||
dragOrderIds: folderIds,
|
||||
draggedIndex: undefined,
|
||||
});
|
||||
|
||||
// Due to the parent Transition, this component never gets unmounted,
|
||||
// that's why we use throttled API call on every update.
|
||||
useEffect(() => {
|
||||
@ -86,15 +101,15 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const chatsCountByFolderId = useFolderManagerForChatsCount();
|
||||
const userFolders = useMemo(() => {
|
||||
if (!orderedFolderIds) {
|
||||
if (!state.orderedFolderIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (orderedFolderIds.length <= 1) {
|
||||
if (state.orderedFolderIds.length <= 1) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return orderedFolderIds.map((id) => {
|
||||
return state.orderedFolderIds.map((id) => {
|
||||
const folder = foldersById[id];
|
||||
|
||||
if (id === ALL_FOLDER_ID) {
|
||||
@ -110,7 +125,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
||||
};
|
||||
});
|
||||
}, [orderedFolderIds, foldersById, lang, chatsCountByFolderId]);
|
||||
}, [state.orderedFolderIds, foldersById, lang, chatsCountByFolderId]);
|
||||
|
||||
const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => {
|
||||
if (Object.keys(foldersById).length >= maxFolders - 1) {
|
||||
@ -124,6 +139,35 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
addChatFolder({ folder });
|
||||
}, [foldersById, maxFolders, addChatFolder, openLimitReachedModal]);
|
||||
|
||||
const handleDrag = useCallback((translation: { x: number; y: number }, id: number) => {
|
||||
const delta = Math.round(translation.y / FOLDER_HEIGHT_PX);
|
||||
const index = state.orderedFolderIds?.indexOf(id) || 0;
|
||||
const dragOrderIds = state.orderedFolderIds?.filter((folderId) => folderId !== id);
|
||||
|
||||
if (!dragOrderIds || !inRange(index + delta, 0, folderIds?.length || 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragOrderIds.splice(index + delta + (isPremium ? 0 : 1), 0, id);
|
||||
setState((current) => ({
|
||||
...current,
|
||||
draggedIndex: index,
|
||||
dragOrderIds,
|
||||
}));
|
||||
}, [folderIds?.length, isPremium, state.orderedFolderIds]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setState((current) => {
|
||||
sortChatFolders({ folderIds: current.dragOrderIds! });
|
||||
|
||||
return {
|
||||
...current,
|
||||
orderedFolderIds: current.dragOrderIds,
|
||||
draggedIndex: undefined,
|
||||
};
|
||||
});
|
||||
}, [sortChatFolders]);
|
||||
|
||||
const canCreateNewFolder = useMemo(() => {
|
||||
return !isPremium || Object.keys(foldersById).length < maxFolders - 1;
|
||||
}, [foldersById, isPremium, maxFolders]);
|
||||
@ -161,24 +205,53 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Filters')}</h4>
|
||||
|
||||
<div className="settings-sortable-container" style={`height: ${(folderIds?.length || 0) * FOLDER_HEIGHT_PX}px`}>
|
||||
{userFolders?.length ? userFolders.map((folder, i) => {
|
||||
const isBlocked = i > maxFolders - 1;
|
||||
const isDragged = state.draggedIndex === i;
|
||||
const draggedTop = (state.orderedFolderIds?.indexOf(folder.id) ?? 0) * FOLDER_HEIGHT_PX;
|
||||
const top = (state.dragOrderIds?.indexOf(folder.id) ?? 0) * FOLDER_HEIGHT_PX;
|
||||
|
||||
if (folder.id === ALL_FOLDER_ID) {
|
||||
return (
|
||||
<Draggable
|
||||
key={folder.id}
|
||||
id={folder.id}
|
||||
onDrag={handleDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={`top: ${isDragged ? draggedTop : top}px;`}
|
||||
knobStyle={`${lang.isRtl ? 'left' : 'right'}: 0.375rem;`}
|
||||
isDisabled={!isPremium || !isActive}
|
||||
>
|
||||
<ListItem
|
||||
className="mb-2 no-icon"
|
||||
key={folder.id}
|
||||
className="mb-2 no-icon settings-sortable-item"
|
||||
narrow
|
||||
inactive
|
||||
multiline
|
||||
isStatic
|
||||
>
|
||||
<span className="title">
|
||||
{folder.title}
|
||||
</span>
|
||||
<span className="subtitle">{lang('FoldersAllChatsDesc')}</span>
|
||||
</ListItem>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={folder.id}
|
||||
id={folder.id}
|
||||
onDrag={handleDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={`top: ${isDragged ? draggedTop : top}px;`}
|
||||
knobStyle={`${lang.isRtl ? 'left' : 'right'}: 3rem;`}
|
||||
isDisabled={isBlocked || !isActive}
|
||||
>
|
||||
<ListItem
|
||||
className="mb-2 no-icon"
|
||||
className="mb-2 no-icon settings-sortable-item"
|
||||
narrow
|
||||
secondaryIcon="more"
|
||||
multiline
|
||||
@ -209,6 +282,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
</span>
|
||||
<span className="subtitle">{folder.subtitle}</span>
|
||||
</ListItem>
|
||||
</Draggable>
|
||||
);
|
||||
}) : userFolders && !userFolders.length ? (
|
||||
<p className="settings-item-description my-4" dir="auto">
|
||||
@ -216,6 +290,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
</p>
|
||||
) : <Loading />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(recommendedChatFolders && Boolean(recommendedChatFolders.length)) && (
|
||||
<div className="settings-item pt-3">
|
||||
@ -258,13 +333,13 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
orderedIds: orderedFolderIds,
|
||||
orderedIds: folderIds,
|
||||
byId: foldersById,
|
||||
recommended: recommendedChatFolders,
|
||||
} = global.chatFolders;
|
||||
|
||||
return {
|
||||
orderedFolderIds,
|
||||
folderIds,
|
||||
foldersById,
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
recommendedChatFolders,
|
||||
@ -272,3 +347,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
},
|
||||
)(SettingsFoldersMain));
|
||||
|
||||
function inRange(x: number, min: number, max: number) {
|
||||
return x >= min && x <= max;
|
||||
}
|
||||
|
43
src/components/ui/Draggable.module.scss
Normal file
43
src/components/ui/Draggable.module.scss
Normal file
@ -0,0 +1,43 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.isDragging {
|
||||
z-index: 2;
|
||||
|
||||
> *:not(.knob) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
|
||||
cursor: grab !important;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.container:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.isDragging & {
|
||||
opacity: 1;
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
opacity: 1 !important;
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
189
src/components/ui/Draggable.tsx
Normal file
189
src/components/ui/Draggable.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import styles from './Draggable.module.scss';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
type TPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type DraggableState = {
|
||||
isDragging: boolean;
|
||||
origin: TPoint;
|
||||
translation: TPoint;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode;
|
||||
onDrag: (translation: TPoint, id: number) => void;
|
||||
onDragEnd: NoneToVoidFunction;
|
||||
id: number;
|
||||
style?: string;
|
||||
knobStyle?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const ZERO_POINT: TPoint = { x: 0, y: 0 };
|
||||
|
||||
const Draggable: FC<OwnProps> = ({
|
||||
children,
|
||||
id,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
style: externalStyle,
|
||||
knobStyle,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [state, setState] = useState<DraggableState>({
|
||||
isDragging: false,
|
||||
origin: ZERO_POINT,
|
||||
translation: ZERO_POINT,
|
||||
});
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const { x, y } = getClientCoordinate(e);
|
||||
|
||||
setState({
|
||||
...state,
|
||||
isDragging: true,
|
||||
origin: { x, y },
|
||||
width: ref.current?.offsetWidth,
|
||||
height: ref.current?.offsetHeight,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
const { x, y } = getClientCoordinate(e);
|
||||
|
||||
const translation = {
|
||||
x: x - state.origin.x,
|
||||
y: y - state.origin.y,
|
||||
};
|
||||
|
||||
setState((current) => ({
|
||||
...current,
|
||||
translation,
|
||||
}));
|
||||
|
||||
onDrag(translation, id);
|
||||
}, [id, onDrag, state.origin.x, state.origin.y]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
isDragging: false,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
}));
|
||||
|
||||
onDragEnd();
|
||||
});
|
||||
}, [onDragEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isDragging && isDisabled) {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
isDragging: false,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
}));
|
||||
}
|
||||
}, [isDisabled, state.isDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isDragging) {
|
||||
window.addEventListener('touchmove', handleMouseMove);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('touchend', handleMouseUp);
|
||||
window.addEventListener('touchcancel', handleMouseUp);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
} else {
|
||||
window.removeEventListener('touchmove', handleMouseMove);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('touchend', handleMouseUp);
|
||||
window.removeEventListener('touchcancel', handleMouseUp);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
setState((current) => ({
|
||||
...current,
|
||||
translation: ZERO_POINT,
|
||||
}));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (state.isDragging) {
|
||||
window.removeEventListener('touchmove', handleMouseMove);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('touchend', handleMouseUp);
|
||||
window.removeEventListener('touchcancel', handleMouseUp);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp, state.isDragging]);
|
||||
|
||||
const fullClassName = buildClassName(styles.container, state.isDragging && styles.isDragging);
|
||||
|
||||
const cssStyles = useMemo(() => {
|
||||
return buildStyle(
|
||||
`transform: translate(${state.translation.x}px, ${state.translation.y}px)`,
|
||||
state.width ? `width: ${state.width}px` : undefined,
|
||||
state.height ? `height: ${state.height}px` : undefined,
|
||||
externalStyle,
|
||||
);
|
||||
}, [externalStyle, state.height, state.translation.x, state.translation.y, state.width]);
|
||||
|
||||
return (
|
||||
<div style={cssStyles} className={fullClassName} ref={ref}>
|
||||
{children}
|
||||
|
||||
{!isDisabled && (
|
||||
<div
|
||||
aria-label={lang('i18n_dragToSort')}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className={buildClassName(styles.knob, 'draggable-knob')}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleMouseDown}
|
||||
style={knobStyle}
|
||||
>
|
||||
<i className="icon-sort" aria-hidden />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Draggable);
|
||||
|
||||
function getClientCoordinate(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) {
|
||||
let x;
|
||||
let y;
|
||||
|
||||
if ('touches' in e) {
|
||||
x = e.touches[0].clientX;
|
||||
y = e.touches[0].clientY;
|
||||
} else {
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
}
|
||||
|
||||
return { x, y };
|
||||
}
|
@ -38,7 +38,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v10';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v11';
|
||||
export const ASSET_CACHE_NAME = 'tt-assets';
|
||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||
|
||||
|
@ -491,6 +491,22 @@ addActionHandler('addChatFolder', (global, actions, payload) => {
|
||||
void createChatFolder(folder, maxId);
|
||||
});
|
||||
|
||||
addActionHandler('sortChatFolders', async (global, actions, payload) => {
|
||||
const { folderIds } = payload!;
|
||||
|
||||
const result = await callApi('sortChatFolders', folderIds);
|
||||
if (result) {
|
||||
global = getGlobal();
|
||||
setGlobal({
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
orderedIds: folderIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('deleteChatFolder', (global, actions, payload) => {
|
||||
const { id } = payload!;
|
||||
const folder = selectChatFolder(global, id);
|
||||
|
@ -1052,6 +1052,7 @@ export interface ActionPayloads {
|
||||
|
||||
// Settings
|
||||
requestNextSettingsScreen: SettingsScreens;
|
||||
sortChatFolders: { folderIds: number[] };
|
||||
closeDeleteChatFolderModal: never;
|
||||
openDeleteChatFolderModal: { folderId: number };
|
||||
loadGlobalPrivacySettings: never;
|
||||
|
@ -1143,6 +1143,7 @@ messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?byte
|
||||
messages.getDialogFilters#f19ed96d = Vector<DialogFilter>;
|
||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
||||
messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;
|
||||
messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;
|
||||
messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
|
||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||
|
@ -146,6 +146,7 @@
|
||||
"messages.getDialogFilters",
|
||||
"messages.getSuggestedDialogFilters",
|
||||
"messages.updateDialogFilter",
|
||||
"messages.updateDialogFiltersOrder",
|
||||
"messages.getReplies",
|
||||
"messages.getDiscussionMessage",
|
||||
"messages.readDiscussion",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,9 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-sort:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
.icon-web:before {
|
||||
content: "\e9ab";
|
||||
}
|
||||
|
@ -1853,4 +1853,8 @@ export default {
|
||||
key: 'Settings.TipsUsername',
|
||||
value: 'TelegramTips',
|
||||
},
|
||||
FoldersAllChatsDesc: {
|
||||
key: 'FoldersAllChatsDesc',
|
||||
value: 'All unarchived chats',
|
||||
},
|
||||
} as ApiLangPack;
|
||||
|
Loading…
Reference in New Issue
Block a user