diff --git a/CLAUDE.md b/CLAUDE.md index 0eac1231c..e653d22ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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' }); diff --git a/src/assets/font-icons/check-bold.svg b/src/assets/font-icons/check-bold.svg new file mode 100644 index 000000000..314a5d4ba --- /dev/null +++ b/src/assets/font-icons/check-bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/new-send.svg b/src/assets/font-icons/new-send.svg new file mode 100644 index 000000000..5c7d8e4be --- /dev/null +++ b/src/assets/font-icons/new-send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 3221c2048..55aff40bd 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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**"; diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index ae1d4de01..61298bf70 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -896,7 +896,7 @@ } #caption-input-text .placeholder-text { - bottom: 0.8125rem; + bottom: 0.875rem; } #story-input-text .placeholder-text { diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 0dacba168..1b8538857 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -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} diff --git a/src/components/common/File.scss b/src/components/common/File.scss index c2ad90875..1ddf1ecc8 100644 --- a/src/components/common/File.scss +++ b/src/components/common/File.scss @@ -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, diff --git a/src/components/common/File.tsx b/src/components/common/File.tsx index 6e584b26d..68820e4e5 100644 --- a/src/components/common/File.tsx +++ b/src/components/common/File.tsx @@ -26,6 +26,8 @@ import Icon from './icons/Icon'; import './File.scss'; +type FileSize = 'small' | 'medium' | 'large'; + type OwnProps = { ref?: ElementRef; 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 = ({
diff --git a/src/components/common/PeerChip.module.scss b/src/components/common/PeerChip.module.scss index a1ff9c09f..b1498557b 100644 --- a/src/components/common/PeerChip.module.scss +++ b/src/components/common/PeerChip.module.scss @@ -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; } } diff --git a/src/components/common/PeerChip.tsx b/src/components/common/PeerChip.tsx index c87cae219..2307c748c 100644 --- a/src/components/common/PeerChip.tsx +++ b/src/components/common/PeerChip.tsx @@ -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 = { - peerId?: string; - + size?: PeerChipSize; forceShowSelf?: boolean; customPeer?: CustomPeer; mockPeer?: ApiPeer; @@ -33,11 +38,11 @@ type OwnProps = { 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 = ({ icon, title, + size = 'medium', isMinimized, canClose, isCloseNonDestructive, @@ -57,10 +63,10 @@ const PeerChip = ({ mockPeer, customPeer, className, + itemClassName, isSavedMessages, withPeerColors, withEmojiStatus, - itemClassName, theme, onClick, }: OwnProps & StateProps) => { @@ -106,6 +112,7 @@ const PeerChip = ({ 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 = ({ className, ); + const chipSize = size === 'small' ? CHIP_SIZE_SMALL : CHIP_SIZE_MEDIUM; + const style = buildStyle( + `--chip-size: ${chipSize}px`, withPeerColors && peerColorStyle, ); diff --git a/src/components/common/RecipientPicker.module.scss b/src/components/common/RecipientPicker.module.scss new file mode 100644 index 000000000..608914bb3 --- /dev/null +++ b/src/components/common/RecipientPicker.module.scss @@ -0,0 +1,7 @@ +.recentContacts { + margin-block: 1rem; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/src/components/common/RecipientPicker.tsx b/src/components/common/RecipientPicker.tsx index 543b0397c..0925a10b2 100644 --- a/src/components/common/RecipientPicker.tsx +++ b/src/components/common/RecipientPicker.tsx @@ -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([]); + const [removingIds, setRemovingIds] = useState([]); + const [appearingIds, setAppearingIds] = useState([]); + 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 ( +
+ + +
+ ); + } + return ( - +
+
+ {selectedIds.map((selectionId) => { + const { peerId } = parseSelectionId(selectionId); + const chipTitle = getChipTitle(selectionId); + const isAppearing = appearingIds.includes(selectionId); + const isRemoving = removingIds.includes(selectionId); + + return ( +
+ +
+ ); + })} +
+ + +
+
+
); - }, [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 && ( + + )} + {Boolean(hasFolderTabs) && folderTabs && ( + + )} + + ); + }, [ + recentContactIds, + search, + shouldRenderFolders, + currentUserId, + handleSelect, + isMultiSelect, + selectedIds, + folderTabs, + activeFolderIndex, + handleSwitchFolderIndex, + ]); return ( .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; + } + } } diff --git a/src/components/common/pickers/ChatOrUserPicker.tsx b/src/components/common/pickers/ChatOrUserPicker.tsx index 52c7856f6..9bcca1e79 100644 --- a/src/components/common/pickers/ChatOrUserPicker.tsx +++ b/src/components/common/pickers/ChatOrUserPicker.tsx @@ -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; + value: string; + placeholder: string; + onChange: (e: React.ChangeEvent) => void; + onKeyDown?: React.KeyboardEventHandler; +}; + 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 ? ( +
+ {(isSelected || hasSelectedTopics) && } + {hasSelectedTopics && ( +
{selectedTopicsCount}
+ )} +
+ ) : undefined; + return ( )} avatarElement={( - +
+ + {isForum && } +
)} + 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 ( <> -
-
-
+
+ {renderSearchRow ? renderSearchRow({ + inputRef: topicSearchRef, + value: topicSearch, + placeholder: searchPlaceholder, + onChange: handleTopicSearchChange, + onKeyDown: handleTopicKeyDown, + }) : ( +
+ + +
+ )}
{topicIds?.length ? ( - {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={( - - )} - title={renderText(topics[topicId].title)} + const topicCheckboxElement = isMultiSelect ? ( +
+ {isTopicSelected && } +
+ ) : undefined; + + return ( + onSelectChatOrUser(forumId!, topicId)} + style={`top: ${i * TOPIC_ITEM_HEIGHT_PX}px;`} + avatarElement={( +
+ +
+ )} + title={renderText(topics[topicId].title)} + inputElement={topicCheckboxElement} + inputPosition="end" + /> + ); + })} + {Boolean(viewportFooter) && ( +
- ))} + )} ) : topicIds && !topicIds.length ? (

{lang('NothingFound')}

) : ( - +
+ +
)} ); @@ -326,24 +394,28 @@ const ChatOrUserPicker = ({ function renderChatList() { return ( <> -
-
-
+
+ {renderSearchRow ? renderSearchRow({ + inputRef: searchRef, + value: search, + placeholder: searchPlaceholder, + onChange: handleSearchChange, + onKeyDown: chatKeyDownHandler, + }) : ( +
+ + +
+ )} {subheader}
{ + if (forumId) { + handleHeaderBackClick(); + } else { + onClose(); + } + }); + return ( @@ -378,6 +463,7 @@ const ChatOrUserPicker = ({ return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList(); }} + {footer} ); }; @@ -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) && ( +
+ )} ) : viewportIds && !viewportIds.length ? (

{lang('NothingFound')}

diff --git a/src/components/common/pickers/PickerRecentContacts.module.scss b/src/components/common/pickers/PickerRecentContacts.module.scss new file mode 100644 index 000000000..bc9bc3d69 --- /dev/null +++ b/src/components/common/pickers/PickerRecentContacts.module.scss @@ -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; + } +} diff --git a/src/components/common/pickers/PickerRecentContacts.tsx b/src/components/common/pickers/PickerRecentContacts.tsx new file mode 100644 index 000000000..8ae9fd57d --- /dev/null +++ b/src/components/common/pickers/PickerRecentContacts.tsx @@ -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(); + + 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 ( +
+
+ {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 ( +
handleClick(peerId)} + > +
+ +
+ +
+
+
{renderText(name || lang('ActionFallbackSomeone'))}
+
+ ); + })} +
+
+ ); +}; + +export default memo(PickerRecentContacts); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 1f479918b..071bfc4bd 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -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 = ({ > {shouldRenderStoryRibbon && } {shouldRenderFolders ? ( - = ({ 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!)} diff --git a/src/components/left/search/LeftSearch.scss b/src/components/left/search/LeftSearch.scss index d3634041f..7c05a3a6a 100644 --- a/src/components/left/search/LeftSearch.scss +++ b/src/components/left/search/LeftSearch.scss @@ -11,7 +11,7 @@ flex: 1; } - .TabList { + .SquareTabList { z-index: 1; } diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index 76d30e7c2..b028cc029 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -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 = ({ return (
- + = ({ blockUser, } = getActions(); - const lang = useOldLang(); + const lang = useLang(); const [search, setSearch] = useState(''); useEffect(() => { @@ -76,7 +76,8 @@ const BlockUserModal: FC = ({ = ({ 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 = ({ return ( = ({ resetOpenChatWithDraft, } = getActions(); - const lang = useOldLang(); + const lang = useLang(); const [isShown, markIsShown, unmarkIsShown] = useFlag(); useEffect(() => { @@ -54,7 +54,8 @@ const DraftRecipientPicker: FC = ({ return ( = ({ @@ -37,26 +56,80 @@ const ForwardRecipientPicker: FC = ({ 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([]); + 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 = ({ 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 = ({ 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 = ({ 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(() => ( +
+ ), []); + + 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 ( + + ); + }; + + const renderComposer = () => ( +
+
+ setCaption(e.currentTarget.value)} + placeholder={lang('AttachmentCaptionPlaceholder')} + /> + +
+
+ ); + + return ( +
+
+ {renderButton()} +
+
+ {renderComposer()} +
+
+ ); + }, [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 ( - + <> + + + {confirmPaymentMessage} + + + ); }; export default memo(withGlobal((global): Complete => { - 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)); diff --git a/src/components/middle/composer/AttachmentModal.module.scss b/src/components/middle/composer/AttachmentModal.module.scss index aa828a289..7a1c36c14 100644 --- a/src/components/middle/composer/AttachmentModal.module.scss +++ b/src/components/middle/composer/AttachmentModal.module.scss @@ -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 { diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 3310df42d..5a14a2c66 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -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 ( -
+
{canShowCustomSendMenu && ( { + 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 ( <> @@ -117,7 +120,7 @@ const AttachmentModalItem = ({ ); return ( -
+
{content} { onClose={handleClose} > {renderHeader()} - = ({ <>
- {renderContent()} - +
)} diff --git a/src/components/right/hooks/useProfileState.ts b/src/components/right/hooks/useProfileState.ts index 0a058d5ed..8dacdabaa 100644 --- a/src/components/right/hooks/useProfileState.ts +++ b/src/components/right/hooks/useProfileState.ts @@ -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('.TabList')!; + const tabsEl = container.querySelector('.SquareTabList')!; handleStopAutoScrollToTabs(); if (container.scrollTop < tabsEl.offsetTop) { onProfileStateChange(getStateFromTabType(tabType)); @@ -69,7 +69,7 @@ export default function useProfileState({ return; } - const tabListEl = container.querySelector('.TabList'); + const tabListEl = container.querySelector('.SquareTabList'); if (!tabListEl || tabListEl.offsetTop > container.scrollTop) { return; } @@ -94,7 +94,7 @@ export default function useProfileState({ return; } - const tabListEl = container.querySelector('.TabList'); + const tabListEl = container.querySelector('.SquareTabList'); if (!tabListEl) { return; } diff --git a/src/components/right/hooks/useTransitionFixes.ts b/src/components/right/hooks/useTransitionFixes.ts index 7748265ff..575aa2f0b 100644 --- a/src/components/right/hooks/useTransitionFixes.ts +++ b/src/components/right/hooks/useTransitionFixes.ts @@ -12,7 +12,7 @@ export default function useTransitionFixes( function setMinHeight() { const container = containerRef.current!; const transitionEl = container.querySelector(transitionElSelector); - const tabsEl = container.querySelector('.TabList'); + const tabsEl = container.querySelector('.SquareTabList'); if (transitionEl && tabsEl) { const newHeight = container.clientHeight - tabsEl.offsetHeight; diff --git a/src/components/right/management/RemoveGroupUserModal.tsx b/src/components/right/management/RemoveGroupUserModal.tsx index b9a1d5540..255b7b105 100644 --- a/src/components/right/management/RemoveGroupUserModal.tsx +++ b/src/components/right/management/RemoveGroupUserModal.tsx @@ -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 = ({ deleteChatMember, } = getActions(); - const lang = useOldLang(); + const lang = useLang(); const [search, setSearch] = useState(''); const usersId = useMemo(() => { @@ -65,7 +65,8 @@ const RemoveGroupUserModal: FC = ({ {renderContent()} - +
) : ( <> diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index 16c2de449..ba919b405 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -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; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 559d62092..fe9d7264f 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -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 ? ( ) : undefined; return title ? ( diff --git a/src/components/ui/TabList.scss b/src/components/ui/SquareTabList.scss similarity index 97% rename from src/components/ui/TabList.scss rename to src/components/ui/SquareTabList.scss index 957191be2..7eaeadfd0 100644 --- a/src/components/ui/TabList.scss +++ b/src/components/ui/SquareTabList.scss @@ -1,4 +1,4 @@ -.TabList { +.SquareTabList { scrollbar-color: rgba(0, 0, 0, 0); scrollbar-width: none; diff --git a/src/components/ui/SquareTabList.tsx b/src/components/ui/SquareTabList.tsx new file mode 100644 index 000000000..3f1ebe2a0 --- /dev/null +++ b/src/components/ui/SquareTabList.tsx @@ -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; + 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(); + 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 ( +
+ {tabs.map((tab, i) => ( + + ))} +
+ ); +}; + +export default memo(SquareTabList); diff --git a/src/components/ui/TabList.module.scss b/src/components/ui/TabList.module.scss new file mode 100644 index 000000000..9a970f259 --- /dev/null +++ b/src/components/ui/TabList.module.scss @@ -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; +} diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index 4652efe9e..7337bf3b5 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -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; 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(); - if (ref) { - containerRef = ref; - } - const previousActiveTab = usePreviousDeprecated(activeTab); + const containerRef = useRef(); + const clipPathContainerRef = useRef(); + const [clipPath, setClipPath] = useState(''); - 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) => ( +
handleTabClick(index)} + > + {tab.title} + {tab.isBlocked && } +
+ ); return (
- {tabs.map((tab, i) => ( - - ))} + {tabs.map(renderTab)} + +
+ {tabs.map(renderTab)} +
); }; diff --git a/src/components/ui/mediaEditor/MediaEditor.tsx b/src/components/ui/mediaEditor/MediaEditor.tsx index 93f4138c6..25c0b5bda 100644 --- a/src/components/ui/mediaEditor/MediaEditor.tsx +++ b/src/components/ui/mediaEditor/MediaEditor.tsx @@ -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()} - { + 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 || {}; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 19ece30c9..d33994c14 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -692,6 +692,20 @@ export function selectAllowedMessageActionsSlow( }; } +export function selectCanCopyMessageLink( + 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( global: T, chatId: string, diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 282c8e1ca..f813d8c15 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -2023,6 +2023,10 @@ export interface ActionPayloads { } & WithTabId; exitForwardMode: WithTabId | undefined; changeRecipient: WithTabId | undefined; + forwardToMultipleChats: { + toChatIds: string[]; + comment?: string; + } & WithTabId; forwardToSavedMessages: { scheduledAt?: number; } & WithTabId; diff --git a/src/hooks/useFolderTabs.ts b/src/hooks/useFolderTabs.ts index 36d988e14..84f38a25a 100644 --- a/src/hooks/useFolderTabs.ts +++ b/src/hooks/useFolderTabs.ts @@ -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'; diff --git a/src/styles/icons.css b/src/styles/icons.css index 1858b9d41..c672e4dec 100644 --- a/src/styles/icons.css +++ b/src/styles/icons.css @@ -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"; +} diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 607781dfa..29f55cb11 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -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", ); diff --git a/src/styles/icons.woff b/src/styles/icons.woff index b18001d45..7ad482c27 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 7b4531a8f..96f91a52b 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index ba34177fc..d0e61469e 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -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' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 967b40d6c..04a9073fd 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'ConversationOpenBotLinkAllowMessages': { 'bot': V; }; + 'ForwardForStars': { + 'price': V; + }; 'BlockUserTitle': { 'user': V; }; @@ -3085,6 +3089,10 @@ export interface LangPairWithVariables { 'ComposerPlaceholderPaidReply': { 'amount': V; }; + 'ForwardPaidChatsConfirmation': { + 'chatsSelected': V; + 'payConfirmation': V; + }; 'MessageSentPaidToastText': { 'amount': V; }; @@ -3910,6 +3918,13 @@ export interface LangPairPluralWithVariables { 'PayForMessage': { 'count': V; }; + 'ForwardPaidChatsSelected': { + 'paidChatsCount': V; + }; + 'ForwardPaidChatsPayConfirmation': { + 'totalAmount': V; + 'count': V; + }; 'MessageSentPaidToastTitle': { 'count': V; }; @@ -4045,6 +4060,9 @@ export interface LangPairPluralWithVariables { 'GiftPreviewCountBackdrops': { 'count': V; }; + 'FwdMessagesToChats': { + 'count': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;