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({
|
export async function toggleDialogUnread({
|
||||||
chat, hasUnreadMark,
|
chat, hasUnreadMark,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export {
|
|||||||
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
||||||
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
||||||
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
||||||
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders,
|
||||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -76,3 +76,15 @@
|
|||||||
width: calc(100% + 2rem);
|
width: calc(100% + 2rem);
|
||||||
padding-left: 1rem !important;
|
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 type { FC } from '../../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useMemo, useCallback, useEffect,
|
memo, useMemo, useCallback, useEffect, useState,
|
||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../../global';
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ import ListItem from '../../../ui/ListItem';
|
|||||||
import Button from '../../../ui/Button';
|
import Button from '../../../ui/Button';
|
||||||
import Loading from '../../../ui/Loading';
|
import Loading from '../../../ui/Loading';
|
||||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
|
import Draggable from '../../../ui/Draggable';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -30,13 +31,20 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
orderedFolderIds?: number[];
|
folderIds?: number[];
|
||||||
foldersById: Record<number, ApiChatFolder>;
|
foldersById: Record<number, ApiChatFolder>;
|
||||||
recommendedChatFolders?: ApiChatFolder[];
|
recommendedChatFolders?: ApiChatFolder[];
|
||||||
maxFolders: number;
|
maxFolders: number;
|
||||||
isPremium?: boolean;
|
isPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SortState = {
|
||||||
|
orderedFolderIds?: number[];
|
||||||
|
dragOrderIds?: number[];
|
||||||
|
draggedIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FOLDER_HEIGHT_PX = 68;
|
||||||
const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
|
const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
|
||||||
|
|
||||||
const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||||
@ -44,7 +52,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
onCreateFolder,
|
onCreateFolder,
|
||||||
onEditFolder,
|
onEditFolder,
|
||||||
onReset,
|
onReset,
|
||||||
orderedFolderIds,
|
folderIds,
|
||||||
foldersById,
|
foldersById,
|
||||||
isPremium,
|
isPremium,
|
||||||
recommendedChatFolders,
|
recommendedChatFolders,
|
||||||
@ -55,8 +63,15 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
addChatFolder,
|
addChatFolder,
|
||||||
openLimitReachedModal,
|
openLimitReachedModal,
|
||||||
openDeleteChatFolderModal,
|
openDeleteChatFolderModal,
|
||||||
|
sortChatFolders,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const [state, setState] = useState<SortState>({
|
||||||
|
orderedFolderIds: folderIds,
|
||||||
|
dragOrderIds: folderIds,
|
||||||
|
draggedIndex: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
// Due to the parent Transition, this component never gets unmounted,
|
// Due to the parent Transition, this component never gets unmounted,
|
||||||
// that's why we use throttled API call on every update.
|
// that's why we use throttled API call on every update.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -86,15 +101,15 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const chatsCountByFolderId = useFolderManagerForChatsCount();
|
const chatsCountByFolderId = useFolderManagerForChatsCount();
|
||||||
const userFolders = useMemo(() => {
|
const userFolders = useMemo(() => {
|
||||||
if (!orderedFolderIds) {
|
if (!state.orderedFolderIds) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderedFolderIds.length <= 1) {
|
if (state.orderedFolderIds.length <= 1) {
|
||||||
return MEMO_EMPTY_ARRAY;
|
return MEMO_EMPTY_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderedFolderIds.map((id) => {
|
return state.orderedFolderIds.map((id) => {
|
||||||
const folder = foldersById[id];
|
const folder = foldersById[id];
|
||||||
|
|
||||||
if (id === ALL_FOLDER_ID) {
|
if (id === ALL_FOLDER_ID) {
|
||||||
@ -110,7 +125,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [orderedFolderIds, foldersById, lang, chatsCountByFolderId]);
|
}, [state.orderedFolderIds, foldersById, lang, chatsCountByFolderId]);
|
||||||
|
|
||||||
const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => {
|
const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => {
|
||||||
if (Object.keys(foldersById).length >= maxFolders - 1) {
|
if (Object.keys(foldersById).length >= maxFolders - 1) {
|
||||||
@ -124,6 +139,35 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
addChatFolder({ folder });
|
addChatFolder({ folder });
|
||||||
}, [foldersById, maxFolders, addChatFolder, openLimitReachedModal]);
|
}, [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(() => {
|
const canCreateNewFolder = useMemo(() => {
|
||||||
return !isPremium || Object.keys(foldersById).length < maxFolders - 1;
|
return !isPremium || Object.keys(foldersById).length < maxFolders - 1;
|
||||||
}, [foldersById, isPremium, maxFolders]);
|
}, [foldersById, isPremium, maxFolders]);
|
||||||
@ -161,60 +205,91 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="settings-item pt-3">
|
<div className="settings-item pt-3">
|
||||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Filters')}</h4>
|
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Filters')}</h4>
|
||||||
|
|
||||||
{userFolders?.length ? userFolders.map((folder, i) => {
|
<div className="settings-sortable-container" style={`height: ${(folderIds?.length || 0) * FOLDER_HEIGHT_PX}px`}>
|
||||||
const isBlocked = i > maxFolders - 1;
|
{userFolders?.length ? userFolders.map((folder, i) => {
|
||||||
if (folder.id === ALL_FOLDER_ID) {
|
const isBlocked = i > maxFolders - 1;
|
||||||
return (
|
const isDragged = state.draggedIndex === i;
|
||||||
<ListItem
|
const draggedTop = (state.orderedFolderIds?.indexOf(folder.id) ?? 0) * FOLDER_HEIGHT_PX;
|
||||||
className="mb-2 no-icon"
|
const top = (state.dragOrderIds?.indexOf(folder.id) ?? 0) * FOLDER_HEIGHT_PX;
|
||||||
narrow
|
|
||||||
inactive
|
|
||||||
isStatic
|
|
||||||
>
|
|
||||||
{folder.title}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (folder.id === ALL_FOLDER_ID) {
|
||||||
<ListItem
|
return (
|
||||||
className="mb-2 no-icon"
|
<Draggable
|
||||||
narrow
|
key={folder.id}
|
||||||
secondaryIcon="more"
|
id={folder.id}
|
||||||
multiline
|
onDrag={handleDrag}
|
||||||
contextActions={[
|
onDragEnd={handleDragEnd}
|
||||||
{
|
style={`top: ${isDragged ? draggedTop : top}px;`}
|
||||||
handler: () => {
|
knobStyle={`${lang.isRtl ? 'left' : 'right'}: 0.375rem;`}
|
||||||
openDeleteChatFolderModal({ folderId: folder.id });
|
isDisabled={!isPremium || !isActive}
|
||||||
},
|
>
|
||||||
destructive: true,
|
<ListItem
|
||||||
title: lang('Delete'),
|
key={folder.id}
|
||||||
icon: 'delete',
|
className="mb-2 no-icon settings-sortable-item"
|
||||||
},
|
narrow
|
||||||
]}
|
inactive
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
multiline
|
||||||
onClick={() => {
|
isStatic
|
||||||
if (isBlocked) {
|
>
|
||||||
openLimitReachedModal({
|
<span className="title">
|
||||||
limit: 'dialogFilters',
|
{folder.title}
|
||||||
});
|
</span>
|
||||||
} else {
|
<span className="subtitle">{lang('FoldersAllChatsDesc')}</span>
|
||||||
onEditFolder(foldersById[folder.id]);
|
</ListItem>
|
||||||
}
|
</Draggable>
|
||||||
}}
|
);
|
||||||
>
|
}
|
||||||
<span className="title">
|
|
||||||
{folder.title}
|
return (
|
||||||
{isBlocked && <i className="icon-lock-badge settings-folders-blocked-icon" />}
|
<Draggable
|
||||||
</span>
|
key={folder.id}
|
||||||
<span className="subtitle">{folder.subtitle}</span>
|
id={folder.id}
|
||||||
</ListItem>
|
onDrag={handleDrag}
|
||||||
);
|
onDragEnd={handleDragEnd}
|
||||||
}) : userFolders && !userFolders.length ? (
|
style={`top: ${isDragged ? draggedTop : top}px;`}
|
||||||
<p className="settings-item-description my-4" dir="auto">
|
knobStyle={`${lang.isRtl ? 'left' : 'right'}: 3rem;`}
|
||||||
You have no folders yet.
|
isDisabled={isBlocked || !isActive}
|
||||||
</p>
|
>
|
||||||
) : <Loading />}
|
<ListItem
|
||||||
|
className="mb-2 no-icon settings-sortable-item"
|
||||||
|
narrow
|
||||||
|
secondaryIcon="more"
|
||||||
|
multiline
|
||||||
|
contextActions={[
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openDeleteChatFolderModal({ folderId: folder.id });
|
||||||
|
},
|
||||||
|
destructive: true,
|
||||||
|
title: lang('Delete'),
|
||||||
|
icon: 'delete',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => {
|
||||||
|
if (isBlocked) {
|
||||||
|
openLimitReachedModal({
|
||||||
|
limit: 'dialogFilters',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onEditFolder(foldersById[folder.id]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="title">
|
||||||
|
{folder.title}
|
||||||
|
{isBlocked && <i className="icon-lock-badge settings-folders-blocked-icon" />}
|
||||||
|
</span>
|
||||||
|
<span className="subtitle">{folder.subtitle}</span>
|
||||||
|
</ListItem>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}) : userFolders && !userFolders.length ? (
|
||||||
|
<p className="settings-item-description my-4" dir="auto">
|
||||||
|
You have no folders yet.
|
||||||
|
</p>
|
||||||
|
) : <Loading />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(recommendedChatFolders && Boolean(recommendedChatFolders.length)) && (
|
{(recommendedChatFolders && Boolean(recommendedChatFolders.length)) && (
|
||||||
@ -258,13 +333,13 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const {
|
const {
|
||||||
orderedIds: orderedFolderIds,
|
orderedIds: folderIds,
|
||||||
byId: foldersById,
|
byId: foldersById,
|
||||||
recommended: recommendedChatFolders,
|
recommended: recommendedChatFolders,
|
||||||
} = global.chatFolders;
|
} = global.chatFolders;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderedFolderIds,
|
folderIds,
|
||||||
foldersById,
|
foldersById,
|
||||||
isPremium: selectIsCurrentUserPremium(global),
|
isPremium: selectIsCurrentUserPremium(global),
|
||||||
recommendedChatFolders,
|
recommendedChatFolders,
|
||||||
@ -272,3 +347,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsFoldersMain));
|
)(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_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
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 ASSET_CACHE_NAME = 'tt-assets';
|
||||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
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);
|
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) => {
|
addActionHandler('deleteChatFolder', (global, actions, payload) => {
|
||||||
const { id } = payload!;
|
const { id } = payload!;
|
||||||
const folder = selectChatFolder(global, id);
|
const folder = selectChatFolder(global, id);
|
||||||
|
|||||||
@ -1052,6 +1052,7 @@ export interface ActionPayloads {
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
requestNextSettingsScreen: SettingsScreens;
|
requestNextSettingsScreen: SettingsScreens;
|
||||||
|
sortChatFolders: { folderIds: number[] };
|
||||||
closeDeleteChatFolderModal: never;
|
closeDeleteChatFolderModal: never;
|
||||||
openDeleteChatFolderModal: { folderId: number };
|
openDeleteChatFolderModal: { folderId: number };
|
||||||
loadGlobalPrivacySettings: never;
|
loadGlobalPrivacySettings: never;
|
||||||
|
|||||||
@ -1143,6 +1143,7 @@ messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?byte
|
|||||||
messages.getDialogFilters#f19ed96d = Vector<DialogFilter>;
|
messages.getDialogFilters#f19ed96d = Vector<DialogFilter>;
|
||||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
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.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.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
|
||||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||||
|
|||||||
@ -146,6 +146,7 @@
|
|||||||
"messages.getDialogFilters",
|
"messages.getDialogFilters",
|
||||||
"messages.getSuggestedDialogFilters",
|
"messages.getSuggestedDialogFilters",
|
||||||
"messages.updateDialogFilter",
|
"messages.updateDialogFilter",
|
||||||
|
"messages.updateDialogFiltersOrder",
|
||||||
"messages.getReplies",
|
"messages.getReplies",
|
||||||
"messages.getDiscussionMessage",
|
"messages.getDiscussionMessage",
|
||||||
"messages.readDiscussion",
|
"messages.readDiscussion",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,9 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
|
.icon-sort:before {
|
||||||
|
content: "\e9ac";
|
||||||
|
}
|
||||||
.icon-web:before {
|
.icon-web:before {
|
||||||
content: "\e9ab";
|
content: "\e9ab";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1853,4 +1853,8 @@ export default {
|
|||||||
key: 'Settings.TipsUsername',
|
key: 'Settings.TipsUsername',
|
||||||
value: 'TelegramTips',
|
value: 'TelegramTips',
|
||||||
},
|
},
|
||||||
|
FoldersAllChatsDesc: {
|
||||||
|
key: 'FoldersAllChatsDesc',
|
||||||
|
value: 'All unarchived chats',
|
||||||
|
},
|
||||||
} as ApiLangPack;
|
} as ApiLangPack;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user