diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 11b784607..d5b6b180a 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1393,62 +1393,66 @@ function buildUploadingMedia( previewBlobUrl, mimeType, size, + audio, + shouldSendAsFile, } = attachment; - if (attachment.quick) { - if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { - const { width, height } = attachment.quick; + if (!shouldSendAsFile) { + if (attachment.quick) { + if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { + const { width, height } = attachment.quick; + return { + photo: { + id: LOCAL_MEDIA_UPLOADING_TEMP_ID, + sizes: [], + thumbnail: { width, height, dataUri: '' }, // Used only for dimensions + blobUrl, + }, + }; + } + if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { + const { width, height, duration } = attachment.quick; + return { + video: { + id: LOCAL_MEDIA_UPLOADING_TEMP_ID, + mimeType, + duration: duration || 0, + fileName, + width, + height, + blobUrl, + ...(previewBlobUrl && { thumbnail: { width, height, dataUri: previewBlobUrl } }), + size, + }, + }; + } + } + if (attachment.voice) { + const { duration, waveform } = attachment.voice; + const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); return { - photo: { + voice: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, - sizes: [], - thumbnail: { width, height, dataUri: '' }, // Used only for dimensions - blobUrl, + duration, + waveform: inputWaveform, }, }; } - if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { - const { width, height, duration } = attachment.quick; + if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) { + const { duration, performer, title } = audio || {}; return { - video: { + audio: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, mimeType, - duration: duration || 0, fileName, - width, - height, - blobUrl, - ...(previewBlobUrl && { thumbnail: { width, height, dataUri: previewBlobUrl } }), size, + duration: duration || 0, + title, + performer, }, }; } } - if (attachment.voice) { - const { duration, waveform } = attachment.voice; - const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); - return { - voice: { - id: LOCAL_MEDIA_UPLOADING_TEMP_ID, - duration, - waveform: inputWaveform, - }, - }; - } - if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) { - const { duration, performer, title } = attachment.audio || {}; - return { - audio: { - id: LOCAL_MEDIA_UPLOADING_TEMP_ID, - mimeType, - fileName, - size, - duration: duration || 0, - title, - performer, - }, - }; - } return { document: { mimeType, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 3ea3b5615..6ddaf328a 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -566,7 +566,7 @@ export async function rescheduleMessage({ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) { const { - filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, + filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, shouldSendAsFile, } = attachment; const patchedOnProgress: ApiOnProgress = (progress) => { @@ -584,41 +584,43 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, const thumb = thumbFile ? await uploadFile(thumbFile) : undefined; const attributes: GramJs.TypeDocumentAttribute[] = [new GramJs.DocumentAttributeFilename({ fileName: filename })]; - if (quick) { - if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { - return new GramJs.InputMediaUploadedPhoto({ file: inputFile }); - } + if (!shouldSendAsFile) { + if (quick) { + if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { + return new GramJs.InputMediaUploadedPhoto({ file: inputFile }); + } - if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { - const { width, height, duration } = quick; - if (duration !== undefined) { - attributes.push(new GramJs.DocumentAttributeVideo({ - duration, - w: width, - h: height, - supportsStreaming: true, - })); + if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { + const { width, height, duration } = quick; + if (duration !== undefined) { + attributes.push(new GramJs.DocumentAttributeVideo({ + duration, + w: width, + h: height, + supportsStreaming: true, + })); + } } } - } - if (audio) { - const { duration, title, performer } = audio; - attributes.push(new GramJs.DocumentAttributeAudio({ - duration, - title, - performer, - })); - } + if (audio) { + const { duration, title, performer } = audio; + attributes.push(new GramJs.DocumentAttributeAudio({ + duration, + title, + performer, + })); + } - if (voice) { - const { duration, waveform } = voice; - const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); - attributes.push(new GramJs.DocumentAttributeAudio({ - voice: true, - duration, - waveform: Buffer.from(inputWaveform), - })); + if (voice) { + const { duration, waveform } = voice; + const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); + attributes.push(new GramJs.DocumentAttributeAudio({ + voice: true, + duration, + waveform: Buffer.from(inputWaveform), + })); + } } return new GramJs.InputMediaUploadedDocument({ @@ -626,6 +628,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, mimeType, attributes, thumb, + forceFile: shouldSendAsFile || undefined, }); } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index fa7bb2d68..177407e52 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -45,6 +45,10 @@ export interface ApiAttachment { performer?: string; }; previewBlobUrl?: string; + + shouldSendAsFile?: boolean; + + uniqueId?: string; } export interface ApiWallpaper { diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index a9fcf97cd..7d6f7cbec 100644 Binary files a/src/assets/fonts/icomoon.woff and b/src/assets/fonts/icomoon.woff differ diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index 599c93bcc..a08edb587 100644 Binary files a/src/assets/fonts/icomoon.woff2 and b/src/assets/fonts/icomoon.woff2 differ diff --git a/src/components/common/File.tsx b/src/components/common/File.tsx index d619e6fe2..6b1fe4ff6 100644 --- a/src/components/common/File.tsx +++ b/src/components/common/File.tsx @@ -140,7 +140,7 @@ const File: FC = ({ )}
-
{renderText(name)}
+
{renderText(name)}
{isTransferring && transferProgress ? `${Math.round(transferProgress * 100)}%` : sizeString} diff --git a/src/components/left/settings/SettingsGeneralBackground.tsx b/src/components/left/settings/SettingsGeneralBackground.tsx index 6260e6d8a..47a3bfe44 100644 --- a/src/components/left/settings/SettingsGeneralBackground.tsx +++ b/src/components/left/settings/SettingsGeneralBackground.tsx @@ -10,6 +10,7 @@ import type { ApiWallpaper } from '../../../api/types'; import { DARK_THEME_PATTERN_COLOR, DEFAULT_PATTERN_COLOR } from '../../../config'; import { throttle } from '../../../util/schedulers'; +import { validateFiles } from '../../../util/files'; import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; import { getAverageColor, getPatternColor, rgb2hex } from '../../../util/colors'; import { selectTheme } from '../../../global/selectors'; @@ -68,8 +69,9 @@ const SettingsGeneralBackground: FC = ({ const handleFileSelect = useCallback((e: Event) => { const { files } = e.target as HTMLInputElement; - if (files && files.length > 0) { - uploadWallpaper(files[0]); + const validatedFiles = validateFiles(files); + if (validatedFiles?.length) { + uploadWallpaper(validatedFiles[0]); } }, [uploadWallpaper]); diff --git a/src/components/middle/MessageSelectToolbar.tsx b/src/components/middle/MessageSelectToolbar.tsx index a2985d1c5..7cf957f3b 100644 --- a/src/components/middle/MessageSelectToolbar.tsx +++ b/src/components/middle/MessageSelectToolbar.tsx @@ -188,7 +188,7 @@ export default memo(withGlobal( const canDownload = selectCanDownloadSelectedMessages(global); const { messageIds: selectedMessageIds } = global.selectedMessages || {}; const hasProtectedMessage = chatId ? selectHasProtectedMessage(global, chatId, selectedMessageIds) : false; - const canForward = chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false; + const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false; const isForwardModalOpen = global.forwardMessages.isModalShown; const isAnyModalOpen = Boolean(isForwardModalOpen || global.requestedDraft || global.requestedAttachBotInChat || global.requestedAttachBotInstall); diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 7b7a0da9e..9a9b44e1b 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -28,7 +28,6 @@ import { import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT, - IS_TOUCH_ENV, MASK_IMAGE_DISABLED, } from '../../util/environment'; import { DropAreaState } from './composer/DropArea'; @@ -285,10 +284,6 @@ const MiddleColumn: FC = ({ }, [shouldLoadFullChat, chatId, isReady, loadFullChat]); const handleDragEnter = useCallback((e: React.DragEvent) => { - if (IS_TOUCH_ENV) { - return; - } - const { items } = e.dataTransfer || {}; const shouldDrawQuick = items && items.length > 0 && Array.from(items) // Filter unnecessary element for drag and drop images in Firefox (https://github.com/Ajaxy/telegram-tt/issues/49) diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index 0cf7626a5..745e46686 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -10,6 +10,7 @@ import type { ISettings } from '../../../types'; import { CONTENT_TYPES_WITH_PREVIEW } from '../../../config'; import { IS_TOUCH_ENV } from '../../../util/environment'; import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; +import { validateFiles } from '../../../util/files'; import useMouseInside from '../../../hooks/useMouseInside'; import useLang from '../../../hooks/useLang'; @@ -31,7 +32,7 @@ export type OwnProps = { isScheduled?: boolean; attachBots: GlobalState['attachMenu']['bots']; peerType?: ApiAttachMenuPeerType; - onFileSelect: (files: File[], isQuick: boolean) => void; + onFileSelect: (files: File[], shouldSuggestCompression?: boolean) => void; onPollCreate: () => void; theme: ISettings['theme']; }; @@ -67,11 +68,12 @@ const AttachMenu: FC = ({ } }, [isAttachMenuOpen, openAttachMenu, closeAttachMenu]); - const handleFileSelect = useCallback((e: Event, isQuick: boolean) => { + const handleFileSelect = useCallback((e: Event, shouldSuggestCompression?: boolean) => { const { files } = e.target as HTMLInputElement; + const validatedFiles = validateFiles(files); - if (files && files.length > 0) { - onFileSelect(Array.from(files), isQuick); + if (validatedFiles?.length) { + onFileSelect(validatedFiles, shouldSuggestCompression); } }, [onFileSelect]); diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index 7dbd4643e..599b777f7 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -5,65 +5,49 @@ max-width: 26.25rem; @media (max-width: 600px) { max-height: 100%; - padding-bottom: 1.5rem; } } + .modal-header-condensed { + padding: 0.3125rem 0.5rem !important; + border-bottom: 1px solid var(--color-borders); + } + .modal-content { - padding: 0.5rem 1.25rem 1.875rem; + padding: 0; max-height: calc(100vh - 3.25rem); - @media (max-width: 600px) { - padding-bottom: 0.25rem; - } + + overflow-x: auto; } - .media-wrapper { + .attachments-wrapper { max-height: 26rem; overflow: auto; + flex-shrink: 0; + display: flex; flex-wrap: wrap; - margin-bottom: 1.5rem; + gap: 0.5rem; + margin: 0 0.25rem; + padding: 0.5rem 0.25rem; - video, - img { - flex: 1; - width: calc(50% - 0.15rem); - height: 12rem; - margin-bottom: 0.3125rem; - border-radius: var(--border-radius-default); - object-fit: cover; - - &:only-child { - height: auto; - max-height: 25rem; - margin-bottom: 0; - } - - &:nth-child(even) { - margin-left: 0.3125rem; - } - } - } - - .document-wrapper { - max-height: 25rem; - overflow: auto; - flex-shrink: 0; - display: flex; - flex-direction: column; - margin: 0.75rem 0 1.75rem; - - .File:not(:last-child) { - margin-bottom: 1.5rem; - } - - .file-icon { - cursor: default !important; + @media (max-width: 600px) { + max-height: 80vh; } } .attachment-caption-wrapper { position: relative; + margin: 0 0.5rem; + + &::before { + content: ''; + position: absolute; + left: -0.5rem; + top: 0; + right: -0.5rem; + border-top: 1px solid var(--color-borders); + } .form-control { background: var(--color-background); @@ -75,6 +59,15 @@ } } + .attachment-caption { + display: flex; + gap: 0.5rem; + } + + .attachment-checkbox { + margin-left: -1rem; + } + .drop-target { position: relative; overflow: hidden; @@ -119,9 +112,8 @@ } .attachment-caption-wrapper, - .document-wrapper, - .media-wrapper, - .form-control { + .attachments-wrapper, + .input-scroller { pointer-events: none; } @@ -136,13 +128,24 @@ } .CustomSendMenu { - bottom: auto; + bottom: 2.25rem; .is-pointer-env & > .backdrop { width: 100%; - top: -2.25rem; + top: -2rem; bottom: auto; - height: 2.75rem; + height: 3.5rem; } } + + .AttachmentModal--send-wrapper { + align-self: flex-end; + margin-right: 0.25rem; + } + + .AttachmentModal--send { + height: 2.5rem; + padding: 0 1rem; + margin-bottom: 0.25rem; + } } diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 70f04f4bc..72665db44 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -1,22 +1,28 @@ import React, { - memo, useCallback, useEffect, useMemo, useRef, + memo, useCallback, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; -import { getActions } from '../../../global'; +import { getActions, withGlobal } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import type { ApiAttachment, ApiChatMember, ApiSticker } from '../../../api/types'; +import type { GlobalState } from '../../../global/types'; import { + BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_MODAL_ID, SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; -import { getFileExtension } from '../../common/helpers/documentInfo'; +import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems'; -import { hasPreview } from '../../../util/files'; import { getHtmlTextLength } from './helpers/getHtmlTextLength'; +import { selectChat, selectIsChatWithSelf } from '../../../global/selectors'; +import { selectCurrentLimit } from '../../../global/selectors/limits'; +import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; +import buildClassName from '../../../util/buildClassName'; +import { validateFiles } from '../../../util/files'; import usePrevious from '../../../hooks/usePrevious'; import useMentionTooltip from './hooks/useMentionTooltip'; @@ -29,12 +35,14 @@ import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; -import File from '../../common/File'; import MessageInput from './MessageInput'; import MentionTooltip from './MentionTooltip'; import EmojiTooltip from './EmojiTooltip.async'; import CustomSendMenu from './CustomSendMenu.async'; import CustomEmojiTooltip from './CustomEmojiTooltip.async'; +import AttachmentModalItem from './AttachmentModalItem'; +import DropdownMenu from '../../ui/DropdownMenu'; +import MenuItem from '../../ui/MenuItem'; import './AttachmentModal.scss'; @@ -45,28 +53,34 @@ export type OwnProps = { caption: string; canShowCustomSendMenu?: boolean; isReady?: boolean; + shouldSchedule?: boolean; + shouldSuggestCompression?: boolean; + onCaptionUpdate: (html: string) => void; + onSend: (sendCompressed: boolean, sendGrouped: boolean) => void; + onFileAppend: (files: File[]) => void; + onDelete: (attachmentIndex: number) => void; + onClear: () => void; + onSendSilent: (sendCompressed: boolean, sendGrouped: boolean) => void; + onSendScheduled: (sendCompressed: boolean, sendGrouped: boolean) => void; +}; + +type StateProps = { isChatWithSelf?: boolean; currentUserId?: string; groupChatMembers?: ApiChatMember[]; recentEmojis: string[]; baseEmojiKeywords?: Record; emojiKeywords?: Record; - shouldSchedule?: boolean; shouldSuggestCustomEmoji?: boolean; customEmojiForEmoji?: ApiSticker[]; captionLimit: number; - onCaptionUpdate: (html: string) => void; - onSend: () => void; - onFileAppend: (files: File[], isQuick: boolean) => void; - onClear: () => void; - onSendSilent: () => void; - onSendScheduled: () => void; + attachmentSettings: GlobalState['attachmentSettings']; }; const DROP_LEAVE_TIMEOUT_MS = 150; const CAPTION_SYMBOLS_LEFT_THRESHOLD = 100; -const AttachmentModal: FC = ({ +const AttachmentModal: FC = ({ chatId, threadId, attachments, @@ -83,24 +97,39 @@ const AttachmentModal: FC = ({ shouldSchedule, shouldSuggestCustomEmoji, customEmojiForEmoji, + attachmentSettings, + shouldSuggestCompression, onCaptionUpdate, onSend, onFileAppend, + onDelete, onClear, onSendSilent, onSendScheduled, }) => { - const { addRecentCustomEmoji, addRecentEmoji } = getActions(); + const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions(); + const lang = useLang(); const captionRef = useStateRef(caption); // eslint-disable-next-line no-null/no-null const mainButtonRef = useStateRef(null); const hideTimeoutRef = useRef(); const prevAttachments = usePrevious(attachments); const renderingAttachments = attachments.length ? attachments : prevAttachments; + + const [shouldSendCompressed, setShouldSendCompressed] = useState( + shouldSuggestCompression ?? attachmentSettings.shouldCompress, + ); + const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped); + const isOpen = Boolean(attachments.length); const [isHovered, markHovered, unmarkHovered] = useFlag(); - const isQuick = Boolean(renderingAttachments && renderingAttachments.every((a) => a.quick)); - const lang = useLang(); + + const [hasMedia, hasOnlyMedia] = useMemo(() => { + const onlyMedia = Boolean(renderingAttachments?.every((a) => a.quick || a.audio)); + if (onlyMedia) return [true, true]; + const oneMedia = Boolean(renderingAttachments?.some((a) => a.quick || a.audio)); + return [oneMedia, false]; + }, [renderingAttachments]); const { isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, @@ -142,6 +171,13 @@ const AttachmentModal: FC = ({ useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]); + useEffect(() => { + if (isOpen) { + setShouldSendCompressed(shouldSuggestCompression ?? attachmentSettings.shouldCompress); + setShouldSendGrouped(attachmentSettings.shouldSendGrouped); + } + }, [attachmentSettings, isOpen, shouldSuggestCompression]); + const { isContextMenuOpen: isCustomSendMenuOpen, handleContextMenu, @@ -149,15 +185,32 @@ const AttachmentModal: FC = ({ handleContextMenuHide, } = useContextMenuHandlers(mainButtonRef, !canShowCustomSendMenu || !isOpen); - const sendAttachments = useCallback(() => { + const sendAttachments = useCallback((isSilent?: boolean, shouldSendScheduled?: boolean) => { if (isOpen) { - if (shouldSchedule) { - onSendScheduled(); - } else { - onSend(); - } + const send = (shouldSchedule || shouldSendScheduled) ? onSendScheduled + : isSilent ? onSendSilent : onSend; + send(shouldSendCompressed, shouldSendGrouped); + updateAttachmentSettings({ + shouldCompress: shouldSendCompressed, + shouldSendGrouped, + }); } - }, [isOpen, onSendScheduled, onSend, shouldSchedule]); + }, [ + isOpen, shouldSchedule, onSendScheduled, onSend, updateAttachmentSettings, shouldSendCompressed, shouldSendGrouped, + onSendSilent, + ]); + + const handleSendSilent = useCallback(() => { + sendAttachments(true); + }, [sendAttachments]); + + const handleSendClick = useCallback(() => { + sendAttachments(); + }, [sendAttachments]); + + const handleScheduleClick = useCallback(() => { + sendAttachments(false, true); + }, [sendAttachments]); const handleDragLeave = (e: React.DragEvent) => { const { relatedTarget: toTarget, target: fromTarget } = e; @@ -187,11 +240,9 @@ const AttachmentModal: FC = ({ const files = await getFilesFromDataTransferItems(dataTransfer.items); if (files?.length) { - const newFiles = Array.from(files).filter((file) => !isQuick || hasPreview(file)); - - onFileAppend(newFiles, isQuick); + onFileAppend(files); } - }, [isQuick, onFileAppend, unmarkHovered]); + }, [onFileAppend, unmarkHovered]); function handleDragOver(e: React.MouseEvent) { e.preventDefault(); @@ -202,18 +253,55 @@ const AttachmentModal: FC = ({ } } + const handleFileSelect = useCallback((e: Event) => { + const { files } = e.target as HTMLInputElement; + const validatedFiles = validateFiles(files); + + if (validatedFiles?.length) { + onFileAppend(validatedFiles); + } + }, [onFileAppend]); + + const handleDocumentSelect = useCallback(() => { + openSystemFilesDialog('*', (e) => handleFileSelect(e)); + }, [handleFileSelect]); + + const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { + return ({ onTrigger, isOpen: isMenuOpen }) => ( + + ); + }, []); + const leftChars = useMemo(() => { const captionLeftBeforeLimit = captionLimit - getHtmlTextLength(caption); return captionLeftBeforeLimit <= CAPTION_SYMBOLS_LEFT_THRESHOLD ? captionLeftBeforeLimit : undefined; }, [caption, captionLimit]); + const isQuickGallery = shouldSendCompressed && hasOnlyMedia; + + const [areAllPhotos, areAllVideos, areAllAudios] = useMemo(() => { + if (!isQuickGallery || !renderingAttachments) return [false, false, false]; + const everyPhoto = renderingAttachments.every((a) => SUPPORTED_IMAGE_CONTENT_TYPES.has(a.mimeType)); + const everyVideo = renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType)); + const everyAudio = renderingAttachments.every((a) => SUPPORTED_AUDIO_CONTENT_TYPES.has(a.mimeType)); + return [everyPhoto, everyVideo, everyAudio]; + }, [renderingAttachments, isQuickGallery]); + if (!renderingAttachments) { return undefined; } - const areAllPhotos = renderingAttachments.every((a) => SUPPORTED_IMAGE_CONTENT_TYPES.has(a.mimeType)); - const areAllVideos = renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType)); - const areAllAudios = renderingAttachments.every((a) => SUPPORTED_AUDIO_CONTENT_TYPES.has(a.mimeType)); + const isMultiple = renderingAttachments.length > 1; let title = ''; if (areAllPhotos) { @@ -237,29 +325,42 @@ const AttachmentModal: FC = ({
{title}
-
- - {canShowCustomSendMenu && ( - + + {lang('Add')} + {hasMedia && ( + shouldSendCompressed ? ( + // eslint-disable-next-line react/jsx-no-bind + setShouldSendCompressed(false)}> + {lang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')} + + ) : ( + // eslint-disable-next-line react/jsx-no-bind + setShouldSendCompressed(true)}> + {isMultiple ? 'Send All as Media' : 'Send as Media'} + + ) )} -
+ {isMultiple && ( + shouldSendGrouped ? ( + setShouldSendGrouped(false)} + > + Ungroup All Media + + ) : ( + // eslint-disable-next-line react/jsx-no-bind + setShouldSendGrouped(true)}> + Group All Media + + ) + )} +
); } @@ -270,6 +371,7 @@ const AttachmentModal: FC = ({ onClose={onClear} header={renderHeader()} className={`AttachmentModal ${isHovered ? 'hovered' : ''}`} + noBackdropClose >
= ({ onDrop={handleFilesDrop} onDragOver={handleDragOver} onDragLeave={handleDragLeave} + onClick={unmarkHovered} data-attach-description={lang('Preview.Dragging.AddItems', 10)} data-dropzone > - {isQuick ? ( -
- {renderingAttachments.map((attachment) => ( - attachment.mimeType.startsWith('image/') - ? - :
- ) : ( -
- {renderingAttachments.map((attachment) => ( - - ))} -
- )} - +
+ {renderingAttachments.map((attachment, i) => ( + + ))} +
= ({ onCustomEmojiSelect={insertCustomEmoji} addRecentCustomEmoji={addRecentCustomEmoji} /> - +
+ +
+ + {canShowCustomSendMenu && ( + + )} +
+
); }; -export default memo(AttachmentModal); +export default memo(withGlobal( + (global, { chatId }): StateProps => { + const { + currentUserId, + recentEmojis, + customEmojis, + attachmentSettings, + } = global; + + const chat = selectChat(global, chatId); + const isChatWithSelf = selectIsChatWithSelf(global, chatId); + const { language, shouldSuggestCustomEmoji } = global.settings.byKey; + const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG]; + const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined; + + return { + isChatWithSelf, + currentUserId, + groupChatMembers: chat?.fullInfo?.members, + recentEmojis, + baseEmojiKeywords: baseEmojiKeywords?.keywords, + emojiKeywords: emojiKeywords?.keywords, + shouldSuggestCustomEmoji, + customEmojiForEmoji: customEmojis.forEmoji.stickers, + captionLimit: selectCurrentLimit(global, 'captionLength'), + attachmentSettings, + }; + }, +)(AttachmentModal)); diff --git a/src/components/middle/composer/AttachmentModalItem.module.scss b/src/components/middle/composer/AttachmentModalItem.module.scss new file mode 100644 index 000000000..b7a046bba --- /dev/null +++ b/src/components/middle/composer/AttachmentModalItem.module.scss @@ -0,0 +1,79 @@ +.root { + flex: 1 calc(50% - 0.5rem); + min-width: 0; + position: relative; + + display: flex; + align-items: center; +} + +.preview { + width: 100%; + height: 12rem; + object-fit: cover; + border-radius: var(--border-radius-default); +} + +.duration { + background: rgba(0, 0, 0, 0.25); + color: #fff; + font-size: 0.75rem; + position: absolute; + left: 0.1875rem; + top: 0.1875rem; + z-index: 1; + padding: 0 0.375rem; + border-radius: 0.75rem; + line-height: 1.125rem; + -webkit-user-select: none; + user-select: none; +} + +.single .preview { + height: auto; + max-height: 25rem; +} + +.no-grouping { + flex-basis: 100%; +} + +.file { + margin: 0.5rem; + flex-grow: 1; + min-width: 0; +} + +.overlay { + position: absolute; + bottom: 0.5rem; + right: 0.5rem; + background-color: rgba(0, 0, 0, 0.25); + border-radius: var(--border-radius-default); + + overflow: hidden; + + backdrop-filter: blur(10px); +} + +.action-item { + display: block; + padding: 0.3125rem; + color: white; + border-radius: var(--border-radius-messages-small); + + transition: 0.2s background-color ease-in-out; + + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.15); + } +} + +.delete-file { + font-size: 1.5rem; + color: var(--color-text-secondary); + + margin-right: 1rem; +} diff --git a/src/components/middle/composer/AttachmentModalItem.tsx b/src/components/middle/composer/AttachmentModalItem.tsx new file mode 100644 index 000000000..876592b6d --- /dev/null +++ b/src/components/middle/composer/AttachmentModalItem.tsx @@ -0,0 +1,118 @@ +import React, { memo, useMemo } from '../../../lib/teact/teact'; + +import type { FC } from '../../../lib/teact/teact'; +import type { ApiAttachment } from '../../../api/types'; + +import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../../config'; +import { getFileExtension } from '../../common/helpers/documentInfo'; +import buildClassName from '../../../util/buildClassName'; +import { formatMediaDuration } from '../../../util/dateFormat'; + +import File from '../../common/File'; + +import styles from './AttachmentModalItem.module.scss'; + +type OwnProps = { + attachment: ApiAttachment; + className?: string; + shouldDisplayCompressed?: boolean; + shouldDisplayGrouped?: boolean; + isSingle?: boolean; + index: number; + onDelete?: (index: number) => void; +}; + +const AttachmentModalItem: FC = ({ + attachment, + className, + isSingle, + shouldDisplayCompressed, + shouldDisplayGrouped, + index, + onDelete, +}) => { + const displayType = getDisplayType(attachment, shouldDisplayCompressed); + + const content = useMemo(() => { + switch (displayType) { + case 'image': + return ( + + ); + case 'video': + return ( + <> + {Boolean(attachment.quick?.duration) && ( +
{formatMediaDuration(attachment.quick!.duration)}
+ )} +