Attachment Modal: Redesign (#6777)
This commit is contained in:
parent
a44ecb0113
commit
6770fff857
@ -366,6 +366,7 @@ lang('SimpleKey');
|
||||
|
||||
// Plurals
|
||||
lang('PluralKey', undefined, { pluralValue: 3 });
|
||||
lang('PluralKey', { count: 3 }, { pluralValue: 3 }); // if key has variables
|
||||
|
||||
// String replacements
|
||||
lang('ReplKey', { name: 'Amy' });
|
||||
|
||||
1
src/assets/font-icons/check-bold.svg
Normal file
1
src/assets/font-icons/check-bold.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M25.468 9.259a1.82 1.82 0 0 0-2.572 0l-9.623 9.625-3.26-3.26a1.82 1.82 0 0 0-2.573 2.571l4.548 4.545c.71.71 1.861.71 2.571 0l10.909-10.909a1.82 1.82 0 0 0 0-2.572"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
1
src/assets/font-icons/new-send.svg
Normal file
1
src/assets/font-icons/new-send.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M4.219 14.363s10.066-4.358 13.425-5.746c6.393-2.643 7.72-3.102 8.586-3.117.19-.003.616.044.892.266.233.188.297.442.327.62s.07.583.039.9c-.347 3.617-1.846 12.395-2.608 16.447-.323 1.714-.958 2.289-1.573 2.345-1.337.122-2.352-.878-3.647-1.721-2.026-1.32-3.17-2.142-5.138-3.43-2.272-1.488-.799-2.306.496-3.643.34-.35 6.23-5.674 6.343-6.157.015-.06.028-.285-.107-.404-.134-.119-.333-.078-.476-.046-.305.069-9.71 6.378-9.71 6.378s-1.379.94-2.497.916c-.822-.017-2.403-.462-3.578-.841-1.442-.466-2.588-.712-2.488-1.503.078-.618 1.713-1.264 1.713-1.264"/></svg>
|
||||
|
After Width: | Height: | Size: 625 B |
@ -302,6 +302,7 @@
|
||||
"ConversationPinMessagesFor" = "Pin for me and {user}";
|
||||
"ConversationPinMessageAlertPinAndNotifyMembers" = "Pin and notify all members";
|
||||
"SavedMessages" = "Saved Messages";
|
||||
"SavedMessagesShort" = "Saved";
|
||||
"AccDescrPrevious" = "Previous";
|
||||
"ReportReasonSpam" = "Spam";
|
||||
"ReportReasonViolence" = "Violence";
|
||||
@ -584,6 +585,8 @@
|
||||
"ContactShare" = "Share Contact";
|
||||
"OK" = "OK";
|
||||
"ForwardTo" = "Forward to...";
|
||||
"ShareWith" = "Share with";
|
||||
"SelectChats" = "Select chats";
|
||||
"AttachGame" = "Game";
|
||||
"JumpToDate" = "Jump to Date";
|
||||
"FloodWait" = "Too many attempts, please try again later.";
|
||||
@ -617,6 +620,7 @@
|
||||
"WebAppAddToAttachmentAdd" = "Add";
|
||||
"AccActionDownload" = "Download";
|
||||
"Forward" = "Forward";
|
||||
"ForwardForStars" = "Forward for {price}";
|
||||
"MediaZoomOut" = "Zoom Out";
|
||||
"MediaZoomIn" = "Zoom In";
|
||||
"PeerInfoReportProfileVideo" = "Report Profile Photo";
|
||||
@ -2143,6 +2147,11 @@
|
||||
"ConfirmationModalPaymentForMessage_other" = "{user} charges **{amount}** per incoming message. Would you like to pay **{totalAmount}** to send **{count} messages?**";
|
||||
"PayForMessage_one" = "Pay for {count} message";
|
||||
"PayForMessage_other" = "Pay for {count} messages";
|
||||
"ForwardPaidChatsConfirmation" = "{chatsSelected} {payConfirmation}";
|
||||
"ForwardPaidChatsSelected_one" = "You selected **{paidChatsCount}** chat that charges Stars for messages.";
|
||||
"ForwardPaidChatsSelected_other" = "You selected **{paidChatsCount}** chats that charge Stars for messages.";
|
||||
"ForwardPaidChatsPayConfirmation_one" = "Would you like to pay **{totalAmount}** to send **{count}** message?";
|
||||
"ForwardPaidChatsPayConfirmation_other" = "Would you like to pay **{totalAmount}** to send **{count}** messages?";
|
||||
"MessageSentPaidToastTitle_one" = "Message sent!";
|
||||
"MessageSentPaidToastTitle_other" = "{count} messages sent!";
|
||||
"MessageSentPaidToastText" = "You paid {amount}";
|
||||
@ -2690,8 +2699,6 @@
|
||||
"AttachmentSendAudio_other" = "Send {count} Audios";
|
||||
"AttachmentSendFile_one" = "Send File";
|
||||
"AttachmentSendFile_other" = "Send {count} Files";
|
||||
"AttachmentSendGif" = "Send GIF";
|
||||
"AttachmentReplaceGif" = "Replace GIF";
|
||||
"AttachmentDragAddItems" = "Add Items";
|
||||
"AttachmentCaptionPlaceholder" = "Add a caption...";
|
||||
"MessageSummaryTitle" = "AI Summary";
|
||||
@ -2772,6 +2779,8 @@
|
||||
"RankEditTextOwn" = "Share your role, title or how you're known in this group. Your tag is visible to all members.";
|
||||
"RankEditText" = "Add a short tag next to {user}'s name.";
|
||||
"MenuAddCaption" = "Add Caption";
|
||||
"FwdMessagesToChats_one" = "Message forwarded to {count} chat";
|
||||
"FwdMessagesToChats_other" = "Message forwarded to {count} chats";
|
||||
"MenuCopyDate" = "Copy Date";
|
||||
"DateCopiedToast" = "Date copied to clipboard";
|
||||
"ReminderSetToast" = "You set up a reminder in **Saved Messages**";
|
||||
|
||||
@ -896,7 +896,7 @@
|
||||
}
|
||||
|
||||
#caption-input-text .placeholder-text {
|
||||
bottom: 0.8125rem;
|
||||
bottom: 0.875rem;
|
||||
}
|
||||
|
||||
#story-input-text .placeholder-text {
|
||||
|
||||
@ -31,7 +31,7 @@ import File from './File';
|
||||
type OwnProps = {
|
||||
document: ApiDocument;
|
||||
observeIntersection?: ObserveFn;
|
||||
smaller?: boolean;
|
||||
fileSize?: 'small' | 'medium' | 'large';
|
||||
isSelected?: boolean;
|
||||
isSelectable?: boolean;
|
||||
canAutoLoad?: boolean;
|
||||
@ -59,7 +59,7 @@ const BYTES_PER_MB = 1024 * 1024;
|
||||
const Document = ({
|
||||
document,
|
||||
observeIntersection,
|
||||
smaller,
|
||||
fileSize,
|
||||
canAutoLoad,
|
||||
autoLoadFileMaxSizeMb,
|
||||
uploadProgress,
|
||||
@ -201,7 +201,7 @@ const Document = ({
|
||||
timestamp={datetime}
|
||||
thumbnailDataUri={thumbDataUri}
|
||||
previewData={localBlobUrl || previewData}
|
||||
smaller={smaller}
|
||||
previewSize={fileSize}
|
||||
isTransferring={isTransferring}
|
||||
isUploading={isUploading}
|
||||
transferProgress={transferProgress}
|
||||
|
||||
@ -185,7 +185,7 @@
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
&.smaller {
|
||||
&.size-small {
|
||||
--background-color: var(--color-background);
|
||||
--border-radius-messages-small: 0.3125rem;
|
||||
|
||||
@ -218,6 +218,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.size-large {
|
||||
.action-icon,
|
||||
.file-progress,
|
||||
.file-icon,
|
||||
.file-preview {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.file-subtitle {
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.file-icon-container {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:dir(rtl),
|
||||
&[dir="rtl"] {
|
||||
.file-progress,
|
||||
|
||||
@ -26,6 +26,8 @@ import Icon from './icons/Icon';
|
||||
|
||||
import './File.scss';
|
||||
|
||||
type FileSize = 'small' | 'medium' | 'large';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
id?: string;
|
||||
@ -37,7 +39,7 @@ type OwnProps = {
|
||||
thumbnailDataUri?: string;
|
||||
previewData?: string;
|
||||
className?: string;
|
||||
smaller?: boolean;
|
||||
previewSize?: FileSize;
|
||||
isTransferring?: boolean;
|
||||
isUploading?: boolean;
|
||||
isSelectable?: boolean;
|
||||
@ -59,7 +61,7 @@ const File = ({
|
||||
thumbnailDataUri,
|
||||
previewData,
|
||||
className,
|
||||
smaller,
|
||||
previewSize = 'medium',
|
||||
isTransferring,
|
||||
isUploading,
|
||||
isSelectable,
|
||||
@ -89,12 +91,12 @@ const File = ({
|
||||
|
||||
const color = getColorFromExtension(extension);
|
||||
|
||||
const { width, height } = getDocumentThumbnailDimensions(smaller);
|
||||
const { width, height } = getDocumentThumbnailDimensions(previewSize);
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'File',
|
||||
className,
|
||||
smaller && 'smaller',
|
||||
previewSize !== 'medium' && `size-${previewSize}`,
|
||||
onClick && !isUploading && 'interactive',
|
||||
isSelected && 'file-is-selected',
|
||||
);
|
||||
@ -135,7 +137,7 @@ const File = ({
|
||||
<div className={buildClassName('file-progress', color, spinnerClassNames)}>
|
||||
<ProgressSpinner
|
||||
progress={transferProgress}
|
||||
size={smaller ? 's' : 'm'}
|
||||
size={previewSize === 'small' ? 's' : 'm'}
|
||||
onClick={isUploading ? onClick : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
align-items: center;
|
||||
|
||||
min-width: 0;
|
||||
height: 2rem;
|
||||
height: var(--chip-size, 2rem);
|
||||
margin-inline: 0.25rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
@ -47,12 +47,17 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.small {
|
||||
margin-inline: 0;
|
||||
border-radius: 1.4375rem;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.iconWrapper {
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: var(--chip-size, 2rem);
|
||||
height: var(--chip-size, 2rem);
|
||||
|
||||
opacity: 1;
|
||||
|
||||
@ -102,8 +107,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: var(--chip-size, 2rem);
|
||||
height: var(--chip-size, 2rem);
|
||||
border-radius: 50%;
|
||||
|
||||
font-size: 1.5rem;
|
||||
@ -118,10 +123,9 @@
|
||||
&.squareAvatar {
|
||||
--border-radius-forum-avatar: 0.625rem;
|
||||
|
||||
border-start-start-radius: 0.625rem;
|
||||
border-end-start-radius: 0.625rem;
|
||||
border-radius: 0.625rem;
|
||||
|
||||
&.minimized, .remove {
|
||||
.remove {
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { getPeerTitle, isApiPeerChat } from '../../global/helpers/peers';
|
||||
import { selectPeer, selectTheme, selectUser } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePeerColor from '../../hooks/usePeerColor';
|
||||
@ -20,10 +21,14 @@ import Icon from './icons/Icon';
|
||||
|
||||
import styles from './PeerChip.module.scss';
|
||||
|
||||
const CHIP_SIZE_SMALL = 1.875 * REM;
|
||||
const CHIP_SIZE_MEDIUM = 2 * REM;
|
||||
|
||||
export type PeerChipSize = 'small' | 'medium';
|
||||
|
||||
type OwnProps<T = undefined> = {
|
||||
|
||||
peerId?: string;
|
||||
|
||||
size?: PeerChipSize;
|
||||
forceShowSelf?: boolean;
|
||||
customPeer?: CustomPeer;
|
||||
mockPeer?: ApiPeer;
|
||||
@ -33,11 +38,11 @@ type OwnProps<T = undefined> = {
|
||||
canClose?: boolean;
|
||||
isCloseNonDestructive?: boolean;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
withPeerColors?: boolean;
|
||||
withEmojiStatus?: boolean;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T) => void;
|
||||
itemClassName?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -49,6 +54,7 @@ type StateProps = {
|
||||
const PeerChip = <T,>({
|
||||
icon,
|
||||
title,
|
||||
size = 'medium',
|
||||
isMinimized,
|
||||
canClose,
|
||||
isCloseNonDestructive,
|
||||
@ -57,10 +63,10 @@ const PeerChip = <T,>({
|
||||
mockPeer,
|
||||
customPeer,
|
||||
className,
|
||||
itemClassName,
|
||||
isSavedMessages,
|
||||
withPeerColors,
|
||||
withEmojiStatus,
|
||||
itemClassName,
|
||||
theme,
|
||||
onClick,
|
||||
}: OwnProps<T> & StateProps) => {
|
||||
@ -106,6 +112,7 @@ const PeerChip = <T,>({
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
styles.root,
|
||||
size === 'small' && styles.small,
|
||||
(chat?.isForum || customPeer?.isAvatarSquare) && styles.squareAvatar,
|
||||
isMinimized && styles.minimized,
|
||||
canClose && styles.closeable,
|
||||
@ -115,7 +122,10 @@ const PeerChip = <T,>({
|
||||
className,
|
||||
);
|
||||
|
||||
const chipSize = size === 'small' ? CHIP_SIZE_SMALL : CHIP_SIZE_MEDIUM;
|
||||
|
||||
const style = buildStyle(
|
||||
`--chip-size: ${chipSize}px`,
|
||||
withPeerColors && peerColorStyle,
|
||||
);
|
||||
|
||||
|
||||
7
src/components/common/RecipientPicker.module.scss
Normal file
7
src/components/common/RecipientPicker.module.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.recentContacts {
|
||||
margin-block: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { memo, useMemo, useState } from '../../lib/teact/teact';
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo, useState } from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChatFolder, ApiChatType } from '../../api/types';
|
||||
@ -7,36 +8,51 @@ import type { ThreadId } from '../../types';
|
||||
import { ALL_FOLDER_ID, API_CHAT_TYPES } from '../../config';
|
||||
import {
|
||||
getCanPostInChat,
|
||||
getChatTitle,
|
||||
getHasAdminRight,
|
||||
isChatChannel,
|
||||
isDeletedUser,
|
||||
isSystemBot,
|
||||
} from '../../global/helpers';
|
||||
import { filterPeersByQuery } from '../../global/helpers/peers';
|
||||
import {
|
||||
filterChatIdsByType, selectChat, selectChatFullInfo, selectIsMonoforumAdmin, selectUser,
|
||||
filterChatIdsByType, selectCanAnimateInterface, selectChat, selectChatFullInfo, selectIsMonoforumAdmin,
|
||||
selectTopic, selectUser,
|
||||
} from '../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../global/selectors/limits';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { unique } from '../../util/iteratees';
|
||||
import sortChatIds from './helpers/sortChatIds';
|
||||
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import { useFolderManagerForOrderedIds } from '../../hooks/useFolderManager';
|
||||
import useFolderTabs from '../../hooks/useFolderTabs';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
|
||||
import TabList from '../ui/TabList';
|
||||
import ChatOrUserPicker from './pickers/ChatOrUserPicker';
|
||||
import PeerChip from './PeerChip';
|
||||
import ChatOrUserPicker, { type SearchRowRenderProps } from './pickers/ChatOrUserPicker';
|
||||
import PickerRecentContacts from './pickers/PickerRecentContacts';
|
||||
|
||||
import styles from './RecipientPicker.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
searchPlaceholder: string;
|
||||
className?: string;
|
||||
filter?: readonly ApiChatType[];
|
||||
isLowStackPriority?: boolean;
|
||||
isForwarding?: boolean;
|
||||
isMultiSelect?: boolean;
|
||||
withFolders?: boolean;
|
||||
footer?: TeactNode;
|
||||
viewportFooter?: TeactNode;
|
||||
loadMore?: NoneToVoidFunction;
|
||||
onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
|
||||
onSelectedIdsChange?: (ids: string[]) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
};
|
||||
@ -52,6 +68,8 @@ type StateProps = {
|
||||
maxFolders: number;
|
||||
};
|
||||
|
||||
const RECENT_CONTACTS_LIMIT = 15;
|
||||
|
||||
const RecipientPicker = ({
|
||||
isOpen,
|
||||
currentUserId,
|
||||
@ -60,21 +78,34 @@ const RecipientPicker = ({
|
||||
pinnedIds,
|
||||
contactIds,
|
||||
filter = API_CHAT_TYPES,
|
||||
title,
|
||||
className,
|
||||
searchPlaceholder,
|
||||
isLowStackPriority,
|
||||
chatFoldersById,
|
||||
orderedFolderIds,
|
||||
isForwarding,
|
||||
isMultiSelect,
|
||||
maxFolders,
|
||||
withFolders,
|
||||
footer,
|
||||
viewportFooter,
|
||||
loadMore,
|
||||
onSelectRecipient,
|
||||
onSelectedIdsChange,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { openLimitReachedModal } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [removingIds, setRemovingIds] = useState<string[]>([]);
|
||||
const [appearingIds, setAppearingIds] = useState<string[]>([]);
|
||||
const selectedIdsRef = useStateRef(selectedIds);
|
||||
const removingIdsRef = useStateRef(removingIds);
|
||||
const appearingIdsRef = useStateRef(appearingIds);
|
||||
|
||||
const [activeFolderIndex, setActiveFolderIndex] = useState(0);
|
||||
const { displayedFolders, folderTabs } = useFolderTabs({
|
||||
@ -85,7 +116,7 @@ const RecipientPicker = ({
|
||||
isReadOnly: true,
|
||||
});
|
||||
|
||||
const shouldRenderFolders = withFolders && folderTabs?.length && !search;
|
||||
const shouldRenderFolders = withFolders && folderTabs && folderTabs.length > 1 && !search;
|
||||
const displayedFolderId = displayedFolders?.[activeFolderIndex]?.id || ALL_FOLDER_ID;
|
||||
const orderedChatIds = useFolderManagerForOrderedIds(displayedFolderId);
|
||||
|
||||
@ -103,6 +134,57 @@ const RecipientPicker = ({
|
||||
setActiveFolderIndex(index);
|
||||
});
|
||||
|
||||
const updateSelectedIds = useLastCallback((newIds: string[], newlyAddedId?: string) => {
|
||||
setSelectedIds(newIds);
|
||||
onSelectedIdsChange?.(newIds);
|
||||
|
||||
if (newlyAddedId && selectCanAnimateInterface(getGlobal())) {
|
||||
setAppearingIds([...appearingIdsRef.current, newlyAddedId]);
|
||||
setTimeout(() => {
|
||||
setAppearingIds(appearingIdsRef.current.filter((id) => id !== newlyAddedId));
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRemoveSelected = useLastCallback((selectionId: string) => {
|
||||
if (removingIdsRef.current.includes(selectionId)) return;
|
||||
|
||||
const canAnimate = selectCanAnimateInterface(getGlobal());
|
||||
if (!canAnimate) {
|
||||
const newIds = selectedIdsRef.current.filter((id) => id !== selectionId);
|
||||
setSelectedIds(newIds);
|
||||
onSelectedIdsChange?.(newIds);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemovingIds([...removingIdsRef.current, selectionId]);
|
||||
|
||||
setTimeout(() => {
|
||||
setRemovingIds(removingIdsRef.current.filter((id) => id !== selectionId));
|
||||
const newIds = selectedIdsRef.current.filter((id) => id !== selectionId);
|
||||
setSelectedIds(newIds);
|
||||
onSelectedIdsChange?.(newIds);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const handleToggleSelection = useLastCallback((peerId: string, threadId?: ThreadId) => {
|
||||
const selectionId = threadId ? `${peerId}:${threadId}` : peerId;
|
||||
|
||||
if (selectedIds.includes(selectionId)) {
|
||||
handleRemoveSelected(selectionId);
|
||||
} else {
|
||||
updateSelectedIds([...selectedIds, selectionId], selectionId);
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelect = useLastCallback((peerId: string, threadId?: ThreadId) => {
|
||||
if (isMultiSelect) {
|
||||
handleToggleSelection(peerId, threadId);
|
||||
} else {
|
||||
onSelectRecipient(peerId, threadId);
|
||||
}
|
||||
});
|
||||
|
||||
const ids = useMemo(() => {
|
||||
if (!isOpen) return [];
|
||||
|
||||
@ -120,6 +202,8 @@ const RecipientPicker = ({
|
||||
];
|
||||
|
||||
const peerIds = allIds.filter((id) => {
|
||||
if (isSystemBot(id)) return false;
|
||||
|
||||
const chat = selectChat(global, id);
|
||||
const user = selectUser(global, id);
|
||||
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
|
||||
@ -171,16 +255,145 @@ const RecipientPicker = ({
|
||||
|
||||
const renderingIds = useCurrentOrPrev(ids, true);
|
||||
|
||||
const chatFolders = useMemo(() => {
|
||||
if (!shouldRenderFolders) return undefined;
|
||||
const recentContactIds = useMemo(() => {
|
||||
if (!contactIds) return [];
|
||||
return contactIds.slice(0, RECENT_CONTACTS_LIMIT);
|
||||
}, [contactIds]);
|
||||
|
||||
const hasSelectedChips = isMultiSelect && selectedIds.length > 0;
|
||||
|
||||
const parseSelectionId = useLastCallback((selectionId: string): { peerId: string; topicId?: number } => {
|
||||
const colonIndex = selectionId.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
return { peerId: selectionId };
|
||||
}
|
||||
return {
|
||||
peerId: selectionId.substring(0, colonIndex),
|
||||
topicId: Number(selectionId.substring(colonIndex + 1)),
|
||||
};
|
||||
});
|
||||
|
||||
const getChipTitle = useLastCallback((selectionId: string): string | undefined => {
|
||||
const { peerId, topicId } = parseSelectionId(selectionId);
|
||||
if (!topicId) return undefined;
|
||||
|
||||
const global = getGlobal();
|
||||
const topic = selectTopic(global, peerId, topicId);
|
||||
const chat = selectChat(global, peerId);
|
||||
|
||||
if (!topic || !chat) return undefined;
|
||||
|
||||
const chatTitle = getChatTitle(lang, chat);
|
||||
return `${topic.title} • ${chatTitle}`;
|
||||
});
|
||||
|
||||
const renderSearchRow = useCallback((props: SearchRowRenderProps) => {
|
||||
if (!hasSelectedChips) {
|
||||
return (
|
||||
<div className="search-input-wrapper">
|
||||
<i className="icon icon-search" />
|
||||
<input
|
||||
ref={props.inputRef}
|
||||
className="search-input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
onKeyDown={props.onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabList
|
||||
tabs={folderTabs}
|
||||
activeTab={activeFolderIndex}
|
||||
onSwitchTab={handleSwitchFolderIndex}
|
||||
/>
|
||||
<div className="search-row-with-chips">
|
||||
<div className="chips-and-search-scroll no-scrollbar">
|
||||
{selectedIds.map((selectionId) => {
|
||||
const { peerId } = parseSelectionId(selectionId);
|
||||
const chipTitle = getChipTitle(selectionId);
|
||||
const isAppearing = appearingIds.includes(selectionId);
|
||||
const isRemoving = removingIds.includes(selectionId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={selectionId}
|
||||
className={buildClassName(
|
||||
'picker-chip-wrapper',
|
||||
isAppearing && 'picker-chip-appear',
|
||||
isRemoving && 'picker-chip-disappear',
|
||||
)}
|
||||
>
|
||||
<PeerChip
|
||||
peerId={peerId}
|
||||
title={chipTitle}
|
||||
size="small"
|
||||
forceShowSelf
|
||||
canClose
|
||||
className="picker-chip"
|
||||
itemClassName="picker-chip-name"
|
||||
clickArg={selectionId}
|
||||
onClick={handleRemoveSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="inline-search">
|
||||
<i className="icon icon-search" />
|
||||
<input
|
||||
ref={props.inputRef}
|
||||
className="search-input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
onKeyDown={props.onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [folderTabs, activeFolderIndex, shouldRenderFolders]);
|
||||
}, [hasSelectedChips, selectedIds, appearingIds, removingIds]);
|
||||
|
||||
const subheaderContent = useMemo(() => {
|
||||
const hasRecentContacts = recentContactIds.length > 0 && !search;
|
||||
const hasFolderTabs = shouldRenderFolders;
|
||||
|
||||
if (!hasRecentContacts && !hasFolderTabs) return undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasRecentContacts && (
|
||||
<PickerRecentContacts
|
||||
contactIds={recentContactIds}
|
||||
currentUserId={currentUserId}
|
||||
selectedIds={isMultiSelect ? selectedIds : undefined}
|
||||
className={styles.recentContacts}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
{Boolean(hasFolderTabs) && folderTabs && (
|
||||
<TabList
|
||||
tabs={folderTabs}
|
||||
activeTab={activeFolderIndex}
|
||||
onSwitchTab={handleSwitchFolderIndex}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
recentContactIds,
|
||||
search,
|
||||
shouldRenderFolders,
|
||||
currentUserId,
|
||||
handleSelect,
|
||||
isMultiSelect,
|
||||
selectedIds,
|
||||
folderTabs,
|
||||
activeFolderIndex,
|
||||
handleSwitchFolderIndex,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ChatOrUserPicker
|
||||
@ -188,13 +401,18 @@ const RecipientPicker = ({
|
||||
className={className}
|
||||
chatOrUserIds={renderingIds}
|
||||
currentUserId={currentUserId}
|
||||
title={title}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
search={search}
|
||||
subheader={chatFolders}
|
||||
renderSearchRow={renderSearchRow}
|
||||
subheader={subheaderContent}
|
||||
footer={footer}
|
||||
viewportFooter={viewportFooter}
|
||||
listActiveKey={activeFolderIndex}
|
||||
selectedIds={isMultiSelect ? selectedIds : undefined}
|
||||
onSearchChange={setSearch}
|
||||
loadMore={loadMore}
|
||||
onSelectChatOrUser={onSelectRecipient}
|
||||
onSelectChatOrUser={handleSelect}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
isLowStackPriority={isLowStackPriority}
|
||||
|
||||
@ -192,18 +192,26 @@ export function getPictogramDimensions(): ApiDimensions {
|
||||
};
|
||||
}
|
||||
|
||||
export function getDocumentThumbnailDimensions(smaller?: boolean): ApiDimensions {
|
||||
if (smaller) {
|
||||
return {
|
||||
width: 3 * REM,
|
||||
height: 3 * REM,
|
||||
};
|
||||
export function getDocumentThumbnailDimensions(
|
||||
size: 'small' | 'medium' | 'large' = 'medium',
|
||||
): ApiDimensions {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return {
|
||||
width: 3 * REM,
|
||||
height: 3 * REM,
|
||||
};
|
||||
case 'large':
|
||||
return {
|
||||
width: 4.5 * REM,
|
||||
height: 4.5 * REM,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
width: 3.375 * REM,
|
||||
height: 3.375 * REM,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: 3.375 * REM,
|
||||
height: 3.375 * REM,
|
||||
};
|
||||
}
|
||||
|
||||
export function getStickerDimensions(sticker: ApiSticker, isMobile?: boolean): ApiDimensions {
|
||||
|
||||
@ -1,13 +1,49 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
@keyframes pickerChipAppear {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable plugin/no-low-performance-animation-properties */
|
||||
@keyframes pickerChipDisappear {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
max-width: 10rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.8);
|
||||
max-width: 10rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
max-width: 0;
|
||||
margin-inline-end: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable plugin/no-low-performance-animation-properties */
|
||||
|
||||
.ChatOrUserPicker {
|
||||
.modal-dialog {
|
||||
overflow: hidden;
|
||||
max-width: 25rem;
|
||||
height: 70%;
|
||||
|
||||
max-width: 26.25rem;
|
||||
height: 38.75rem;
|
||||
max-height: 80%;
|
||||
border-radius: 2.5rem;
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,40 +52,144 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
.picker-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
&-topics {
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
.Button {
|
||||
margin-right: 0.5rem;
|
||||
.picker-back-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.search-input-wrapper,
|
||||
.search-row-with-chips {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
height: 2.75rem;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.TabList {
|
||||
margin-bottom: -0.375rem;
|
||||
margin-inline: -1rem;
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-search {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
|
||||
height: 3rem;
|
||||
padding: 0.5rem 0.75rem 0.5rem 3.125rem;
|
||||
border: none;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--color-text);
|
||||
|
||||
background-color: var(--color-background);
|
||||
outline: none;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.chips-and-search-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
height: 3rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.picker-chip-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.picker-chip-appear {
|
||||
animation: pickerChipAppear 0.2s ease-out;
|
||||
}
|
||||
|
||||
.picker-chip-disappear {
|
||||
overflow: hidden;
|
||||
animation: pickerChipDisappear 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.picker-chip {
|
||||
flex-shrink: 0;
|
||||
max-width: 10rem;
|
||||
|
||||
&:not(:hover) {
|
||||
background-color: var(--color-primary-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-chip:not(:hover) .picker-chip-name {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.picker-chip-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.inline-search {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 10rem;
|
||||
|
||||
.icon-search {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
padding: 0 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
font-size: 1rem;
|
||||
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +197,9 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 !important;
|
||||
|
||||
padding-block: 0 !important;
|
||||
padding-inline: 1rem !important;
|
||||
|
||||
> .Transition {
|
||||
overflow: hidden;
|
||||
@ -69,10 +211,15 @@
|
||||
overflow-y: auto;
|
||||
|
||||
height: 100%;
|
||||
padding-block: 0.125rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-block: 0.375rem;
|
||||
padding-inline: 0.5rem;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
background-color: var(--color-background);
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem, 0.125rem);
|
||||
|
||||
body.is-ios &,
|
||||
body.is-android & {
|
||||
@ -85,6 +232,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.picker-footer) {
|
||||
.picker-list {
|
||||
margin-bottom: 3rem;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -97,8 +252,16 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
.picker-list-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.picker-list-spacer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.ChatOrUserPicker-item {
|
||||
@ -108,6 +271,86 @@
|
||||
.online {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover .forum-badge {
|
||||
outline-color: var(--color-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-avatar-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.forum-badge {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: -0.125rem;
|
||||
bottom: -0.125rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
border-radius: 50%;
|
||||
|
||||
font-size: 0.625rem;
|
||||
color: var(--color-white);
|
||||
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(50px);
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
outline: 1.5px solid var(--color-background);
|
||||
}
|
||||
|
||||
.picker-checkbox {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
border: 2px solid var(--color-borders-input);
|
||||
border-radius: 50%;
|
||||
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
color: var(--color-white);
|
||||
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
|
||||
&.selected {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-checkbox-count {
|
||||
position: absolute;
|
||||
right: -0.375rem;
|
||||
bottom: -0.375rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
min-width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
border: 1px solid var(--color-white);
|
||||
border-radius: 0.4375rem;
|
||||
|
||||
font-size: 0.625rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 0.625rem;
|
||||
color: var(--color-white);
|
||||
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.topic-icon {
|
||||
@ -143,4 +386,84 @@
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-footer {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
|
||||
padding-top: 1rem;
|
||||
|
||||
background:
|
||||
linear-gradient(
|
||||
to top,
|
||||
var(--color-background-secondary) 0%,
|
||||
var(--color-background-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.picker-caption-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.1875rem;
|
||||
padding-inline-start: 1rem;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.picker-caption-input {
|
||||
flex: 1;
|
||||
|
||||
min-width: 0;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-footer-button {
|
||||
width: 100%;
|
||||
border-radius: 1.5rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.picker-footer-input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.picker-send-button {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
height: 2.625rem;
|
||||
padding: 0 1.0625rem !important;
|
||||
border-radius: 1.375rem;
|
||||
|
||||
font-size: 2rem;
|
||||
|
||||
&:has(.icon-star) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import type { ElementRef, TeactNode } from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
@ -34,14 +34,13 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import InfiniteScroll, { type OwnProps as InfiniteScrollProps } from '../../ui/InfiniteScroll';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Transition from '../../ui/Transition';
|
||||
import Avatar from '../Avatar';
|
||||
import FullNameTitle from '../FullNameTitle';
|
||||
import Icon from '../icons/Icon';
|
||||
import TopicIcon from '../TopicIcon';
|
||||
import PickerItem from './PickerItem';
|
||||
|
||||
@ -51,12 +50,17 @@ export type OwnProps = {
|
||||
currentUserId?: string;
|
||||
chatOrUserIds: string[];
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
searchPlaceholder: string;
|
||||
search: string;
|
||||
className?: string;
|
||||
isLowStackPriority?: boolean;
|
||||
listActiveKey?: number;
|
||||
subheader?: TeactNode;
|
||||
renderSearchRow?: (props: SearchRowRenderProps) => TeactNode;
|
||||
footer?: TeactNode;
|
||||
viewportFooter?: TeactNode;
|
||||
selectedIds?: string[];
|
||||
loadMore?: NoneToVoidFunction;
|
||||
onSearchChange: (search: string) => void;
|
||||
onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
|
||||
@ -75,16 +79,29 @@ const TOPIC_ICON_SIZE = 2.75 * REM;
|
||||
const ITEM_CLASS_NAME = 'ChatOrUserPicker-item';
|
||||
const TOPIC_ITEM_HEIGHT_PX = 56;
|
||||
|
||||
export type SearchRowRenderProps = {
|
||||
inputRef: ElementRef<HTMLInputElement>;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
const ChatOrUserPicker = ({
|
||||
isOpen,
|
||||
currentUserId,
|
||||
chatOrUserIds,
|
||||
title,
|
||||
search,
|
||||
searchPlaceholder,
|
||||
className,
|
||||
isLowStackPriority,
|
||||
subheader,
|
||||
renderSearchRow,
|
||||
footer,
|
||||
viewportFooter,
|
||||
listActiveKey,
|
||||
selectedIds,
|
||||
animationLevel,
|
||||
shouldSkipHistoryAnimations,
|
||||
loadMore,
|
||||
@ -113,13 +130,13 @@ const ChatOrUserPicker = ({
|
||||
useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch);
|
||||
useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE);
|
||||
|
||||
const selectTopicsById = useLastCallback((global: GlobalState) => {
|
||||
const selectTopicsById = useCallback((global: GlobalState) => {
|
||||
if (!forumId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectTopics(global, forumId);
|
||||
});
|
||||
}, [forumId]);
|
||||
|
||||
const forumTopicsById = useSelector(selectTopicsById);
|
||||
|
||||
@ -190,6 +207,8 @@ const ChatOrUserPicker = ({
|
||||
}
|
||||
}, `.${ITEM_CLASS_NAME}`, true);
|
||||
|
||||
const isMultiSelect = Boolean(selectedIds);
|
||||
|
||||
const handleClick = useLastCallback((chatId: string) => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const chat = chatsById?.[chatId];
|
||||
@ -214,7 +233,16 @@ const ChatOrUserPicker = ({
|
||||
peer = monoforumChannel;
|
||||
}
|
||||
|
||||
const chat = global.chats.byId[id];
|
||||
const isForum = chat?.isForum;
|
||||
|
||||
const isSelf = peer && !isApiPeerChat(peer) ? peer.isSelf : undefined;
|
||||
const isSelected = selectedIds?.includes(id);
|
||||
|
||||
const selectedTopicsCount = isForum && selectedIds
|
||||
? selectedIds.filter((selId) => selId.startsWith(`${id}:`)).length
|
||||
: 0;
|
||||
const hasSelectedTopics = selectedTopicsCount > 0;
|
||||
|
||||
function getSubtitle() {
|
||||
if (!peer) return undefined;
|
||||
@ -232,6 +260,15 @@ const ChatOrUserPicker = ({
|
||||
|
||||
const [subtitle, subtitleClassName] = getSubtitle() || [];
|
||||
|
||||
const checkboxElement = selectedIds ? (
|
||||
<div className={buildClassName('picker-checkbox', (isSelected || hasSelectedTopics) && 'selected')}>
|
||||
{(isSelected || hasSelectedTopics) && <Icon name="check-bold" />}
|
||||
{hasSelectedTopics && (
|
||||
<div className="picker-checkbox-count">{selectedTopicsCount}</div>
|
||||
)}
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<PickerItem
|
||||
key={id}
|
||||
@ -247,13 +284,18 @@ const ChatOrUserPicker = ({
|
||||
</div>
|
||||
)}
|
||||
avatarElement={(
|
||||
<Avatar
|
||||
peer={peer}
|
||||
asMessageBubble={Boolean(monoforumChannel)}
|
||||
isSavedMessages={isSelf}
|
||||
size="medium"
|
||||
/>
|
||||
<div className="picker-avatar-wrapper">
|
||||
<Avatar
|
||||
peer={peer}
|
||||
asMessageBubble={Boolean(monoforumChannel)}
|
||||
isSavedMessages={isSelf}
|
||||
size="medium"
|
||||
/>
|
||||
{isForum && <Icon name="forums" className="forum-badge" />}
|
||||
</div>
|
||||
)}
|
||||
inputElement={checkboxElement}
|
||||
inputPosition="end"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
ripple
|
||||
@ -262,29 +304,33 @@ const ChatOrUserPicker = ({
|
||||
onClick={() => handleClick(id)}
|
||||
/>
|
||||
);
|
||||
}, [currentUserId, oldLang, lang, viewportOffset]);
|
||||
}, [currentUserId, oldLang, lang, viewportOffset, selectedIds]);
|
||||
|
||||
function renderTopicList() {
|
||||
return (
|
||||
<>
|
||||
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className="search-wrapper">
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="tiny"
|
||||
ariaLabel={oldLang('Back')}
|
||||
onClick={handleHeaderBackClick}
|
||||
iconName="arrow-left"
|
||||
/>
|
||||
<InputText
|
||||
ref={topicSearchRef}
|
||||
value={topicSearch}
|
||||
onChange={handleTopicSearchChange}
|
||||
onKeyDown={handleTopicKeyDown}
|
||||
placeholder={searchPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="picker-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{renderSearchRow ? renderSearchRow({
|
||||
inputRef: topicSearchRef,
|
||||
value: topicSearch,
|
||||
placeholder: searchPlaceholder,
|
||||
onChange: handleTopicSearchChange,
|
||||
onKeyDown: handleTopicKeyDown,
|
||||
}) : (
|
||||
<div className="search-input-wrapper">
|
||||
<i className="icon icon-search" />
|
||||
<input
|
||||
ref={topicSearchRef}
|
||||
className="search-input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={searchPlaceholder}
|
||||
value={topicSearch}
|
||||
onChange={handleTopicSearchChange}
|
||||
onKeyDown={handleTopicKeyDown}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{topicIds?.length ? (
|
||||
<InfiniteScroll
|
||||
@ -295,29 +341,51 @@ const ChatOrUserPicker = ({
|
||||
maxHeight={(topicIds?.length || 0) * TOPIC_ITEM_HEIGHT_PX}
|
||||
onKeyDown={handleTopicKeyDown}
|
||||
>
|
||||
{topicIds.map((topicId, i) => (
|
||||
<PickerItem
|
||||
key={`${forumId}_${topicId}`}
|
||||
className={ITEM_CLASS_NAME}
|
||||
{topicIds.map((topicId, i) => {
|
||||
const selectionId = `${forumId}:${topicId}`;
|
||||
const isTopicSelected = selectedIds?.includes(selectionId);
|
||||
|
||||
onClick={() => onSelectChatOrUser(forumId!, topicId)}
|
||||
style={`top: ${i * TOPIC_ITEM_HEIGHT_PX}px;`}
|
||||
avatarElement={(
|
||||
<TopicIcon
|
||||
size={TOPIC_ICON_SIZE}
|
||||
topic={topics[topicId]}
|
||||
className="topic-icon"
|
||||
letterClassName="topic-icon-letter"
|
||||
/>
|
||||
)}
|
||||
title={renderText(topics[topicId].title)}
|
||||
const topicCheckboxElement = isMultiSelect ? (
|
||||
<div className={buildClassName('picker-checkbox', isTopicSelected && 'selected')}>
|
||||
{isTopicSelected && <Icon name="check-bold" />}
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<PickerItem
|
||||
key={`${forumId}_${topicId}`}
|
||||
className={ITEM_CLASS_NAME}
|
||||
onClick={() => onSelectChatOrUser(forumId!, topicId)}
|
||||
style={`top: ${i * TOPIC_ITEM_HEIGHT_PX}px;`}
|
||||
avatarElement={(
|
||||
<div className="picker-avatar-wrapper">
|
||||
<TopicIcon
|
||||
size={TOPIC_ICON_SIZE}
|
||||
topic={topics[topicId]}
|
||||
className="topic-icon"
|
||||
letterClassName="topic-icon-letter"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
title={renderText(topics[topicId].title)}
|
||||
inputElement={topicCheckboxElement}
|
||||
inputPosition="end"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{Boolean(viewportFooter) && (
|
||||
<div
|
||||
className="picker-list-spacer"
|
||||
style={`top: ${topicIds.length * TOPIC_ITEM_HEIGHT_PX}px`}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
) : topicIds && !topicIds.length ? (
|
||||
<p className="no-results">{lang('NothingFound')}</p>
|
||||
) : (
|
||||
<Loading />
|
||||
<div className="picker-list picker-list-loading">
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -326,24 +394,28 @@ const ChatOrUserPicker = ({
|
||||
function renderChatList() {
|
||||
return (
|
||||
<>
|
||||
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className="search-wrapper">
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="tiny"
|
||||
ariaLabel={oldLang('Close')}
|
||||
onClick={onClose}
|
||||
iconName="close"
|
||||
/>
|
||||
<InputText
|
||||
ref={searchRef}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={chatKeyDownHandler}
|
||||
placeholder={searchPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="picker-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{renderSearchRow ? renderSearchRow({
|
||||
inputRef: searchRef,
|
||||
value: search,
|
||||
placeholder: searchPlaceholder,
|
||||
onChange: handleSearchChange,
|
||||
onKeyDown: chatKeyDownHandler,
|
||||
}) : (
|
||||
<div className="search-input-wrapper">
|
||||
<i className="icon icon-search" />
|
||||
<input
|
||||
ref={searchRef}
|
||||
className="search-input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={searchPlaceholder}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={chatKeyDownHandler}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{subheader}
|
||||
</div>
|
||||
<Transition
|
||||
@ -355,6 +427,7 @@ const ChatOrUserPicker = ({
|
||||
isOpen={isOpen}
|
||||
viewportIds={viewportIds}
|
||||
maxHeight={chatOrUserIds.length * PEER_PICKER_ITEM_HEIGHT_PX}
|
||||
viewportFooter={viewportFooter}
|
||||
onLoadMore={getMore}
|
||||
onSelect={handleChatSelect}
|
||||
renderItem={renderChatItem}
|
||||
@ -365,12 +438,24 @@ const ChatOrUserPicker = ({
|
||||
);
|
||||
}
|
||||
|
||||
const handleModalClose = useLastCallback(() => {
|
||||
if (forumId) {
|
||||
handleHeaderBackClick();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
hasCloseButton
|
||||
isBackButton={Boolean(forumId)}
|
||||
headerClassName="modal-header-condensed-wide"
|
||||
className={buildClassName('ChatOrUserPicker', className)}
|
||||
isLowStackPriority={isLowStackPriority}
|
||||
onClose={onClose}
|
||||
onClose={handleModalClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
>
|
||||
<Transition activeKey={activeKey} name="slideFade" slideClassName="ChatOrUserPicker_slide">
|
||||
@ -378,6 +463,7 @@ const ChatOrUserPicker = ({
|
||||
return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList();
|
||||
}}
|
||||
</Transition>
|
||||
{footer}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -386,6 +472,7 @@ type ChatListContentProps = {
|
||||
isOpen: boolean;
|
||||
viewportIds?: string[];
|
||||
maxHeight: number;
|
||||
viewportFooter?: TeactNode;
|
||||
onLoadMore: InfiniteScrollProps['onLoadMore'];
|
||||
onSelect: (index: number) => void;
|
||||
renderItem: (id: string, index: number) => TeactNode;
|
||||
@ -396,6 +483,7 @@ function ChatListContent({
|
||||
isOpen,
|
||||
viewportIds,
|
||||
maxHeight,
|
||||
viewportFooter,
|
||||
onLoadMore,
|
||||
onSelect,
|
||||
onKeyDownHandlerUpdate,
|
||||
@ -424,6 +512,9 @@ function ChatListContent({
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{viewportIds.map(renderItem)}
|
||||
{Boolean(viewportFooter) && (
|
||||
<div className="picker-list-spacer" style={`top: ${maxHeight}px`} />
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
) : viewportIds && !viewportIds.length ? (
|
||||
<p className="no-results">{lang('NothingFound')}</p>
|
||||
|
||||
110
src/components/common/pickers/PickerRecentContacts.module.scss
Normal file
110
src/components/common/pickers/PickerRecentContacts.module.scss
Normal file
@ -0,0 +1,110 @@
|
||||
.root {
|
||||
overflow: hidden;
|
||||
|
||||
padding: 0.3125rem 0;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
scrollbar-width: none;
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
padding-inline: 0.5625rem;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
min-width: 3.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.375rem;
|
||||
padding-inline: 0.25rem;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
position: relative;
|
||||
margin-bottom: 0.3125rem;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: -0.125rem;
|
||||
bottom: -0.125rem;
|
||||
transform: scale(0);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--color-background);
|
||||
border-radius: 50%;
|
||||
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-white);
|
||||
|
||||
opacity: 0;
|
||||
background-color: var(--color-primary);
|
||||
|
||||
transition: transform 0.15s, opacity 0.15s, border-color 0.15s;
|
||||
|
||||
.item:hover & {
|
||||
border-color: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.selected {
|
||||
.name {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
88
src/components/common/pickers/PickerRecentContacts.tsx
Normal file
88
src/components/common/pickers/PickerRecentContacts.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { memo, useRef } from '../../../lib/teact/teact';
|
||||
import { getGlobal } from '../../../global';
|
||||
|
||||
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||
import { selectPeer } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import Icon from '../icons/Icon';
|
||||
|
||||
import styles from './PickerRecentContacts.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
contactIds: string[];
|
||||
currentUserId?: string;
|
||||
selectedIds?: string[];
|
||||
className?: string;
|
||||
onSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
const PickerRecentContacts = ({
|
||||
contactIds,
|
||||
currentUserId,
|
||||
selectedIds,
|
||||
className,
|
||||
onSelect,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
|
||||
useHorizontalScroll(containerRef, !contactIds.length);
|
||||
|
||||
const handleClick = useLastCallback((id: string) => {
|
||||
onSelect(id);
|
||||
});
|
||||
|
||||
// Current user (Saved Messages) goes first, then contacts
|
||||
const displayIds = currentUserId
|
||||
? [currentUserId, ...contactIds.filter((id) => id !== currentUserId)]
|
||||
: contactIds;
|
||||
|
||||
if (!displayIds.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div ref={containerRef} className={styles.scrollContainer}>
|
||||
{displayIds.map((peerId) => {
|
||||
const global = getGlobal();
|
||||
const peer = selectPeer(global, peerId);
|
||||
if (!peer) return undefined;
|
||||
|
||||
const isSelf = peerId === currentUserId;
|
||||
const isSelected = selectedIds?.includes(peerId);
|
||||
const name = isSelf ? lang('SavedMessagesShort') : getPeerTitle(lang, peer);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={peerId}
|
||||
className={buildClassName(styles.item, isSelected && styles.selected)}
|
||||
onClick={() => handleClick(peerId)}
|
||||
>
|
||||
<div className={styles.avatarWrapper}>
|
||||
<Avatar
|
||||
peer={peer}
|
||||
isSavedMessages={isSelf}
|
||||
size={48}
|
||||
/>
|
||||
<div className={styles.checkmark}>
|
||||
<Icon name="check-bold" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.name}>{renderText(name || lang('ActionFallbackSomeone'))}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PickerRecentContacts);
|
||||
@ -26,7 +26,7 @@ import useScrolledState from '../../../hooks/useScrolledState';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import StoryRibbon from '../../story/StoryRibbon';
|
||||
import TabList from '../../ui/TabList';
|
||||
import SquareTabList from '../../ui/SquareTabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import ChatList from './ChatList';
|
||||
|
||||
@ -259,7 +259,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{shouldRenderStoryRibbon && <StoryRibbon isClosing={isStoryRibbonClosing} />}
|
||||
{shouldRenderFolders ? (
|
||||
<TabList
|
||||
<SquareTabList
|
||||
contextRootElementSelector="#LeftColumn"
|
||||
tabs={folderTabs}
|
||||
activeTab={activeChatFolder}
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.TabList {
|
||||
.SquareTabList {
|
||||
z-index: 1;
|
||||
|
||||
justify-content: flex-start;
|
||||
@ -48,7 +48,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--tabs-hidden .TabList {
|
||||
&--tabs-hidden .SquareTabList {
|
||||
pointer-events: none;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
|
||||
document={getMessageDocument(message)!}
|
||||
message={message}
|
||||
datetime={message.date}
|
||||
smaller
|
||||
fileSize="small"
|
||||
sender={getSenderName(lang, message, chatsById, usersById)}
|
||||
className="scroll-item"
|
||||
isDownloading={getIsDownloading(activeDownloads, message.content.document!)}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.TabList {
|
||||
.SquareTabList {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import TabList from '../../ui/TabList';
|
||||
import SquareTabList from '../../ui/SquareTabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import AudioResults from './AudioResults';
|
||||
import BotAppResults from './BotAppResults';
|
||||
@ -122,7 +122,7 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
|
||||
<TabList activeTab={activeTab} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
||||
<SquareTabList activeTab={activeTab} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
||||
<Transition
|
||||
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||
renderCount={tabs.length}
|
||||
|
||||
@ -12,7 +12,7 @@ import { filterPeersByQuery } from '../../../global/helpers/peers';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||
|
||||
@ -43,7 +43,7 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
|
||||
blockUser,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,7 +76,8 @@ const BlockUserModal: FC<OwnProps & StateProps> = ({
|
||||
<ChatOrUserPicker
|
||||
isOpen={isOpen}
|
||||
chatOrUserIds={filteredContactIds}
|
||||
searchPlaceholder={lang('BlockedUsers.BlockUser')}
|
||||
title={lang('BlockedUsersBlockUser')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
onSelectChatOrUser={handleRemoveUser}
|
||||
|
||||
@ -5,7 +5,7 @@ import { getActions } from '../../global';
|
||||
import type { TabState } from '../../global/types';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import RecipientPicker from '../common/RecipientPicker';
|
||||
|
||||
@ -17,7 +17,7 @@ const AttachBotRecipientPicker: FC<OwnProps> = ({
|
||||
requestedAttachBotInChat,
|
||||
}) => {
|
||||
const { cancelAttachBotInChat, callAttachBot } = getActions();
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(requestedAttachBotInChat);
|
||||
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
||||
@ -41,6 +41,7 @@ const AttachBotRecipientPicker: FC<OwnProps> = ({
|
||||
return (
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
title={lang('SelectChat')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
filter={filter}
|
||||
onSelectRecipient={handlePeerRecipient}
|
||||
|
||||
@ -8,7 +8,7 @@ import type { TabState } from '../../global/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import RecipientPicker from '../common/RecipientPicker';
|
||||
|
||||
@ -25,7 +25,7 @@ const DraftRecipientPicker: FC<OwnProps> = ({
|
||||
resetOpenChatWithDraft,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
||||
useEffect(() => {
|
||||
@ -54,7 +54,8 @@ const DraftRecipientPicker: FC<OwnProps> = ({
|
||||
return (
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
searchPlaceholder={lang('ForwardTo')}
|
||||
title={lang('ShareWith')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
filter={requestedDraft?.filter}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onClose={handleClose}
|
||||
|
||||
29
src/components/main/ForwardRecipientPicker.module.scss
Normal file
29
src/components/main/ForwardRecipientPicker.module.scss
Normal file
@ -0,0 +1,29 @@
|
||||
.buttonLayer {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.composerLayer {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.visible {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.buttonSlide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import {
|
||||
memo, useCallback, useEffect,
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
@ -8,17 +8,33 @@ import type { ThreadId } from '../../types';
|
||||
|
||||
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
|
||||
import {
|
||||
selectCanCopyMessageLink,
|
||||
selectChat,
|
||||
selectChatMessages,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectTabState,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { isUserId } from '../../util/entities/ids';
|
||||
import { formatStarsAsIcon, formatStarsAsText } from '../../util/localization/format';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useFrozenProps from '../../hooks/useFrozenProps';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import RecipientPicker from '../common/RecipientPicker';
|
||||
import Button from '../ui/Button';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import Transition from '../ui/Transition';
|
||||
|
||||
import styles from './ForwardRecipientPicker.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
@ -29,6 +45,9 @@ interface StateProps {
|
||||
isManyMessages?: boolean;
|
||||
isStory?: boolean;
|
||||
isForwarding?: boolean;
|
||||
fromChatId?: string;
|
||||
forwardMessageIds?: number[];
|
||||
shouldPaidMessageAutoApprove?: boolean;
|
||||
}
|
||||
|
||||
const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
@ -37,26 +56,80 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
isManyMessages,
|
||||
isStory,
|
||||
isForwarding,
|
||||
fromChatId,
|
||||
forwardMessageIds,
|
||||
shouldPaidMessageAutoApprove,
|
||||
}) => {
|
||||
const {
|
||||
openChatOrTopicWithReplyInDraft,
|
||||
setForwardChatOrTopic,
|
||||
exitForwardMode,
|
||||
forwardToSavedMessages,
|
||||
forwardToMultipleChats,
|
||||
forwardStory,
|
||||
showNotification,
|
||||
copyMessageLink,
|
||||
openStarsBalanceModal,
|
||||
setPaidMessageAutoApprove,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const renderingIsStory = usePreviousDeprecated(isStory, true);
|
||||
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [caption, setCaption] = useState('');
|
||||
const [isPaymentConfirmOpen, openPaymentConfirm, closePaymentConfirm] = useFlag();
|
||||
const [shouldAutoApprove, setShouldAutoApprove] = useState(shouldPaidMessageAutoApprove);
|
||||
|
||||
const isMultiSelect = isForwarding && !isStory;
|
||||
const messageCount = forwardMessageIds?.length || 0;
|
||||
|
||||
const paidChatsInfo = useMemo(() => {
|
||||
if (!selectedIds.length) return { paidChatsCount: 0, totalStars: 0, totalMessages: 0 };
|
||||
|
||||
const global = getGlobal();
|
||||
let paidChatsCount = 0;
|
||||
let totalStars = 0;
|
||||
const hasCaption = caption.trim().length > 0;
|
||||
const totalMessages = messageCount + (hasCaption ? 1 : 0);
|
||||
|
||||
for (const chatId of selectedIds) {
|
||||
const paidStars = selectPeerPaidMessagesStars(global, chatId);
|
||||
if (paidStars) {
|
||||
paidChatsCount++;
|
||||
totalStars += paidStars * totalMessages;
|
||||
}
|
||||
}
|
||||
|
||||
return { paidChatsCount, totalStars, totalMessages };
|
||||
}, [selectedIds, messageCount, caption]);
|
||||
|
||||
const canCopyLink = useMemo(() => {
|
||||
if (!fromChatId || forwardMessageIds?.length !== 1) return false;
|
||||
|
||||
const global = getGlobal();
|
||||
const chatMessages = selectChatMessages(global, fromChatId);
|
||||
if (!chatMessages) return false;
|
||||
|
||||
const message = chatMessages[forwardMessageIds[0]];
|
||||
return message && selectCanCopyMessageLink(global, message);
|
||||
}, [fromChatId, forwardMessageIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
markIsShown();
|
||||
}
|
||||
}, [isOpen, markIsShown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSelectedIds([]);
|
||||
setCaption('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
|
||||
const isSelf = recipientId === currentUserId;
|
||||
if (isStory) {
|
||||
@ -65,8 +138,8 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
if (isUserId(recipientId)) {
|
||||
showNotification({
|
||||
message: isSelf
|
||||
? lang('Conversation.StoryForwardTooltip.SavedMessages.One')
|
||||
: lang(
|
||||
? oldLang('Conversation.StoryForwardTooltip.SavedMessages.One')
|
||||
: oldLang(
|
||||
'StorySharedTo',
|
||||
getUserFirstOrLastName(selectUser(global, recipientId)),
|
||||
),
|
||||
@ -76,14 +149,14 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
if (!chat) return;
|
||||
|
||||
showNotification({
|
||||
message: lang('StorySharedTo', getChatTitle(lang, chat)),
|
||||
message: oldLang('StorySharedTo', getChatTitle(oldLang, chat)),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelf) {
|
||||
const message = lang(
|
||||
const message = oldLang(
|
||||
isManyMessages
|
||||
? 'Conversation.ForwardTooltip.SavedMessages.Many'
|
||||
: 'Conversation.ForwardTooltip.SavedMessages.One',
|
||||
@ -100,37 +173,239 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
openChatOrTopicWithReplyInDraft({ chatId, topicId });
|
||||
}
|
||||
}
|
||||
}, [currentUserId, isManyMessages, isStory, lang, isForwarding]);
|
||||
}, [currentUserId, isManyMessages, isStory, oldLang, isForwarding]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
exitForwardMode();
|
||||
}, [exitForwardMode]);
|
||||
|
||||
const handleSelectedIdsChange = useLastCallback((ids: string[]) => {
|
||||
setSelectedIds(ids);
|
||||
});
|
||||
|
||||
const handleCopyLink = useLastCallback(() => {
|
||||
if (!fromChatId || !forwardMessageIds?.length) return;
|
||||
copyMessageLink({
|
||||
chatId: fromChatId,
|
||||
messageId: forwardMessageIds[0],
|
||||
});
|
||||
exitForwardMode();
|
||||
});
|
||||
|
||||
const handleForwardToMultiple = useLastCallback(() => {
|
||||
if (!selectedIds.length) return;
|
||||
|
||||
if (selectedIds.length === 1) {
|
||||
setForwardChatOrTopic({ chatId: selectedIds[0] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (paidChatsInfo.totalStars > 0 && !shouldPaidMessageAutoApprove) {
|
||||
openPaymentConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (paidChatsInfo.totalStars > 0) {
|
||||
const starsBalance = getGlobal().stars?.balance?.amount || 0;
|
||||
if (paidChatsInfo.totalStars > starsBalance) {
|
||||
openStarsBalanceModal({
|
||||
topup: {
|
||||
balanceNeeded: paidChatsInfo.totalStars,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
executeForward();
|
||||
});
|
||||
|
||||
const executeForward = useLastCallback(() => {
|
||||
forwardToMultipleChats({ toChatIds: selectedIds, comment: caption || undefined });
|
||||
|
||||
showNotification({
|
||||
message: lang('FwdMessagesToChats', { count: selectedIds.length }, { pluralValue: selectedIds.length }),
|
||||
});
|
||||
exitForwardMode();
|
||||
});
|
||||
|
||||
const handlePaymentConfirm = useLastCallback(() => {
|
||||
const { totalStars } = paidChatsInfo;
|
||||
const starsBalance = getGlobal().stars?.balance?.amount || 0;
|
||||
|
||||
if (totalStars > starsBalance) {
|
||||
openStarsBalanceModal({
|
||||
topup: {
|
||||
balanceNeeded: totalStars,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
closePaymentConfirm();
|
||||
if (shouldAutoApprove) {
|
||||
setPaidMessageAutoApprove();
|
||||
}
|
||||
executeForward();
|
||||
});
|
||||
|
||||
const viewportFooter = useMemo(() => (
|
||||
<div className="picker-list-spacer" />
|
||||
), []);
|
||||
|
||||
const selectedCount = selectedIds.length;
|
||||
const showComposer = selectedCount >= 2;
|
||||
const { totalStars: displayedTotalStars } = useFrozenProps(
|
||||
{ totalStars: paidChatsInfo.totalStars },
|
||||
!showComposer,
|
||||
);
|
||||
|
||||
const footerContent = useMemo(() => {
|
||||
if (!isForwarding || isStory) return undefined;
|
||||
|
||||
const renderButton = () => {
|
||||
const isInitial = selectedCount === 0;
|
||||
const singleChatStars = selectedCount === 1 ? paidChatsInfo.totalStars : 0;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="picker-footer-button"
|
||||
color="primary"
|
||||
disabled={isInitial && !canCopyLink}
|
||||
onClick={isInitial ? handleCopyLink : handleForwardToMultiple}
|
||||
>
|
||||
<Transition name="fade" activeKey={isInitial ? 0 : 1} slideClassName={styles.buttonSlide}>
|
||||
<span>
|
||||
{isInitial
|
||||
? (canCopyLink ? oldLang('CopyLink') : lang('SelectChats'))
|
||||
: (singleChatStars > 0
|
||||
? lang(
|
||||
'ForwardForStars',
|
||||
{ price: formatStarsAsIcon(lang, singleChatStars, { asFont: true }) },
|
||||
{ withNodes: true },
|
||||
)
|
||||
: lang('Forward'))}
|
||||
</span>
|
||||
</Transition>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderComposer = () => (
|
||||
<div className="picker-footer-input">
|
||||
<div className="picker-caption-wrapper">
|
||||
<input
|
||||
className="picker-caption-input"
|
||||
type="text"
|
||||
value={caption}
|
||||
onChange={(e) => setCaption(e.currentTarget.value)}
|
||||
placeholder={lang('AttachmentCaptionPlaceholder')}
|
||||
/>
|
||||
<Button
|
||||
className="picker-send-button"
|
||||
color="primary"
|
||||
onClick={handleForwardToMultiple}
|
||||
ariaLabel={lang('Forward')}
|
||||
>
|
||||
{displayedTotalStars > 0 ? (
|
||||
<>
|
||||
<Icon name="star" className="star-icon" />
|
||||
<AnimatedCounter text={String(displayedTotalStars)} />
|
||||
</>
|
||||
) : <i className="icon icon-new-send" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="picker-footer">
|
||||
<div className={buildClassName(styles.buttonLayer, !showComposer && styles.visible)}>
|
||||
{renderButton()}
|
||||
</div>
|
||||
<div className={buildClassName(styles.composerLayer, showComposer && styles.visible)}>
|
||||
{renderComposer()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [isForwarding, isStory, selectedCount, showComposer, caption, canCopyLink, displayedTotalStars,
|
||||
paidChatsInfo, handleForwardToMultiple, handleCopyLink, lang, oldLang]);
|
||||
|
||||
if (!isOpen && !isShown) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const confirmPaymentMessage = paidChatsInfo.totalStars > 0 ? lang(
|
||||
'ForwardPaidChatsConfirmation',
|
||||
{
|
||||
chatsSelected: lang(
|
||||
'ForwardPaidChatsSelected',
|
||||
{ paidChatsCount: paidChatsInfo.paidChatsCount },
|
||||
{ withNodes: true, withMarkdown: true, pluralValue: paidChatsInfo.paidChatsCount },
|
||||
),
|
||||
payConfirmation: lang(
|
||||
'ForwardPaidChatsPayConfirmation',
|
||||
{
|
||||
totalAmount: formatStarsAsText(lang, paidChatsInfo.totalStars),
|
||||
count: paidChatsInfo.totalMessages,
|
||||
},
|
||||
{ withNodes: true, withMarkdown: true, pluralValue: paidChatsInfo.totalMessages },
|
||||
),
|
||||
},
|
||||
{ withNodes: true },
|
||||
) : undefined;
|
||||
|
||||
const confirmLabel = lang('PayForMessage', { count: paidChatsInfo.totalMessages }, {
|
||||
withNodes: true,
|
||||
pluralValue: paidChatsInfo.totalMessages,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
className={renderingIsStory ? 'component-theme-dark' : undefined}
|
||||
searchPlaceholder={lang(isForwarding ? 'ForwardTo' : 'ReplyToDialog')}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onClose={handleClose}
|
||||
onCloseAnimationEnd={unmarkIsShown}
|
||||
isForwarding={isForwarding}
|
||||
withFolders
|
||||
/>
|
||||
<>
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
className={renderingIsStory ? 'component-theme-dark' : undefined}
|
||||
title={lang('ShareWith')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
isMultiSelect={isMultiSelect}
|
||||
footer={footerContent}
|
||||
viewportFooter={viewportFooter}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
onClose={handleClose}
|
||||
onCloseAnimationEnd={unmarkIsShown}
|
||||
isForwarding={isForwarding}
|
||||
withFolders
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={lang('TitleConfirmPayment')}
|
||||
confirmLabel={confirmLabel}
|
||||
isOpen={isPaymentConfirmOpen}
|
||||
onClose={closePaymentConfirm}
|
||||
confirmHandler={handlePaymentConfirm}
|
||||
>
|
||||
{confirmPaymentMessage}
|
||||
<Checkbox
|
||||
label={lang('DoNotAskAgain')}
|
||||
checked={shouldAutoApprove}
|
||||
onCheck={setShouldAutoApprove}
|
||||
/>
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
|
||||
const { messageIds, storyId } = selectTabState(global).forwardMessages;
|
||||
const { messageIds, storyId, fromChatId } = selectTabState(global).forwardMessages;
|
||||
const isForwarding = (messageIds && messageIds.length > 0);
|
||||
|
||||
return {
|
||||
currentUserId: global.currentUserId,
|
||||
isManyMessages: (messageIds?.length || 0) > 1,
|
||||
isStory: Boolean(storyId),
|
||||
isForwarding,
|
||||
fromChatId,
|
||||
forwardMessageIds: messageIds,
|
||||
shouldPaidMessageAutoApprove: global.settings.byKey.shouldPaidMessageAutoApprove,
|
||||
};
|
||||
})(ForwardRecipientPicker));
|
||||
|
||||
@ -4,13 +4,15 @@
|
||||
:global {
|
||||
.modal-dialog {
|
||||
max-width: 26.25rem;
|
||||
border-radius: 2.5rem;
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header-condensed {
|
||||
.modal-header-condensed-wide {
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 250ms ease-in-out;
|
||||
}
|
||||
@ -32,15 +34,16 @@
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
|
||||
width: 3.5rem !important;
|
||||
height: 3.5rem !important;
|
||||
width: 3rem !important;
|
||||
height: 3rem !important;
|
||||
padding: 0 !important;
|
||||
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.symbol-menu-button, .mobile-symbol-menu-button {
|
||||
margin-right: -1.75rem;
|
||||
align-self: center;
|
||||
margin-right: -1.5rem;
|
||||
margin-left: -0.25rem !important;
|
||||
color: var(--color-composer-button);
|
||||
}
|
||||
@ -59,12 +62,8 @@
|
||||
|
||||
&.mobile :global {
|
||||
.modal-dialog {
|
||||
align-self: end;
|
||||
|
||||
max-width: 100% !important;
|
||||
margin: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,37 +79,49 @@
|
||||
transform: translate3d(0, calc((var(--symbol-menu-height) - env(safe-area-inset-bottom)) * -1), 0);
|
||||
}
|
||||
}
|
||||
|
||||
&.header-border :global(.modal-header-condensed) {
|
||||
border-bottom-color: var(--color-borders);
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
scrollbar-width: none;
|
||||
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
gap: 0.125rem;
|
||||
|
||||
min-height: 5rem;
|
||||
max-height: 26rem;
|
||||
margin: 0 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 1rem;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.asFile {
|
||||
gap: 0;
|
||||
padding-block: 0.5rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments-bottom-padding {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.caption-wrapper {
|
||||
position: relative;
|
||||
padding: 0 0.5rem;
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
margin: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
padding-inline-start: 0.5rem;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
transition: border-color 250ms ease-in-out;
|
||||
|
||||
:global {
|
||||
@ -147,13 +158,14 @@
|
||||
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 0.5rem;
|
||||
left: 1rem;
|
||||
|
||||
overflow: visible !important;
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
height: calc(100% - 0.5rem);
|
||||
padding-top: 0.5rem;
|
||||
width: calc(100% - 2rem);
|
||||
height: calc(100% - 1.125rem);
|
||||
padding-top: 0.0625rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
opacity: 0;
|
||||
background-color: var(--color-background);
|
||||
@ -207,12 +219,20 @@
|
||||
|
||||
.send-wrapper {
|
||||
position: relative;
|
||||
align-self: flex-end;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.send {
|
||||
padding: 0 1rem !important;
|
||||
display: flex;
|
||||
|
||||
height: 2.625rem;
|
||||
padding: 0 1.0625rem !important;
|
||||
border-radius: 1.375rem;
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sendIcon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
:global {
|
||||
|
||||
@ -9,7 +9,6 @@ import type { Signal } from '../../../util/signals';
|
||||
import {
|
||||
BASE_EMOJI_KEYWORD_LANG,
|
||||
EDITABLE_INPUT_MODAL_ID,
|
||||
GIF_MIME_TYPE,
|
||||
SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
@ -39,7 +38,6 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
import useScrolledState from '../../../hooks/useScrolledState';
|
||||
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
|
||||
import useEmojiTooltip from './hooks/useEmojiTooltip';
|
||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||
@ -197,14 +195,6 @@ const AttachmentModal = ({
|
||||
);
|
||||
const [renderingShouldSendInHighQuality, setRenderingShouldSendInHighQuality] = useState(shouldSendInHighQuality);
|
||||
|
||||
const {
|
||||
handleScroll: handleAttachmentsScroll,
|
||||
isAtBeginning: areAttachmentsNotScrolled,
|
||||
isAtEnd: areAttachmentsScrolledToBottom,
|
||||
} = useScrolledState();
|
||||
|
||||
const { handleScroll: handleCaptionScroll, isAtBeginning: isCaptionNotScrolled } = useScrolledState();
|
||||
|
||||
const isOpen = Boolean(attachments.length);
|
||||
const renderingIsOpen = Boolean(renderingAttachments?.length);
|
||||
const [isHovered, markHovered, unmarkHovered] = useFlag();
|
||||
@ -520,14 +510,13 @@ const AttachmentModal = ({
|
||||
const isQuickGallery = isSendingCompressed && hasOnlyMedia;
|
||||
|
||||
const {
|
||||
areAllPhotos, areAllVideos, areAllAudios, areAllGifs, hasAnyPhoto,
|
||||
areAllPhotos, areAllVideos, areAllAudios, hasAnyPhoto,
|
||||
} = useMemo(() => {
|
||||
if (!isQuickGallery || !renderingAttachments) {
|
||||
return {
|
||||
areAllPhotos: false,
|
||||
areAllVideos: false,
|
||||
areAllAudios: false,
|
||||
areAllGifs: false,
|
||||
hasAnyPhoto: false,
|
||||
};
|
||||
}
|
||||
@ -535,7 +524,6 @@ const AttachmentModal = ({
|
||||
areAllPhotos: renderingAttachments.every((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)),
|
||||
areAllVideos: renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType)),
|
||||
areAllAudios: renderingAttachments.every((a) => SUPPORTED_AUDIO_CONTENT_TYPES.has(a.mimeType)),
|
||||
areAllGifs: renderingAttachments.every((a) => a.gif || a.mimeType === GIF_MIME_TYPE),
|
||||
hasAnyPhoto: renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)),
|
||||
};
|
||||
}, [renderingAttachments, isQuickGallery]);
|
||||
@ -575,10 +563,7 @@ const AttachmentModal = ({
|
||||
|
||||
let title = '';
|
||||
const attachmentsLength = renderingAttachments.length;
|
||||
|
||||
if (areAllGifs) {
|
||||
title = lang(isEditing ? 'AttachmentReplaceGif' : 'AttachmentSendGif');
|
||||
} else if (areAllPhotos) {
|
||||
if (areAllPhotos) {
|
||||
title = lang(
|
||||
`Attachment${isEditing ? 'Replace' : 'Send'}Photo`,
|
||||
{ count: attachmentsLength },
|
||||
@ -610,7 +595,7 @@ const AttachmentModal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className="modal-header-condensed-wide" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
@ -701,14 +686,13 @@ const AttachmentModal = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isBottomDividerShown = !areAttachmentsScrolledToBottom || !isCaptionNotScrolled;
|
||||
const buttonSendCaption = paidMessagesStars ? formatStarsAsIcon(
|
||||
const paidSendButtonCaption = paidMessagesStars ? formatStarsAsIcon(
|
||||
lang,
|
||||
attachmentsLength * paidMessagesStars,
|
||||
{
|
||||
asFont: true,
|
||||
},
|
||||
) : lang('Send');
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -717,7 +701,6 @@ const AttachmentModal = ({
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isHovered && styles.hovered,
|
||||
!areAttachmentsNotScrolled && styles.headerBorder,
|
||||
isMobile && styles.mobile,
|
||||
isSymbolMenuOpen && styles.symbolMenuOpen,
|
||||
forceDarkTheme && 'component-theme-dark',
|
||||
@ -744,9 +727,8 @@ const AttachmentModal = ({
|
||||
className={buildClassName(
|
||||
styles.attachments,
|
||||
'custom-scroll',
|
||||
isBottomDividerShown && styles.attachmentsBottomPadding,
|
||||
!isSendingCompressed && styles.asFile,
|
||||
)}
|
||||
onScroll={handleAttachmentsScroll}
|
||||
>
|
||||
{renderingAttachments.map((attachment, i) => (
|
||||
<AttachmentModalItem
|
||||
@ -765,7 +747,6 @@ const AttachmentModal = ({
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.captionWrapper,
|
||||
isBottomDividerShown && styles.captionTopBorder,
|
||||
)}
|
||||
>
|
||||
<MentionTooltip
|
||||
@ -823,7 +804,6 @@ const AttachmentModal = ({
|
||||
placeholder={lang('AttachmentCaptionPlaceholder')}
|
||||
onUpdate={onCaptionUpdate}
|
||||
onSend={handleSendClick}
|
||||
onScroll={handleCaptionScroll}
|
||||
canAutoFocus={Boolean(isReady && isForCurrentMessageList && attachments.length)}
|
||||
captionLimit={leftChars}
|
||||
shouldSuppressFocus={isMobile && isSymbolMenuOpen}
|
||||
@ -837,9 +817,11 @@ const AttachmentModal = ({
|
||||
inline
|
||||
onClick={handleSendClick}
|
||||
onContextMenu={canShowCustomSendMenu ? handleContextMenu : undefined}
|
||||
iconName={!editingMessage && !shouldSchedule && !paidMessagesStars ? 'new-send' : undefined}
|
||||
iconClassName={styles.sendIcon}
|
||||
>
|
||||
{shouldSchedule && !editingMessage ? lang('Next')
|
||||
: editingMessage ? lang('Save') : buttonSendCaption}
|
||||
: editingMessage ? lang('Save') : paidSendButtonCaption}
|
||||
</Button>
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
align-items: center;
|
||||
|
||||
min-width: 0;
|
||||
border-radius: var(--border-radius-default);
|
||||
}
|
||||
|
||||
.preview {
|
||||
@ -42,12 +41,22 @@
|
||||
|
||||
.no-grouping {
|
||||
flex-basis: 100%;
|
||||
margin-inline: 0.5rem;
|
||||
border-radius: 1rem;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-item-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 0;
|
||||
margin: 0.5rem;
|
||||
margin-block: 0.25rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@ -57,12 +66,23 @@
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
border-radius: var(--border-radius-default);
|
||||
padding-inline: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
font-size: 1.25rem;
|
||||
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
.root:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(body.no-menu-blur) & {
|
||||
background-color: #707579;
|
||||
backdrop-filter: none;
|
||||
@ -74,15 +94,15 @@
|
||||
|
||||
display: block;
|
||||
|
||||
padding: 0.3125rem;
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
padding: 0.375rem;
|
||||
border-radius: 1.125rem;
|
||||
|
||||
color: white;
|
||||
|
||||
transition: 0.2s background-color ease-in-out;
|
||||
transition: 0.2s background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@ -91,7 +111,16 @@
|
||||
}
|
||||
|
||||
.delete-file {
|
||||
margin-right: 1rem;
|
||||
margin-inline-end: 0.625rem;
|
||||
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.15s, background-color 0.15s;
|
||||
|
||||
.root:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { getFileExtension } from '../../common/helpers/documentInfo';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import File from '../../common/File';
|
||||
@ -42,6 +43,7 @@ const AttachmentModalItem = ({
|
||||
onToggleSpoiler,
|
||||
onEdit,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
const { isMobile } = useAppLayout();
|
||||
const displayType = getDisplayType(attachment, shouldDisplayCompressed);
|
||||
|
||||
@ -82,15 +84,16 @@ const AttachmentModalItem = ({
|
||||
);
|
||||
default: {
|
||||
const canEdit = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType) && !isMobile;
|
||||
const isPhoto = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType);
|
||||
return (
|
||||
<>
|
||||
<File
|
||||
className={styles.file}
|
||||
name={attachment.filename}
|
||||
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
||||
previewData={attachment.previewBlobUrl}
|
||||
previewData={isPhoto && attachment.blobUrl ? attachment.blobUrl : attachment.previewBlobUrl}
|
||||
size={attachment.size}
|
||||
smaller
|
||||
previewSize="large"
|
||||
onClick={canEdit ? handleEditClick : undefined}
|
||||
actionIcon={canEdit ? 'edit' : undefined}
|
||||
/>
|
||||
@ -117,7 +120,7 @@ const AttachmentModalItem = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<div className={rootClassName} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{content}
|
||||
<MediaSpoiler
|
||||
isVisible={shouldDisplaySpoiler}
|
||||
|
||||
@ -26,7 +26,7 @@ import Button from '../../../ui/Button';
|
||||
import InfiniteScroll from '../../../ui/InfiniteScroll';
|
||||
import Link from '../../../ui/Link';
|
||||
import Modal from '../../../ui/Modal';
|
||||
import TabList, { type TabWithProperties } from '../../../ui/TabList';
|
||||
import SquareTabList, { type TabWithProperties } from '../../../ui/SquareTabList';
|
||||
import Transition from '../../../ui/Transition';
|
||||
import GiftAttributeItem from '../GiftAttributeItem';
|
||||
import UniqueGiftHeader from '../UniqueGiftHeader';
|
||||
@ -474,7 +474,7 @@ const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => {
|
||||
onClose={handleClose}
|
||||
>
|
||||
{renderHeader()}
|
||||
<TabList
|
||||
<SquareTabList
|
||||
className={styles.tabs}
|
||||
activeTab={selectedTabIndex}
|
||||
tabs={tabs}
|
||||
|
||||
@ -26,6 +26,7 @@ const QuickChatPickerModal = ({
|
||||
return (
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
title={lang('SelectChat')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onClose={closeQuickChatPicker}
|
||||
|
||||
@ -149,6 +149,7 @@ const SharePreparedMessageModal: FC<OwnProps & StateProps> = ({
|
||||
<>
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
title={lang('SelectChat')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
filter={filter}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
|
||||
@ -31,7 +31,7 @@ import SafeLink from '../../common/SafeLink';
|
||||
import Button from '../../ui/Button';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import Modal from '../../ui/Modal';
|
||||
import TabList, { type TabWithProperties } from '../../ui/TabList';
|
||||
import SquareTabList, { type TabWithProperties } from '../../ui/SquareTabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import ParticlesHeader from '../common/ParticlesHeader.tsx';
|
||||
import BalanceBlock from './BalanceBlock';
|
||||
@ -426,7 +426,7 @@ const StarsBalanceModal = ({
|
||||
</InfiniteScroll>
|
||||
</Transition>
|
||||
</div>
|
||||
<TabList
|
||||
<SquareTabList
|
||||
ref={tabsRef}
|
||||
className={buildClassName(styles.tabs, areTabsPinned && styles.pinned)}
|
||||
tabClassName={styles.tab}
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
|
||||
@include mixins.with-vt-type('rightColumn');
|
||||
|
||||
.TabList {
|
||||
.SquareTabList {
|
||||
z-index: 1;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import Link from '../ui/Link';
|
||||
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import TabList, { type TabWithProperties } from '../ui/TabList';
|
||||
import SquareTabList, { type TabWithProperties } from '../ui/SquareTabList';
|
||||
import Transition from '../ui/Transition';
|
||||
import DeleteMemberModal from './DeleteMemberModal';
|
||||
import StarGiftCollectionList from './gifts/StarGiftCollectionList';
|
||||
@ -953,7 +953,7 @@ const Profile = ({
|
||||
id={`shared-media${getMessageHtmlId(id)}`}
|
||||
document={getMessageDocument(messagesById[id])!}
|
||||
datetime={messagesById[id].date}
|
||||
smaller
|
||||
fileSize="small"
|
||||
className="scroll-item"
|
||||
isDownloading={getIsDownloading(activeDownloads, getMessageDocument(messagesById[id])!)}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
@ -1264,7 +1264,7 @@ const Profile = ({
|
||||
>
|
||||
{renderContent()}
|
||||
</Transition>
|
||||
<TabList activeTab={activeTabIndex} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
||||
<SquareTabList activeTab={activeTabIndex} tabs={tabs} onSwitchTab={handleSwitchTab} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ export default function useProfileState({
|
||||
useEffectWithPrevDeps(([prevTabType]) => {
|
||||
if ((prevTabType && prevTabType !== tabType && allowAutoScrollToTabs) || (tabType && forceScrollProfileTab)) {
|
||||
const container = containerRef.current!;
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.TabList')!;
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList')!;
|
||||
handleStopAutoScrollToTabs();
|
||||
if (container.scrollTop < tabsEl.offsetTop) {
|
||||
onProfileStateChange(getStateFromTabType(tabType));
|
||||
@ -69,7 +69,7 @@ export default function useProfileState({
|
||||
return;
|
||||
}
|
||||
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.TabList');
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (!tabListEl || tabListEl.offsetTop > container.scrollTop) {
|
||||
return;
|
||||
}
|
||||
@ -94,7 +94,7 @@ export default function useProfileState({
|
||||
return;
|
||||
}
|
||||
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.TabList');
|
||||
const tabListEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (!tabListEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export default function useTransitionFixes(
|
||||
function setMinHeight() {
|
||||
const container = containerRef.current!;
|
||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.TabList');
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.SquareTabList');
|
||||
if (transitionEl && tabsEl) {
|
||||
const newHeight = container.clientHeight - tabsEl.offsetHeight;
|
||||
|
||||
|
||||
@ -9,8 +9,8 @@ import type { ApiChat, ApiChatMember } from '../../../api/types';
|
||||
import { filterPeersByQuery } from '../../../global/helpers/peers';
|
||||
import { selectChatFullInfo } from '../../../global/selectors';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||
|
||||
@ -37,7 +37,7 @@ const RemoveGroupUserModal: FC<OwnProps & StateProps> = ({
|
||||
deleteChatMember,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const usersId = useMemo(() => {
|
||||
@ -65,7 +65,8 @@ const RemoveGroupUserModal: FC<OwnProps & StateProps> = ({
|
||||
<ChatOrUserPicker
|
||||
isOpen={isOpen}
|
||||
chatOrUserIds={usersId}
|
||||
searchPlaceholder={lang('ChannelBlockUser')}
|
||||
title={lang('ChannelBlockUser')}
|
||||
searchPlaceholder={lang('Search')}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
loadMore={handleLoadMore}
|
||||
|
||||
@ -25,7 +25,7 @@ import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import TabList from '../../ui/TabList';
|
||||
import SquareTabList from '../../ui/SquareTabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import StatisticsOverview from './StatisticsOverview';
|
||||
|
||||
@ -366,7 +366,7 @@ const BoostStatistics = ({
|
||||
>
|
||||
{renderContent()}
|
||||
</Transition>
|
||||
<TabList activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
||||
<SquareTabList activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -145,11 +145,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header-condensed-wide,
|
||||
.modal-header-condensed {
|
||||
@extend %modal-header;
|
||||
|
||||
min-height: 3.5rem;
|
||||
padding: 0.375rem 0.75rem !important;
|
||||
|
||||
.modal-action-button {
|
||||
width: auto;
|
||||
@ -175,6 +175,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header-condensed {
|
||||
padding: 0.375rem 0.75rem !important;
|
||||
}
|
||||
|
||||
.modal-header-condensed-wide {
|
||||
padding: 0.375rem 1.375rem !important;
|
||||
.modal-title {
|
||||
&:not(:only-child) {
|
||||
margin: 0 1.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
@ -38,6 +38,7 @@ export type OwnProps = {
|
||||
hasCloseButton?: boolean;
|
||||
hasAbsoluteCloseButton?: boolean;
|
||||
absoluteCloseButtonColor?: ButtonProps['color'];
|
||||
isBackButton?: boolean;
|
||||
noBackdrop?: boolean;
|
||||
noBackdropClose?: boolean;
|
||||
children: React.ReactNode;
|
||||
@ -91,6 +92,7 @@ const Modal = (props: OwnProps) => {
|
||||
hasCloseButton,
|
||||
hasAbsoluteCloseButton,
|
||||
absoluteCloseButtonColor = 'translucent',
|
||||
isBackButton,
|
||||
noBackdrop,
|
||||
style,
|
||||
dialogStyle,
|
||||
@ -176,16 +178,22 @@ const Modal = (props: OwnProps) => {
|
||||
return header;
|
||||
}
|
||||
|
||||
const closeIconClassName = buildClassName(
|
||||
'animated-close-icon',
|
||||
isBackButton && 'state-back',
|
||||
);
|
||||
|
||||
const closeButton = withCloseButton ? (
|
||||
<Button
|
||||
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
|
||||
round
|
||||
color={absoluteCloseButtonColor}
|
||||
size="tiny"
|
||||
iconName="close"
|
||||
ariaLabel={lang('Close')}
|
||||
ariaLabel={isBackButton ? lang('Back') : lang('Close')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
>
|
||||
<div className={closeIconClassName} />
|
||||
</Button>
|
||||
) : undefined;
|
||||
|
||||
return title ? (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.TabList {
|
||||
.SquareTabList {
|
||||
scrollbar-color: rgba(0, 0, 0, 0);
|
||||
scrollbar-width: none;
|
||||
|
||||
111
src/components/ui/SquareTabList.tsx
Normal file
111
src/components/ui/SquareTabList.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
|
||||
import Tab from './Tab';
|
||||
|
||||
import './SquareTabList.scss';
|
||||
|
||||
export type TabWithProperties = {
|
||||
id?: number;
|
||||
title: TeactNode;
|
||||
badgeCount?: number;
|
||||
isBlocked?: boolean;
|
||||
isBadgeActive?: boolean;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
emoticon?: string | ApiMessageEntityCustomEmoji;
|
||||
noTitleAnimations?: boolean;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
tabs: readonly TabWithProperties[];
|
||||
activeTab: number;
|
||||
className?: string;
|
||||
tabClassName?: string;
|
||||
contextRootElementSelector?: string;
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
onSwitchTab: (index: number) => void;
|
||||
};
|
||||
|
||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||
// Should match duration from `--slide-transition` CSS variable
|
||||
const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
|
||||
|
||||
const SquareTabList = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
className,
|
||||
tabClassName,
|
||||
contextRootElementSelector,
|
||||
ref,
|
||||
onSwitchTab,
|
||||
}: OwnProps) => {
|
||||
const internalRef = useRef<HTMLDivElement>();
|
||||
const containerRef = ref || internalRef;
|
||||
const previousActiveTab = usePreviousDeprecated(activeTab);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHorizontalScroll(containerRef, undefined, true);
|
||||
|
||||
// Scroll container to place active tab in the center
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||
if (scrollWidth <= offsetWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTabElement = container.childNodes[activeTab] as HTMLElement | null;
|
||||
if (!activeTabElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { offsetLeft: activeTabOffsetLeft, offsetWidth: activeTabOffsetWidth } = activeTabElement;
|
||||
const newLeft = activeTabOffsetLeft - (offsetWidth / 2) + (activeTabOffsetWidth / 2);
|
||||
|
||||
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
|
||||
if (Math.abs(newLeft - scrollLeft) < TAB_SCROLL_THRESHOLD_PX) {
|
||||
return;
|
||||
}
|
||||
|
||||
animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
|
||||
}, [activeTab, containerRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('SquareTabList', 'no-scrollbar', className)}
|
||||
ref={containerRef}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
isActive={i === activeTab}
|
||||
isBlocked={tab.isBlocked}
|
||||
badgeCount={tab.badgeCount}
|
||||
isBadgeActive={tab.isBadgeActive}
|
||||
previousActiveTab={previousActiveTab}
|
||||
onClick={onSwitchTab}
|
||||
clickArg={i}
|
||||
contextActions={tab.contextActions}
|
||||
contextRootElementSelector={contextRootElementSelector}
|
||||
className={tabClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SquareTabList);
|
||||
85
src/components/ui/TabList.module.scss
Normal file
85
src/components/ui/TabList.module.scss
Normal file
@ -0,0 +1,85 @@
|
||||
.container,
|
||||
.activeIndicator {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
padding-block: 0.375rem;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
user-select: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
border-radius: 1.5rem;
|
||||
|
||||
opacity: 0;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
transition: opacity 150ms;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.ready {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.activeIndicator {
|
||||
will-change: clip-path;
|
||||
|
||||
isolation: isolate;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
contain: layout style paint;
|
||||
overflow: hidden;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
background-color: var(--color-primary-opacity);
|
||||
|
||||
transition: clip-path var(--slide-transition);
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.375rem 1rem;
|
||||
border-radius: 1.25rem;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.activeIndicator & {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.lockIcon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@ -1,111 +1,90 @@
|
||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef, useState } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
import type { TabWithProperties } from './SquareTabList';
|
||||
|
||||
export type { TabWithProperties };
|
||||
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||
|
||||
import Tab from './Tab';
|
||||
import Icon from '../common/icons/Icon';
|
||||
|
||||
import './TabList.scss';
|
||||
|
||||
export type TabWithProperties = {
|
||||
id?: number;
|
||||
title: TeactNode;
|
||||
badgeCount?: number;
|
||||
isBlocked?: boolean;
|
||||
isBadgeActive?: boolean;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
emoticon?: string | ApiMessageEntityCustomEmoji;
|
||||
noTitleAnimations?: boolean;
|
||||
};
|
||||
import styles from './TabList.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
tabs: readonly TabWithProperties[];
|
||||
activeTab: number;
|
||||
className?: string;
|
||||
tabClassName?: string;
|
||||
contextRootElementSelector?: string;
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
onSwitchTab: (index: number) => void;
|
||||
};
|
||||
|
||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||
// Should match duration from `--slide-transition` CSS variable
|
||||
const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
|
||||
|
||||
const TabList = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
className,
|
||||
tabClassName,
|
||||
contextRootElementSelector,
|
||||
ref,
|
||||
onSwitchTab,
|
||||
}: OwnProps) => {
|
||||
let containerRef = useRef<HTMLDivElement>();
|
||||
if (ref) {
|
||||
containerRef = ref;
|
||||
}
|
||||
const previousActiveTab = usePreviousDeprecated(activeTab);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const clipPathContainerRef = useRef<HTMLDivElement>();
|
||||
const [clipPath, setClipPath] = useState<string>('');
|
||||
|
||||
const lang = useLang();
|
||||
useHorizontalScroll(containerRef, !tabs.length, true);
|
||||
|
||||
useHorizontalScroll(containerRef, undefined, true);
|
||||
const updateClipPath = useLastCallback(() => {
|
||||
const clipPathContainer = clipPathContainerRef.current;
|
||||
const activeTabEl = activeTab >= 0 && clipPathContainer?.childNodes[activeTab] as HTMLElement | undefined;
|
||||
|
||||
if (clipPathContainer && activeTabEl && clipPathContainer.offsetWidth > 0) {
|
||||
const { offsetLeft, offsetWidth } = activeTabEl;
|
||||
const containerWidth = clipPathContainer.offsetWidth;
|
||||
const left = (offsetLeft / containerWidth * 100).toFixed(1);
|
||||
const right = ((containerWidth - (offsetLeft + offsetWidth)) / containerWidth * 100).toFixed(1);
|
||||
|
||||
setClipPath(`inset(0.25rem ${right}% 0.25rem ${left}% round 1.25rem)`);
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll container to place active tab in the center
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||
if (scrollWidth <= offsetWidth) {
|
||||
return;
|
||||
}
|
||||
updateClipPath();
|
||||
}, [activeTab, tabs]);
|
||||
|
||||
const activeTabElement = container.childNodes[activeTab] as HTMLElement | null;
|
||||
if (!activeTabElement) {
|
||||
return;
|
||||
}
|
||||
useResizeObserver(clipPathContainerRef, updateClipPath);
|
||||
|
||||
const { offsetLeft: activeTabOffsetLeft, offsetWidth: activeTabOffsetWidth } = activeTabElement;
|
||||
const newLeft = activeTabOffsetLeft - (offsetWidth / 2) + (activeTabOffsetWidth / 2);
|
||||
const handleTabClick = useLastCallback((index: number) => {
|
||||
onSwitchTab(index);
|
||||
});
|
||||
|
||||
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
|
||||
if (Math.abs(newLeft - scrollLeft) < TAB_SCROLL_THRESHOLD_PX) {
|
||||
return;
|
||||
}
|
||||
if (!tabs.length) return undefined;
|
||||
|
||||
animateHorizontalScroll(container, newLeft, SCROLL_DURATION);
|
||||
}, [activeTab]);
|
||||
const renderTab = (tab: TabWithProperties, index: number) => (
|
||||
<div
|
||||
key={tab.id ?? index}
|
||||
className={styles.tab}
|
||||
onClick={() => handleTabClick(index)}
|
||||
>
|
||||
{tab.title}
|
||||
{tab.isBlocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('TabList', 'no-scrollbar', className)}
|
||||
ref={containerRef}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.container, className, clipPath && styles.ready)}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
isActive={i === activeTab}
|
||||
isBlocked={tab.isBlocked}
|
||||
badgeCount={tab.badgeCount}
|
||||
isBadgeActive={tab.isBadgeActive}
|
||||
previousActiveTab={previousActiveTab}
|
||||
onClick={onSwitchTab}
|
||||
clickArg={i}
|
||||
contextActions={tab.contextActions}
|
||||
contextRootElementSelector={contextRootElementSelector}
|
||||
className={tabClassName}
|
||||
/>
|
||||
))}
|
||||
{tabs.map(renderTab)}
|
||||
|
||||
<div
|
||||
ref={clipPathContainerRef}
|
||||
className={styles.activeIndicator}
|
||||
style={clipPath ? `clip-path: ${clipPath}` : undefined}
|
||||
aria-hidden
|
||||
>
|
||||
{tabs.map(renderTab)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -31,7 +31,7 @@ import Icon from '../../common/icons/Icon';
|
||||
import Button from '../Button';
|
||||
import FloatingActionButton from '../FloatingActionButton';
|
||||
import Portal from '../Portal';
|
||||
import TabList from '../TabList';
|
||||
import SquareTabList from '../SquareTabList';
|
||||
import Transition from '../Transition';
|
||||
import CropOverlay from './CropOverlay';
|
||||
import CropPanel from './CropPanel';
|
||||
@ -807,7 +807,7 @@ const MediaEditor = ({
|
||||
>
|
||||
{renderPanelContent()}
|
||||
</Transition>
|
||||
<TabList
|
||||
<SquareTabList
|
||||
tabs={TABS}
|
||||
activeTab={activeTabIndex}
|
||||
onSwitchTab={handleTabSwitch}
|
||||
|
||||
@ -145,6 +145,7 @@ import {
|
||||
selectMessageReplyInfo,
|
||||
selectOutlyingListByMessageId,
|
||||
selectPeer,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectPeerStory,
|
||||
selectPinnedIds,
|
||||
selectPollFromMessage,
|
||||
@ -2607,6 +2608,143 @@ addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionRet
|
||||
actions.forwardMessages({ isSilent: true, scheduledAt, tabId });
|
||||
});
|
||||
|
||||
interface ForwardToChatOptions {
|
||||
global: GlobalState;
|
||||
fromChat: ApiChat;
|
||||
toChat: ApiChat;
|
||||
realMessages: ApiMessage[];
|
||||
serviceMessages: ApiMessage[];
|
||||
comment?: string;
|
||||
withMyScore?: boolean;
|
||||
noAuthors?: boolean;
|
||||
noCaptions?: boolean;
|
||||
isCurrentUserPremium: boolean;
|
||||
}
|
||||
|
||||
function forwardMessagesToChat({
|
||||
global,
|
||||
fromChat,
|
||||
toChat,
|
||||
realMessages,
|
||||
serviceMessages,
|
||||
comment,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
}: ForwardToChatOptions) {
|
||||
const sendAs = selectSendAs(global, toChat.id);
|
||||
const lastMessageId = selectChatLastMessageId(global, toChat.id);
|
||||
const messagePriceInStars = selectPeerPaidMessagesStars(global, toChat.id);
|
||||
|
||||
if (comment) {
|
||||
sendMessage(global, {
|
||||
chat: toChat,
|
||||
text: comment,
|
||||
sendAs,
|
||||
lastMessageId,
|
||||
messagePriceInStars,
|
||||
});
|
||||
}
|
||||
|
||||
if (realMessages.length) {
|
||||
const messageSlices = global.config?.maxForwardedCount
|
||||
? splitMessagesForForwarding(realMessages, global.config.maxForwardedCount)
|
||||
: [realMessages];
|
||||
|
||||
for (const slice of messageSlices) {
|
||||
const forwardParams: ForwardMessagesParams = {
|
||||
fromChat,
|
||||
toChat,
|
||||
toThreadId: MAIN_THREAD_ID,
|
||||
messages: slice,
|
||||
isSilent: true,
|
||||
sendAs,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
wasDrafted: false,
|
||||
lastMessageId,
|
||||
messagePriceInStars,
|
||||
};
|
||||
|
||||
callApi('forwardMessages', forwardParams);
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of serviceMessages) {
|
||||
const { text, entities } = message.content.text || {};
|
||||
const { sticker } = message.content;
|
||||
|
||||
sendMessage(global, {
|
||||
chat: toChat,
|
||||
text,
|
||||
entities,
|
||||
sticker,
|
||||
isSilent: true,
|
||||
sendAs,
|
||||
lastMessageId,
|
||||
messagePriceInStars,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addActionHandler('forwardToMultipleChats', (global, actions, payload): ActionReturnType => {
|
||||
const { toChatIds, comment, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const {
|
||||
fromChatId, messageIds, withMyScore, noAuthors, noCaptions,
|
||||
} = selectTabState(global, tabId).forwardMessages;
|
||||
|
||||
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
const messages = fromChatId && messageIds
|
||||
? messageIds
|
||||
.sort((a, b) => a - b)
|
||||
.map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
if (!fromChat || !messages?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m));
|
||||
const forwardableRealMessages = realMessages.filter((message) => selectCanForwardMessage(global, message));
|
||||
|
||||
if (!forwardableRealMessages.length && !serviceMessages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const toChatId of toChatIds) {
|
||||
const toChat = selectChat(global, toChatId);
|
||||
if (!toChat) continue;
|
||||
|
||||
forwardMessagesToChat({
|
||||
global,
|
||||
fromChat,
|
||||
toChat,
|
||||
realMessages: forwardableRealMessages,
|
||||
serviceMessages,
|
||||
comment,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
});
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
forwardMessages: {},
|
||||
isShareMessageModalShown: false,
|
||||
}, tabId);
|
||||
|
||||
actions.exitMessageSelectMode({ tabId });
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('forwardStory', (global, actions, payload): ActionReturnType => {
|
||||
const { toChatId, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
|
||||
@ -692,6 +692,20 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
export function selectCanCopyMessageLink<T extends GlobalState>(
|
||||
global: T, message: ApiMessage,
|
||||
) {
|
||||
const chat = selectChat(global, message.chatId);
|
||||
if (!chat || selectIsChatRestricted(global, message.chatId)) return false;
|
||||
|
||||
const isLocal = isMessageLocal(message);
|
||||
const isAction = isActionMessage(message);
|
||||
const isChannel = isChatChannel(chat);
|
||||
const isSuperGroup = isChatSuperGroup(chat);
|
||||
|
||||
return !isLocal && !isAction && (isChannel || isSuperGroup) && !chat.isMonoforum;
|
||||
}
|
||||
|
||||
export function selectCanDeleteMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
|
||||
@ -2023,6 +2023,10 @@ export interface ActionPayloads {
|
||||
} & WithTabId;
|
||||
exitForwardMode: WithTabId | undefined;
|
||||
changeRecipient: WithTabId | undefined;
|
||||
forwardToMultipleChats: {
|
||||
toChatIds: string[];
|
||||
comment?: string;
|
||||
} & WithTabId;
|
||||
forwardToSavedMessages: {
|
||||
scheduledAt?: number;
|
||||
} & WithTabId;
|
||||
|
||||
@ -3,7 +3,7 @@ import { getActions, getGlobal } from '../global';
|
||||
|
||||
import type { ApiMessageEntity, ApiMessageEntityCustomEmoji } from '../api/types';
|
||||
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
||||
import type { TabWithProperties } from '../components/ui/TabList';
|
||||
import type { TabWithProperties } from '../components/ui/SquareTabList';
|
||||
import { type ApiChatFolder, type ApiChatlistExportedInvite, ApiMessageEntityTypes } from '../api/types';
|
||||
import { SettingsScreens } from '../types';
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src: url("./icons.woff2?4ca2ae9f8c7763ea420459de680c8340") format("woff2"),
|
||||
url("./icons.woff?4ca2ae9f8c7763ea420459de680c8340") format("woff");
|
||||
src: url("./icons.woff2?711696a367cfec0ddfb1ac8f13f32988") format("woff2"),
|
||||
url("./icons.woff?711696a367cfec0ddfb1ac8f13f32988") format("woff");
|
||||
}
|
||||
|
||||
.icon-char::before {
|
||||
@ -444,564 +444,570 @@ url("./icons.woff?4ca2ae9f8c7763ea420459de680c8340") format("woff");
|
||||
.icon-next-link::before {
|
||||
content: "\f18d";
|
||||
}
|
||||
.icon-new-chat-filled::before {
|
||||
.icon-new-send::before {
|
||||
content: "\f18e";
|
||||
}
|
||||
.icon-my-notes::before {
|
||||
.icon-new-chat-filled::before {
|
||||
content: "\f18f";
|
||||
}
|
||||
.icon-muted::before {
|
||||
.icon-my-notes::before {
|
||||
content: "\f190";
|
||||
}
|
||||
.icon-mute::before {
|
||||
.icon-muted::before {
|
||||
content: "\f191";
|
||||
}
|
||||
.icon-move-caption-up::before {
|
||||
.icon-mute::before {
|
||||
content: "\f192";
|
||||
}
|
||||
.icon-move-caption-down::before {
|
||||
.icon-move-caption-up::before {
|
||||
content: "\f193";
|
||||
}
|
||||
.icon-more::before {
|
||||
.icon-move-caption-down::before {
|
||||
content: "\f194";
|
||||
}
|
||||
.icon-more-circle::before {
|
||||
.icon-more::before {
|
||||
content: "\f195";
|
||||
}
|
||||
.icon-monospace::before {
|
||||
.icon-more-circle::before {
|
||||
content: "\f196";
|
||||
}
|
||||
.icon-microphone::before {
|
||||
.icon-monospace::before {
|
||||
content: "\f197";
|
||||
}
|
||||
.icon-microphone-alt::before {
|
||||
.icon-microphone::before {
|
||||
content: "\f198";
|
||||
}
|
||||
.icon-message::before {
|
||||
.icon-microphone-alt::before {
|
||||
content: "\f199";
|
||||
}
|
||||
.icon-message-succeeded::before {
|
||||
.icon-message::before {
|
||||
content: "\f19a";
|
||||
}
|
||||
.icon-message-read::before {
|
||||
.icon-message-succeeded::before {
|
||||
content: "\f19b";
|
||||
}
|
||||
.icon-message-pending::before {
|
||||
.icon-message-read::before {
|
||||
content: "\f19c";
|
||||
}
|
||||
.icon-message-failed::before {
|
||||
.icon-message-pending::before {
|
||||
content: "\f19d";
|
||||
}
|
||||
.icon-menu::before {
|
||||
.icon-message-failed::before {
|
||||
content: "\f19e";
|
||||
}
|
||||
.icon-mention::before {
|
||||
.icon-menu::before {
|
||||
content: "\f19f";
|
||||
}
|
||||
.icon-loop::before {
|
||||
.icon-mention::before {
|
||||
content: "\f1a0";
|
||||
}
|
||||
.icon-logout::before {
|
||||
.icon-loop::before {
|
||||
content: "\f1a1";
|
||||
}
|
||||
.icon-lock::before {
|
||||
.icon-logout::before {
|
||||
content: "\f1a2";
|
||||
}
|
||||
.icon-lock-badge::before {
|
||||
.icon-lock::before {
|
||||
content: "\f1a3";
|
||||
}
|
||||
.icon-location::before {
|
||||
.icon-lock-badge::before {
|
||||
content: "\f1a4";
|
||||
}
|
||||
.icon-link::before {
|
||||
.icon-location::before {
|
||||
content: "\f1a5";
|
||||
}
|
||||
.icon-link-broken::before {
|
||||
.icon-link::before {
|
||||
content: "\f1a6";
|
||||
}
|
||||
.icon-link-badge::before {
|
||||
.icon-link-broken::before {
|
||||
content: "\f1a7";
|
||||
}
|
||||
.icon-large-play::before {
|
||||
.icon-link-badge::before {
|
||||
content: "\f1a8";
|
||||
}
|
||||
.icon-large-pause::before {
|
||||
.icon-large-play::before {
|
||||
content: "\f1a9";
|
||||
}
|
||||
.icon-language::before {
|
||||
.icon-large-pause::before {
|
||||
content: "\f1aa";
|
||||
}
|
||||
.icon-lamp::before {
|
||||
.icon-language::before {
|
||||
content: "\f1ab";
|
||||
}
|
||||
.icon-keyboard::before {
|
||||
.icon-lamp::before {
|
||||
content: "\f1ac";
|
||||
}
|
||||
.icon-key::before {
|
||||
.icon-keyboard::before {
|
||||
content: "\f1ad";
|
||||
}
|
||||
.icon-italic::before {
|
||||
.icon-key::before {
|
||||
content: "\f1ae";
|
||||
}
|
||||
.icon-install::before {
|
||||
.icon-italic::before {
|
||||
content: "\f1af";
|
||||
}
|
||||
.icon-info::before {
|
||||
.icon-install::before {
|
||||
content: "\f1b0";
|
||||
}
|
||||
.icon-info-filled::before {
|
||||
.icon-info::before {
|
||||
content: "\f1b1";
|
||||
}
|
||||
.icon-help::before {
|
||||
.icon-info-filled::before {
|
||||
content: "\f1b2";
|
||||
}
|
||||
.icon-heart::before {
|
||||
.icon-help::before {
|
||||
content: "\f1b3";
|
||||
}
|
||||
.icon-heart-outline::before {
|
||||
.icon-heart::before {
|
||||
content: "\f1b4";
|
||||
}
|
||||
.icon-hd-photo::before {
|
||||
.icon-heart-outline::before {
|
||||
content: "\f1b5";
|
||||
}
|
||||
.icon-hashtag::before {
|
||||
.icon-hd-photo::before {
|
||||
content: "\f1b6";
|
||||
}
|
||||
.icon-hand-stop::before {
|
||||
.icon-hashtag::before {
|
||||
content: "\f1b7";
|
||||
}
|
||||
.icon-hand-stop-filled::before {
|
||||
.icon-hand-stop::before {
|
||||
content: "\f1b8";
|
||||
}
|
||||
.icon-grouped::before {
|
||||
.icon-hand-stop-filled::before {
|
||||
content: "\f1b9";
|
||||
}
|
||||
.icon-grouped-disable::before {
|
||||
.icon-grouped::before {
|
||||
content: "\f1ba";
|
||||
}
|
||||
.icon-group::before {
|
||||
.icon-grouped-disable::before {
|
||||
content: "\f1bb";
|
||||
}
|
||||
.icon-group-filled::before {
|
||||
.icon-group::before {
|
||||
content: "\f1bc";
|
||||
}
|
||||
.icon-gift::before {
|
||||
.icon-group-filled::before {
|
||||
content: "\f1bd";
|
||||
}
|
||||
.icon-gift-transfer-inline::before {
|
||||
.icon-gift::before {
|
||||
content: "\f1be";
|
||||
}
|
||||
.icon-gifs::before {
|
||||
.icon-gift-transfer-inline::before {
|
||||
content: "\f1bf";
|
||||
}
|
||||
.icon-fullscreen::before {
|
||||
.icon-gifs::before {
|
||||
content: "\f1c0";
|
||||
}
|
||||
.icon-frozen-time::before {
|
||||
.icon-fullscreen::before {
|
||||
content: "\f1c1";
|
||||
}
|
||||
.icon-fragment::before {
|
||||
.icon-frozen-time::before {
|
||||
content: "\f1c2";
|
||||
}
|
||||
.icon-forward::before {
|
||||
.icon-fragment::before {
|
||||
content: "\f1c3";
|
||||
}
|
||||
.icon-forums::before {
|
||||
.icon-forward::before {
|
||||
content: "\f1c4";
|
||||
}
|
||||
.icon-fontsize::before {
|
||||
.icon-forums::before {
|
||||
content: "\f1c5";
|
||||
}
|
||||
.icon-folder::before {
|
||||
.icon-fontsize::before {
|
||||
content: "\f1c6";
|
||||
}
|
||||
.icon-folder-badge::before {
|
||||
.icon-folder::before {
|
||||
content: "\f1c7";
|
||||
}
|
||||
.icon-flip::before {
|
||||
.icon-folder-badge::before {
|
||||
content: "\f1c8";
|
||||
}
|
||||
.icon-flag::before {
|
||||
.icon-flip::before {
|
||||
content: "\f1c9";
|
||||
}
|
||||
.icon-file-badge::before {
|
||||
.icon-flag::before {
|
||||
content: "\f1ca";
|
||||
}
|
||||
.icon-favorite::before {
|
||||
.icon-file-badge::before {
|
||||
content: "\f1cb";
|
||||
}
|
||||
.icon-favorite-filled::before {
|
||||
.icon-favorite::before {
|
||||
content: "\f1cc";
|
||||
}
|
||||
.icon-eye::before {
|
||||
.icon-favorite-filled::before {
|
||||
content: "\f1cd";
|
||||
}
|
||||
.icon-eye-outline::before {
|
||||
.icon-eye::before {
|
||||
content: "\f1ce";
|
||||
}
|
||||
.icon-eye-crossed::before {
|
||||
.icon-eye-outline::before {
|
||||
content: "\f1cf";
|
||||
}
|
||||
.icon-eye-crossed-outline::before {
|
||||
.icon-eye-crossed::before {
|
||||
content: "\f1d0";
|
||||
}
|
||||
.icon-expand::before {
|
||||
.icon-eye-crossed-outline::before {
|
||||
content: "\f1d1";
|
||||
}
|
||||
.icon-expand-modal::before {
|
||||
.icon-expand::before {
|
||||
content: "\f1d2";
|
||||
}
|
||||
.icon-enter::before {
|
||||
.icon-expand-modal::before {
|
||||
content: "\f1d3";
|
||||
}
|
||||
.icon-email::before {
|
||||
.icon-enter::before {
|
||||
content: "\f1d4";
|
||||
}
|
||||
.icon-edit::before {
|
||||
.icon-email::before {
|
||||
content: "\f1d5";
|
||||
}
|
||||
.icon-eats::before {
|
||||
.icon-edit::before {
|
||||
content: "\f1d6";
|
||||
}
|
||||
.icon-dropdown-arrows::before {
|
||||
.icon-eats::before {
|
||||
content: "\f1d7";
|
||||
}
|
||||
.icon-download::before {
|
||||
.icon-dropdown-arrows::before {
|
||||
content: "\f1d8";
|
||||
}
|
||||
.icon-down::before {
|
||||
.icon-download::before {
|
||||
content: "\f1d9";
|
||||
}
|
||||
.icon-double-badge::before {
|
||||
.icon-down::before {
|
||||
content: "\f1da";
|
||||
}
|
||||
.icon-document::before {
|
||||
.icon-double-badge::before {
|
||||
content: "\f1db";
|
||||
}
|
||||
.icon-diamond::before {
|
||||
.icon-document::before {
|
||||
content: "\f1dc";
|
||||
}
|
||||
.icon-delete::before {
|
||||
.icon-diamond::before {
|
||||
content: "\f1dd";
|
||||
}
|
||||
.icon-delete-user::before {
|
||||
.icon-delete::before {
|
||||
content: "\f1de";
|
||||
}
|
||||
.icon-delete-left::before {
|
||||
.icon-delete-user::before {
|
||||
content: "\f1df";
|
||||
}
|
||||
.icon-delete-filled::before {
|
||||
.icon-delete-left::before {
|
||||
content: "\f1e0";
|
||||
}
|
||||
.icon-data::before {
|
||||
.icon-delete-filled::before {
|
||||
content: "\f1e1";
|
||||
}
|
||||
.icon-darkmode::before {
|
||||
.icon-data::before {
|
||||
content: "\f1e2";
|
||||
}
|
||||
.icon-crown-wear::before {
|
||||
.icon-darkmode::before {
|
||||
content: "\f1e3";
|
||||
}
|
||||
.icon-crown-wear-outline::before {
|
||||
.icon-crown-wear::before {
|
||||
content: "\f1e4";
|
||||
}
|
||||
.icon-crown-take-off::before {
|
||||
.icon-crown-wear-outline::before {
|
||||
content: "\f1e5";
|
||||
}
|
||||
.icon-crown-take-off-outline::before {
|
||||
.icon-crown-take-off::before {
|
||||
content: "\f1e6";
|
||||
}
|
||||
.icon-crop::before {
|
||||
.icon-crown-take-off-outline::before {
|
||||
content: "\f1e7";
|
||||
}
|
||||
.icon-craft::before {
|
||||
.icon-crop::before {
|
||||
content: "\f1e8";
|
||||
}
|
||||
.icon-copy::before {
|
||||
.icon-craft::before {
|
||||
content: "\f1e9";
|
||||
}
|
||||
.icon-copy-media::before {
|
||||
.icon-copy::before {
|
||||
content: "\f1ea";
|
||||
}
|
||||
.icon-comments::before {
|
||||
.icon-copy-media::before {
|
||||
content: "\f1eb";
|
||||
}
|
||||
.icon-comments-sticker::before {
|
||||
.icon-comments::before {
|
||||
content: "\f1ec";
|
||||
}
|
||||
.icon-combine-craft::before {
|
||||
.icon-comments-sticker::before {
|
||||
content: "\f1ed";
|
||||
}
|
||||
.icon-colorize::before {
|
||||
.icon-combine-craft::before {
|
||||
content: "\f1ee";
|
||||
}
|
||||
.icon-collapse::before {
|
||||
.icon-colorize::before {
|
||||
content: "\f1ef";
|
||||
}
|
||||
.icon-collapse-modal::before {
|
||||
.icon-collapse::before {
|
||||
content: "\f1f0";
|
||||
}
|
||||
.icon-cloud-download::before {
|
||||
.icon-collapse-modal::before {
|
||||
content: "\f1f1";
|
||||
}
|
||||
.icon-closed-gift::before {
|
||||
.icon-cloud-download::before {
|
||||
content: "\f1f2";
|
||||
}
|
||||
.icon-close::before {
|
||||
.icon-closed-gift::before {
|
||||
content: "\f1f3";
|
||||
}
|
||||
.icon-close-topic::before {
|
||||
.icon-close::before {
|
||||
content: "\f1f4";
|
||||
}
|
||||
.icon-close-circle::before {
|
||||
.icon-close-topic::before {
|
||||
content: "\f1f5";
|
||||
}
|
||||
.icon-clock::before {
|
||||
.icon-close-circle::before {
|
||||
content: "\f1f6";
|
||||
}
|
||||
.icon-clock-edit::before {
|
||||
.icon-clock::before {
|
||||
content: "\f1f7";
|
||||
}
|
||||
.icon-check::before {
|
||||
.icon-clock-edit::before {
|
||||
content: "\f1f8";
|
||||
}
|
||||
.icon-chats-badge::before {
|
||||
.icon-check::before {
|
||||
content: "\f1f9";
|
||||
}
|
||||
.icon-chat-badge::before {
|
||||
.icon-check-bold::before {
|
||||
content: "\f1fa";
|
||||
}
|
||||
.icon-channelviews::before {
|
||||
.icon-chats-badge::before {
|
||||
content: "\f1fb";
|
||||
}
|
||||
.icon-channel::before {
|
||||
.icon-chat-badge::before {
|
||||
content: "\f1fc";
|
||||
}
|
||||
.icon-channel-filled::before {
|
||||
.icon-channelviews::before {
|
||||
content: "\f1fd";
|
||||
}
|
||||
.icon-cash-circle::before {
|
||||
.icon-channel::before {
|
||||
content: "\f1fe";
|
||||
}
|
||||
.icon-card::before {
|
||||
.icon-channel-filled::before {
|
||||
content: "\f1ff";
|
||||
}
|
||||
.icon-car::before {
|
||||
.icon-cash-circle::before {
|
||||
content: "\f200";
|
||||
}
|
||||
.icon-camera::before {
|
||||
.icon-card::before {
|
||||
content: "\f201";
|
||||
}
|
||||
.icon-camera-add::before {
|
||||
.icon-car::before {
|
||||
content: "\f202";
|
||||
}
|
||||
.icon-calendar::before {
|
||||
.icon-camera::before {
|
||||
content: "\f203";
|
||||
}
|
||||
.icon-calendar-filter::before {
|
||||
.icon-camera-add::before {
|
||||
content: "\f204";
|
||||
}
|
||||
.icon-bug::before {
|
||||
.icon-calendar::before {
|
||||
content: "\f205";
|
||||
}
|
||||
.icon-brush::before {
|
||||
.icon-calendar-filter::before {
|
||||
content: "\f206";
|
||||
}
|
||||
.icon-bots::before {
|
||||
.icon-bug::before {
|
||||
content: "\f207";
|
||||
}
|
||||
.icon-bot-commands-filled::before {
|
||||
.icon-brush::before {
|
||||
content: "\f208";
|
||||
}
|
||||
.icon-bot-command::before {
|
||||
.icon-bots::before {
|
||||
content: "\f209";
|
||||
}
|
||||
.icon-boosts::before {
|
||||
.icon-bot-commands-filled::before {
|
||||
content: "\f20a";
|
||||
}
|
||||
.icon-boostcircle::before {
|
||||
.icon-bot-command::before {
|
||||
content: "\f20b";
|
||||
}
|
||||
.icon-boost::before {
|
||||
.icon-boosts::before {
|
||||
content: "\f20c";
|
||||
}
|
||||
.icon-boost-outline::before {
|
||||
.icon-boostcircle::before {
|
||||
content: "\f20d";
|
||||
}
|
||||
.icon-boost-craft-chance::before {
|
||||
.icon-boost::before {
|
||||
content: "\f20e";
|
||||
}
|
||||
.icon-bold::before {
|
||||
.icon-boost-outline::before {
|
||||
content: "\f20f";
|
||||
}
|
||||
.icon-avatar-saved-messages::before {
|
||||
.icon-boost-craft-chance::before {
|
||||
content: "\f210";
|
||||
}
|
||||
.icon-avatar-deleted-account::before {
|
||||
.icon-bold::before {
|
||||
content: "\f211";
|
||||
}
|
||||
.icon-avatar-archived-chats::before {
|
||||
.icon-avatar-saved-messages::before {
|
||||
content: "\f212";
|
||||
}
|
||||
.icon-author-hidden::before {
|
||||
.icon-avatar-deleted-account::before {
|
||||
content: "\f213";
|
||||
}
|
||||
.icon-auction::before {
|
||||
.icon-avatar-archived-chats::before {
|
||||
content: "\f214";
|
||||
}
|
||||
.icon-auction-next-round::before {
|
||||
.icon-author-hidden::before {
|
||||
content: "\f215";
|
||||
}
|
||||
.icon-auction-filled::before {
|
||||
.icon-auction::before {
|
||||
content: "\f216";
|
||||
}
|
||||
.icon-auction-drop::before {
|
||||
.icon-auction-next-round::before {
|
||||
content: "\f217";
|
||||
}
|
||||
.icon-attach::before {
|
||||
.icon-auction-filled::before {
|
||||
content: "\f218";
|
||||
}
|
||||
.icon-ask-support::before {
|
||||
.icon-auction-drop::before {
|
||||
content: "\f219";
|
||||
}
|
||||
.icon-arrow-right::before {
|
||||
.icon-attach::before {
|
||||
content: "\f21a";
|
||||
}
|
||||
.icon-arrow-left::before {
|
||||
.icon-ask-support::before {
|
||||
content: "\f21b";
|
||||
}
|
||||
.icon-arrow-down::before {
|
||||
.icon-arrow-right::before {
|
||||
content: "\f21c";
|
||||
}
|
||||
.icon-arrow-down-circle::before {
|
||||
.icon-arrow-left::before {
|
||||
content: "\f21d";
|
||||
}
|
||||
.icon-archive::before {
|
||||
.icon-arrow-down::before {
|
||||
content: "\f21e";
|
||||
}
|
||||
.icon-archive-to-main::before {
|
||||
.icon-arrow-down-circle::before {
|
||||
content: "\f21f";
|
||||
}
|
||||
.icon-archive-from-main::before {
|
||||
.icon-archive::before {
|
||||
content: "\f220";
|
||||
}
|
||||
.icon-archive-filled::before {
|
||||
.icon-archive-to-main::before {
|
||||
content: "\f221";
|
||||
}
|
||||
.icon-animations::before {
|
||||
.icon-archive-from-main::before {
|
||||
content: "\f222";
|
||||
}
|
||||
.icon-animals::before {
|
||||
.icon-archive-filled::before {
|
||||
content: "\f223";
|
||||
}
|
||||
.icon-allow-speak::before {
|
||||
.icon-animations::before {
|
||||
content: "\f224";
|
||||
}
|
||||
.icon-allow-share::before {
|
||||
.icon-animals::before {
|
||||
content: "\f225";
|
||||
}
|
||||
.icon-admin::before {
|
||||
.icon-allow-speak::before {
|
||||
content: "\f226";
|
||||
}
|
||||
.icon-add::before {
|
||||
.icon-allow-share::before {
|
||||
content: "\f227";
|
||||
}
|
||||
.icon-add-user::before {
|
||||
.icon-admin::before {
|
||||
content: "\f228";
|
||||
}
|
||||
.icon-add-user-filled::before {
|
||||
.icon-add::before {
|
||||
content: "\f229";
|
||||
}
|
||||
.icon-add-one-badge::before {
|
||||
.icon-add-user::before {
|
||||
content: "\f22a";
|
||||
}
|
||||
.icon-add-filled::before {
|
||||
.icon-add-user-filled::before {
|
||||
content: "\f22b";
|
||||
}
|
||||
.icon-add-caption::before {
|
||||
.icon-add-one-badge::before {
|
||||
content: "\f22c";
|
||||
}
|
||||
.icon-active-sessions::before {
|
||||
.icon-add-filled::before {
|
||||
content: "\f22d";
|
||||
}
|
||||
.icon-rating-icons-negative::before {
|
||||
.icon-add-caption::before {
|
||||
content: "\f22e";
|
||||
}
|
||||
.icon-rating-icons-level90::before {
|
||||
.icon-active-sessions::before {
|
||||
content: "\f22f";
|
||||
}
|
||||
.icon-rating-icons-level9::before {
|
||||
.icon-rating-icons-negative::before {
|
||||
content: "\f230";
|
||||
}
|
||||
.icon-rating-icons-level80::before {
|
||||
.icon-rating-icons-level90::before {
|
||||
content: "\f231";
|
||||
}
|
||||
.icon-rating-icons-level8::before {
|
||||
.icon-rating-icons-level9::before {
|
||||
content: "\f232";
|
||||
}
|
||||
.icon-rating-icons-level70::before {
|
||||
.icon-rating-icons-level80::before {
|
||||
content: "\f233";
|
||||
}
|
||||
.icon-rating-icons-level7::before {
|
||||
.icon-rating-icons-level8::before {
|
||||
content: "\f234";
|
||||
}
|
||||
.icon-rating-icons-level60::before {
|
||||
.icon-rating-icons-level70::before {
|
||||
content: "\f235";
|
||||
}
|
||||
.icon-rating-icons-level6::before {
|
||||
.icon-rating-icons-level7::before {
|
||||
content: "\f236";
|
||||
}
|
||||
.icon-rating-icons-level50::before {
|
||||
.icon-rating-icons-level60::before {
|
||||
content: "\f237";
|
||||
}
|
||||
.icon-rating-icons-level5::before {
|
||||
.icon-rating-icons-level6::before {
|
||||
content: "\f238";
|
||||
}
|
||||
.icon-rating-icons-level40::before {
|
||||
.icon-rating-icons-level50::before {
|
||||
content: "\f239";
|
||||
}
|
||||
.icon-rating-icons-level4::before {
|
||||
.icon-rating-icons-level5::before {
|
||||
content: "\f23a";
|
||||
}
|
||||
.icon-rating-icons-level30::before {
|
||||
.icon-rating-icons-level40::before {
|
||||
content: "\f23b";
|
||||
}
|
||||
.icon-rating-icons-level3::before {
|
||||
.icon-rating-icons-level4::before {
|
||||
content: "\f23c";
|
||||
}
|
||||
.icon-rating-icons-level20::before {
|
||||
.icon-rating-icons-level30::before {
|
||||
content: "\f23d";
|
||||
}
|
||||
.icon-rating-icons-level2::before {
|
||||
.icon-rating-icons-level3::before {
|
||||
content: "\f23e";
|
||||
}
|
||||
.icon-rating-icons-level10::before {
|
||||
.icon-rating-icons-level20::before {
|
||||
content: "\f23f";
|
||||
}
|
||||
.icon-rating-icons-level1::before {
|
||||
.icon-rating-icons-level2::before {
|
||||
content: "\f240";
|
||||
}
|
||||
.icon-folder-tabs-user::before {
|
||||
.icon-rating-icons-level10::before {
|
||||
content: "\f241";
|
||||
}
|
||||
.icon-folder-tabs-star::before {
|
||||
.icon-rating-icons-level1::before {
|
||||
content: "\f242";
|
||||
}
|
||||
.icon-folder-tabs-group::before {
|
||||
.icon-folder-tabs-user::before {
|
||||
content: "\f243";
|
||||
}
|
||||
.icon-folder-tabs-folder::before {
|
||||
.icon-folder-tabs-star::before {
|
||||
content: "\f244";
|
||||
}
|
||||
.icon-folder-tabs-chats::before {
|
||||
.icon-folder-tabs-group::before {
|
||||
content: "\f245";
|
||||
}
|
||||
.icon-folder-tabs-chat::before {
|
||||
.icon-folder-tabs-folder::before {
|
||||
content: "\f246";
|
||||
}
|
||||
.icon-folder-tabs-channel::before {
|
||||
.icon-folder-tabs-chats::before {
|
||||
content: "\f247";
|
||||
}
|
||||
.icon-folder-tabs-bot::before {
|
||||
.icon-folder-tabs-chat::before {
|
||||
content: "\f248";
|
||||
}
|
||||
.icon-folder-tabs-channel::before {
|
||||
content: "\f249";
|
||||
}
|
||||
.icon-folder-tabs-bot::before {
|
||||
content: "\f24a";
|
||||
}
|
||||
|
||||
@ -157,191 +157,193 @@ $icons-map: (
|
||||
"no-download": "\f18b",
|
||||
"next": "\f18c",
|
||||
"next-link": "\f18d",
|
||||
"new-chat-filled": "\f18e",
|
||||
"my-notes": "\f18f",
|
||||
"muted": "\f190",
|
||||
"mute": "\f191",
|
||||
"move-caption-up": "\f192",
|
||||
"move-caption-down": "\f193",
|
||||
"more": "\f194",
|
||||
"more-circle": "\f195",
|
||||
"monospace": "\f196",
|
||||
"microphone": "\f197",
|
||||
"microphone-alt": "\f198",
|
||||
"message": "\f199",
|
||||
"message-succeeded": "\f19a",
|
||||
"message-read": "\f19b",
|
||||
"message-pending": "\f19c",
|
||||
"message-failed": "\f19d",
|
||||
"menu": "\f19e",
|
||||
"mention": "\f19f",
|
||||
"loop": "\f1a0",
|
||||
"logout": "\f1a1",
|
||||
"lock": "\f1a2",
|
||||
"lock-badge": "\f1a3",
|
||||
"location": "\f1a4",
|
||||
"link": "\f1a5",
|
||||
"link-broken": "\f1a6",
|
||||
"link-badge": "\f1a7",
|
||||
"large-play": "\f1a8",
|
||||
"large-pause": "\f1a9",
|
||||
"language": "\f1aa",
|
||||
"lamp": "\f1ab",
|
||||
"keyboard": "\f1ac",
|
||||
"key": "\f1ad",
|
||||
"italic": "\f1ae",
|
||||
"install": "\f1af",
|
||||
"info": "\f1b0",
|
||||
"info-filled": "\f1b1",
|
||||
"help": "\f1b2",
|
||||
"heart": "\f1b3",
|
||||
"heart-outline": "\f1b4",
|
||||
"hd-photo": "\f1b5",
|
||||
"hashtag": "\f1b6",
|
||||
"hand-stop": "\f1b7",
|
||||
"hand-stop-filled": "\f1b8",
|
||||
"grouped": "\f1b9",
|
||||
"grouped-disable": "\f1ba",
|
||||
"group": "\f1bb",
|
||||
"group-filled": "\f1bc",
|
||||
"gift": "\f1bd",
|
||||
"gift-transfer-inline": "\f1be",
|
||||
"gifs": "\f1bf",
|
||||
"fullscreen": "\f1c0",
|
||||
"frozen-time": "\f1c1",
|
||||
"fragment": "\f1c2",
|
||||
"forward": "\f1c3",
|
||||
"forums": "\f1c4",
|
||||
"fontsize": "\f1c5",
|
||||
"folder": "\f1c6",
|
||||
"folder-badge": "\f1c7",
|
||||
"flip": "\f1c8",
|
||||
"flag": "\f1c9",
|
||||
"file-badge": "\f1ca",
|
||||
"favorite": "\f1cb",
|
||||
"favorite-filled": "\f1cc",
|
||||
"eye": "\f1cd",
|
||||
"eye-outline": "\f1ce",
|
||||
"eye-crossed": "\f1cf",
|
||||
"eye-crossed-outline": "\f1d0",
|
||||
"expand": "\f1d1",
|
||||
"expand-modal": "\f1d2",
|
||||
"enter": "\f1d3",
|
||||
"email": "\f1d4",
|
||||
"edit": "\f1d5",
|
||||
"eats": "\f1d6",
|
||||
"dropdown-arrows": "\f1d7",
|
||||
"download": "\f1d8",
|
||||
"down": "\f1d9",
|
||||
"double-badge": "\f1da",
|
||||
"document": "\f1db",
|
||||
"diamond": "\f1dc",
|
||||
"delete": "\f1dd",
|
||||
"delete-user": "\f1de",
|
||||
"delete-left": "\f1df",
|
||||
"delete-filled": "\f1e0",
|
||||
"data": "\f1e1",
|
||||
"darkmode": "\f1e2",
|
||||
"crown-wear": "\f1e3",
|
||||
"crown-wear-outline": "\f1e4",
|
||||
"crown-take-off": "\f1e5",
|
||||
"crown-take-off-outline": "\f1e6",
|
||||
"crop": "\f1e7",
|
||||
"craft": "\f1e8",
|
||||
"copy": "\f1e9",
|
||||
"copy-media": "\f1ea",
|
||||
"comments": "\f1eb",
|
||||
"comments-sticker": "\f1ec",
|
||||
"combine-craft": "\f1ed",
|
||||
"colorize": "\f1ee",
|
||||
"collapse": "\f1ef",
|
||||
"collapse-modal": "\f1f0",
|
||||
"cloud-download": "\f1f1",
|
||||
"closed-gift": "\f1f2",
|
||||
"close": "\f1f3",
|
||||
"close-topic": "\f1f4",
|
||||
"close-circle": "\f1f5",
|
||||
"clock": "\f1f6",
|
||||
"clock-edit": "\f1f7",
|
||||
"check": "\f1f8",
|
||||
"chats-badge": "\f1f9",
|
||||
"chat-badge": "\f1fa",
|
||||
"channelviews": "\f1fb",
|
||||
"channel": "\f1fc",
|
||||
"channel-filled": "\f1fd",
|
||||
"cash-circle": "\f1fe",
|
||||
"card": "\f1ff",
|
||||
"car": "\f200",
|
||||
"camera": "\f201",
|
||||
"camera-add": "\f202",
|
||||
"calendar": "\f203",
|
||||
"calendar-filter": "\f204",
|
||||
"bug": "\f205",
|
||||
"brush": "\f206",
|
||||
"bots": "\f207",
|
||||
"bot-commands-filled": "\f208",
|
||||
"bot-command": "\f209",
|
||||
"boosts": "\f20a",
|
||||
"boostcircle": "\f20b",
|
||||
"boost": "\f20c",
|
||||
"boost-outline": "\f20d",
|
||||
"boost-craft-chance": "\f20e",
|
||||
"bold": "\f20f",
|
||||
"avatar-saved-messages": "\f210",
|
||||
"avatar-deleted-account": "\f211",
|
||||
"avatar-archived-chats": "\f212",
|
||||
"author-hidden": "\f213",
|
||||
"auction": "\f214",
|
||||
"auction-next-round": "\f215",
|
||||
"auction-filled": "\f216",
|
||||
"auction-drop": "\f217",
|
||||
"attach": "\f218",
|
||||
"ask-support": "\f219",
|
||||
"arrow-right": "\f21a",
|
||||
"arrow-left": "\f21b",
|
||||
"arrow-down": "\f21c",
|
||||
"arrow-down-circle": "\f21d",
|
||||
"archive": "\f21e",
|
||||
"archive-to-main": "\f21f",
|
||||
"archive-from-main": "\f220",
|
||||
"archive-filled": "\f221",
|
||||
"animations": "\f222",
|
||||
"animals": "\f223",
|
||||
"allow-speak": "\f224",
|
||||
"allow-share": "\f225",
|
||||
"admin": "\f226",
|
||||
"add": "\f227",
|
||||
"add-user": "\f228",
|
||||
"add-user-filled": "\f229",
|
||||
"add-one-badge": "\f22a",
|
||||
"add-filled": "\f22b",
|
||||
"add-caption": "\f22c",
|
||||
"active-sessions": "\f22d",
|
||||
"rating-icons-negative": "\f22e",
|
||||
"rating-icons-level90": "\f22f",
|
||||
"rating-icons-level9": "\f230",
|
||||
"rating-icons-level80": "\f231",
|
||||
"rating-icons-level8": "\f232",
|
||||
"rating-icons-level70": "\f233",
|
||||
"rating-icons-level7": "\f234",
|
||||
"rating-icons-level60": "\f235",
|
||||
"rating-icons-level6": "\f236",
|
||||
"rating-icons-level50": "\f237",
|
||||
"rating-icons-level5": "\f238",
|
||||
"rating-icons-level40": "\f239",
|
||||
"rating-icons-level4": "\f23a",
|
||||
"rating-icons-level30": "\f23b",
|
||||
"rating-icons-level3": "\f23c",
|
||||
"rating-icons-level20": "\f23d",
|
||||
"rating-icons-level2": "\f23e",
|
||||
"rating-icons-level10": "\f23f",
|
||||
"rating-icons-level1": "\f240",
|
||||
"folder-tabs-user": "\f241",
|
||||
"folder-tabs-star": "\f242",
|
||||
"folder-tabs-group": "\f243",
|
||||
"folder-tabs-folder": "\f244",
|
||||
"folder-tabs-chats": "\f245",
|
||||
"folder-tabs-chat": "\f246",
|
||||
"folder-tabs-channel": "\f247",
|
||||
"folder-tabs-bot": "\f248",
|
||||
"new-send": "\f18e",
|
||||
"new-chat-filled": "\f18f",
|
||||
"my-notes": "\f190",
|
||||
"muted": "\f191",
|
||||
"mute": "\f192",
|
||||
"move-caption-up": "\f193",
|
||||
"move-caption-down": "\f194",
|
||||
"more": "\f195",
|
||||
"more-circle": "\f196",
|
||||
"monospace": "\f197",
|
||||
"microphone": "\f198",
|
||||
"microphone-alt": "\f199",
|
||||
"message": "\f19a",
|
||||
"message-succeeded": "\f19b",
|
||||
"message-read": "\f19c",
|
||||
"message-pending": "\f19d",
|
||||
"message-failed": "\f19e",
|
||||
"menu": "\f19f",
|
||||
"mention": "\f1a0",
|
||||
"loop": "\f1a1",
|
||||
"logout": "\f1a2",
|
||||
"lock": "\f1a3",
|
||||
"lock-badge": "\f1a4",
|
||||
"location": "\f1a5",
|
||||
"link": "\f1a6",
|
||||
"link-broken": "\f1a7",
|
||||
"link-badge": "\f1a8",
|
||||
"large-play": "\f1a9",
|
||||
"large-pause": "\f1aa",
|
||||
"language": "\f1ab",
|
||||
"lamp": "\f1ac",
|
||||
"keyboard": "\f1ad",
|
||||
"key": "\f1ae",
|
||||
"italic": "\f1af",
|
||||
"install": "\f1b0",
|
||||
"info": "\f1b1",
|
||||
"info-filled": "\f1b2",
|
||||
"help": "\f1b3",
|
||||
"heart": "\f1b4",
|
||||
"heart-outline": "\f1b5",
|
||||
"hd-photo": "\f1b6",
|
||||
"hashtag": "\f1b7",
|
||||
"hand-stop": "\f1b8",
|
||||
"hand-stop-filled": "\f1b9",
|
||||
"grouped": "\f1ba",
|
||||
"grouped-disable": "\f1bb",
|
||||
"group": "\f1bc",
|
||||
"group-filled": "\f1bd",
|
||||
"gift": "\f1be",
|
||||
"gift-transfer-inline": "\f1bf",
|
||||
"gifs": "\f1c0",
|
||||
"fullscreen": "\f1c1",
|
||||
"frozen-time": "\f1c2",
|
||||
"fragment": "\f1c3",
|
||||
"forward": "\f1c4",
|
||||
"forums": "\f1c5",
|
||||
"fontsize": "\f1c6",
|
||||
"folder": "\f1c7",
|
||||
"folder-badge": "\f1c8",
|
||||
"flip": "\f1c9",
|
||||
"flag": "\f1ca",
|
||||
"file-badge": "\f1cb",
|
||||
"favorite": "\f1cc",
|
||||
"favorite-filled": "\f1cd",
|
||||
"eye": "\f1ce",
|
||||
"eye-outline": "\f1cf",
|
||||
"eye-crossed": "\f1d0",
|
||||
"eye-crossed-outline": "\f1d1",
|
||||
"expand": "\f1d2",
|
||||
"expand-modal": "\f1d3",
|
||||
"enter": "\f1d4",
|
||||
"email": "\f1d5",
|
||||
"edit": "\f1d6",
|
||||
"eats": "\f1d7",
|
||||
"dropdown-arrows": "\f1d8",
|
||||
"download": "\f1d9",
|
||||
"down": "\f1da",
|
||||
"double-badge": "\f1db",
|
||||
"document": "\f1dc",
|
||||
"diamond": "\f1dd",
|
||||
"delete": "\f1de",
|
||||
"delete-user": "\f1df",
|
||||
"delete-left": "\f1e0",
|
||||
"delete-filled": "\f1e1",
|
||||
"data": "\f1e2",
|
||||
"darkmode": "\f1e3",
|
||||
"crown-wear": "\f1e4",
|
||||
"crown-wear-outline": "\f1e5",
|
||||
"crown-take-off": "\f1e6",
|
||||
"crown-take-off-outline": "\f1e7",
|
||||
"crop": "\f1e8",
|
||||
"craft": "\f1e9",
|
||||
"copy": "\f1ea",
|
||||
"copy-media": "\f1eb",
|
||||
"comments": "\f1ec",
|
||||
"comments-sticker": "\f1ed",
|
||||
"combine-craft": "\f1ee",
|
||||
"colorize": "\f1ef",
|
||||
"collapse": "\f1f0",
|
||||
"collapse-modal": "\f1f1",
|
||||
"cloud-download": "\f1f2",
|
||||
"closed-gift": "\f1f3",
|
||||
"close": "\f1f4",
|
||||
"close-topic": "\f1f5",
|
||||
"close-circle": "\f1f6",
|
||||
"clock": "\f1f7",
|
||||
"clock-edit": "\f1f8",
|
||||
"check": "\f1f9",
|
||||
"check-bold": "\f1fa",
|
||||
"chats-badge": "\f1fb",
|
||||
"chat-badge": "\f1fc",
|
||||
"channelviews": "\f1fd",
|
||||
"channel": "\f1fe",
|
||||
"channel-filled": "\f1ff",
|
||||
"cash-circle": "\f200",
|
||||
"card": "\f201",
|
||||
"car": "\f202",
|
||||
"camera": "\f203",
|
||||
"camera-add": "\f204",
|
||||
"calendar": "\f205",
|
||||
"calendar-filter": "\f206",
|
||||
"bug": "\f207",
|
||||
"brush": "\f208",
|
||||
"bots": "\f209",
|
||||
"bot-commands-filled": "\f20a",
|
||||
"bot-command": "\f20b",
|
||||
"boosts": "\f20c",
|
||||
"boostcircle": "\f20d",
|
||||
"boost": "\f20e",
|
||||
"boost-outline": "\f20f",
|
||||
"boost-craft-chance": "\f210",
|
||||
"bold": "\f211",
|
||||
"avatar-saved-messages": "\f212",
|
||||
"avatar-deleted-account": "\f213",
|
||||
"avatar-archived-chats": "\f214",
|
||||
"author-hidden": "\f215",
|
||||
"auction": "\f216",
|
||||
"auction-next-round": "\f217",
|
||||
"auction-filled": "\f218",
|
||||
"auction-drop": "\f219",
|
||||
"attach": "\f21a",
|
||||
"ask-support": "\f21b",
|
||||
"arrow-right": "\f21c",
|
||||
"arrow-left": "\f21d",
|
||||
"arrow-down": "\f21e",
|
||||
"arrow-down-circle": "\f21f",
|
||||
"archive": "\f220",
|
||||
"archive-to-main": "\f221",
|
||||
"archive-from-main": "\f222",
|
||||
"archive-filled": "\f223",
|
||||
"animations": "\f224",
|
||||
"animals": "\f225",
|
||||
"allow-speak": "\f226",
|
||||
"allow-share": "\f227",
|
||||
"admin": "\f228",
|
||||
"add": "\f229",
|
||||
"add-user": "\f22a",
|
||||
"add-user-filled": "\f22b",
|
||||
"add-one-badge": "\f22c",
|
||||
"add-filled": "\f22d",
|
||||
"add-caption": "\f22e",
|
||||
"active-sessions": "\f22f",
|
||||
"rating-icons-negative": "\f230",
|
||||
"rating-icons-level90": "\f231",
|
||||
"rating-icons-level9": "\f232",
|
||||
"rating-icons-level80": "\f233",
|
||||
"rating-icons-level8": "\f234",
|
||||
"rating-icons-level70": "\f235",
|
||||
"rating-icons-level7": "\f236",
|
||||
"rating-icons-level60": "\f237",
|
||||
"rating-icons-level6": "\f238",
|
||||
"rating-icons-level50": "\f239",
|
||||
"rating-icons-level5": "\f23a",
|
||||
"rating-icons-level40": "\f23b",
|
||||
"rating-icons-level4": "\f23c",
|
||||
"rating-icons-level30": "\f23d",
|
||||
"rating-icons-level3": "\f23e",
|
||||
"rating-icons-level20": "\f23f",
|
||||
"rating-icons-level2": "\f240",
|
||||
"rating-icons-level10": "\f241",
|
||||
"rating-icons-level1": "\f242",
|
||||
"folder-tabs-user": "\f243",
|
||||
"folder-tabs-star": "\f244",
|
||||
"folder-tabs-group": "\f245",
|
||||
"folder-tabs-folder": "\f246",
|
||||
"folder-tabs-chats": "\f247",
|
||||
"folder-tabs-chat": "\f248",
|
||||
"folder-tabs-channel": "\f249",
|
||||
"folder-tabs-bot": "\f24a",
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -140,6 +140,7 @@ export type FontIconName =
|
||||
| 'no-download'
|
||||
| 'next'
|
||||
| 'next-link'
|
||||
| 'new-send'
|
||||
| 'new-chat-filled'
|
||||
| 'my-notes'
|
||||
| 'muted'
|
||||
@ -247,6 +248,7 @@ export type FontIconName =
|
||||
| 'clock'
|
||||
| 'clock-edit'
|
||||
| 'check'
|
||||
| 'check-bold'
|
||||
| 'chats-badge'
|
||||
| 'chat-badge'
|
||||
| 'channelviews'
|
||||
|
||||
22
src/types/language.d.ts
vendored
22
src/types/language.d.ts
vendored
@ -262,6 +262,7 @@ export interface LangPair {
|
||||
'DialogPin': undefined;
|
||||
'ConversationPinMessageAlertPinAndNotifyMembers': undefined;
|
||||
'SavedMessages': undefined;
|
||||
'SavedMessagesShort': undefined;
|
||||
'AccDescrPrevious': undefined;
|
||||
'ReportReasonSpam': undefined;
|
||||
'ReportReasonViolence': undefined;
|
||||
@ -526,6 +527,8 @@ export interface LangPair {
|
||||
'ContactShare': undefined;
|
||||
'OK': undefined;
|
||||
'ForwardTo': undefined;
|
||||
'ShareWith': undefined;
|
||||
'SelectChats': undefined;
|
||||
'AttachGame': undefined;
|
||||
'JumpToDate': undefined;
|
||||
'FloodWait': undefined;
|
||||
@ -1978,8 +1981,6 @@ export interface LangPair {
|
||||
'AttachmentMenuUngroupAllMedia': undefined;
|
||||
'AttachmentMenuEnableSpoiler': undefined;
|
||||
'AttachmentMenuDisableSpoiler': undefined;
|
||||
'AttachmentSendGif': undefined;
|
||||
'AttachmentReplaceGif': undefined;
|
||||
'AttachmentDragAddItems': undefined;
|
||||
'AttachmentCaptionPlaceholder': undefined;
|
||||
'MessageSummaryTitle': undefined;
|
||||
@ -2187,6 +2188,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'ConversationOpenBotLinkAllowMessages': {
|
||||
'bot': V;
|
||||
};
|
||||
'ForwardForStars': {
|
||||
'price': V;
|
||||
};
|
||||
'BlockUserTitle': {
|
||||
'user': V;
|
||||
};
|
||||
@ -3085,6 +3089,10 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'ComposerPlaceholderPaidReply': {
|
||||
'amount': V;
|
||||
};
|
||||
'ForwardPaidChatsConfirmation': {
|
||||
'chatsSelected': V;
|
||||
'payConfirmation': V;
|
||||
};
|
||||
'MessageSentPaidToastText': {
|
||||
'amount': V;
|
||||
};
|
||||
@ -3910,6 +3918,13 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'PayForMessage': {
|
||||
'count': V;
|
||||
};
|
||||
'ForwardPaidChatsSelected': {
|
||||
'paidChatsCount': V;
|
||||
};
|
||||
'ForwardPaidChatsPayConfirmation': {
|
||||
'totalAmount': V;
|
||||
'count': V;
|
||||
};
|
||||
'MessageSentPaidToastTitle': {
|
||||
'count': V;
|
||||
};
|
||||
@ -4045,6 +4060,9 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'GiftPreviewCountBackdrops': {
|
||||
'count': V;
|
||||
};
|
||||
'FwdMessagesToChats': {
|
||||
'count': V;
|
||||
};
|
||||
}
|
||||
export type RegularLangKey = keyof LangPair;
|
||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user