Attachment Modal: Redesign (#2253)
This commit is contained in:
parent
288ce625a3
commit
a9dd1221fb
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,10 @@ export interface ApiAttachment {
|
||||
performer?: string;
|
||||
};
|
||||
previewBlobUrl?: string;
|
||||
|
||||
shouldSendAsFile?: boolean;
|
||||
|
||||
uniqueId?: string;
|
||||
}
|
||||
|
||||
export interface ApiWallpaper {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -140,7 +140,7 @@ const File: FC<OwnProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-title" dir="auto">{renderText(name)}</div>
|
||||
<div className="file-title" dir="auto" title={name}>{renderText(name)}</div>
|
||||
<div className="file-subtitle" dir="auto">
|
||||
<span>
|
||||
{isTransferring && transferProgress ? `${Math.round(transferProgress * 100)}%` : sizeString}
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
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]);
|
||||
|
||||
|
||||
@ -188,7 +188,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
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);
|
||||
|
||||
@ -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<StateProps> = ({
|
||||
}, [shouldLoadFullChat, chatId, isReady, loadFullChat]);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
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)
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
}
|
||||
}, [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]);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string, string[]>;
|
||||
emojiKeywords?: Record<string, string[]>;
|
||||
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<OwnProps> = ({
|
||||
const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
attachments,
|
||||
@ -83,24 +97,39 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
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<HTMLButtonElement | null>(null);
|
||||
const hideTimeoutRef = useRef<number>();
|
||||
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<OwnProps> = ({
|
||||
|
||||
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<OwnProps> = ({
|
||||
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<HTMLElement>) => {
|
||||
const { relatedTarget: toTarget, target: fromTarget } = e;
|
||||
@ -187,11 +240,9 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
|
||||
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<HTMLDivElement, MouseEvent>) {
|
||||
e.preventDefault();
|
||||
@ -202,18 +253,55 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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 }) => (
|
||||
<Button
|
||||
round
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
className={isMenuOpen ? 'active' : ''}
|
||||
onClick={onTrigger}
|
||||
ariaLabel="More actions"
|
||||
>
|
||||
<i className="icon-more" />
|
||||
</Button>
|
||||
);
|
||||
}, []);
|
||||
|
||||
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<OwnProps> = ({
|
||||
<i className="icon-close" />
|
||||
</Button>
|
||||
<div className="modal-title">{title}</div>
|
||||
<div className="AttachmentModal--send-wrapper">
|
||||
<Button
|
||||
ref={mainButtonRef}
|
||||
color="primary"
|
||||
size="smaller"
|
||||
className="modal-action-button"
|
||||
onClick={sendAttachments}
|
||||
onContextMenu={canShowCustomSendMenu ? handleContextMenu : undefined}
|
||||
>
|
||||
{lang('Send')}
|
||||
</Button>
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
isOpen={isCustomSendMenuOpen}
|
||||
isOpenToBottom
|
||||
onSendSilent={!isChatWithSelf ? onSendSilent : undefined}
|
||||
onSendSchedule={onSendScheduled}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
isSavedMessages={isChatWithSelf}
|
||||
/>
|
||||
<DropdownMenu
|
||||
className="attachment-modal-more-menu"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="add" onClick={handleDocumentSelect}>{lang('Add')}</MenuItem>
|
||||
{hasMedia && (
|
||||
shouldSendCompressed ? (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="document" onClick={() => setShouldSendCompressed(false)}>
|
||||
{lang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="photo" onClick={() => setShouldSendCompressed(true)}>
|
||||
{isMultiple ? 'Send All as Media' : 'Send as Media'}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{isMultiple && (
|
||||
shouldSendGrouped ? (
|
||||
<MenuItem
|
||||
icon="grouped-disable"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setShouldSendGrouped(false)}
|
||||
>
|
||||
Ungroup All Media
|
||||
</MenuItem>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="grouped" onClick={() => setShouldSendGrouped(true)}>
|
||||
Group All Media
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -270,6 +371,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
onClose={onClear}
|
||||
header={renderHeader()}
|
||||
className={`AttachmentModal ${isHovered ? 'hovered' : ''}`}
|
||||
noBackdropClose
|
||||
>
|
||||
<div
|
||||
className="drop-target"
|
||||
@ -277,31 +379,24 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
onDrop={handleFilesDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={unmarkHovered}
|
||||
data-attach-description={lang('Preview.Dragging.AddItems', 10)}
|
||||
data-dropzone
|
||||
>
|
||||
{isQuick ? (
|
||||
<div className="media-wrapper custom-scroll">
|
||||
{renderingAttachments.map((attachment) => (
|
||||
attachment.mimeType.startsWith('image/')
|
||||
? <img src={attachment.blobUrl} alt="" />
|
||||
: <video src={attachment.blobUrl} autoPlay muted loop disablePictureInPicture />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="document-wrapper custom-scroll">
|
||||
{renderingAttachments.map((attachment) => (
|
||||
<File
|
||||
name={attachment.filename}
|
||||
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
||||
previewData={attachment.previewBlobUrl}
|
||||
size={attachment.size}
|
||||
smaller
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={buildClassName('attachments-wrapper', 'custom-scroll')}>
|
||||
{renderingAttachments.map((attachment, i) => (
|
||||
<AttachmentModalItem
|
||||
attachment={attachment}
|
||||
className="attachment-modal-item"
|
||||
shouldDisplayCompressed={shouldSendCompressed}
|
||||
shouldDisplayGrouped={shouldSendGrouped}
|
||||
isSingle={renderingAttachments.length === 1}
|
||||
index={i}
|
||||
key={attachment.uniqueId || i}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="attachment-caption-wrapper">
|
||||
<MentionTooltip
|
||||
isOpen={isMentionTooltipOpen}
|
||||
@ -325,23 +420,73 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
onCustomEmojiSelect={insertCustomEmoji}
|
||||
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||
/>
|
||||
<MessageInput
|
||||
id="caption-input-text"
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isAttachmentModalInput
|
||||
html={caption}
|
||||
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
||||
placeholder={lang('Caption')}
|
||||
onUpdate={onCaptionUpdate}
|
||||
onSend={sendAttachments}
|
||||
canAutoFocus={Boolean(isReady && attachments.length)}
|
||||
captionLimit={leftChars}
|
||||
/>
|
||||
<div className="attachment-caption">
|
||||
<MessageInput
|
||||
id="caption-input-text"
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isAttachmentModalInput
|
||||
html={caption}
|
||||
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
||||
placeholder={lang('AddCaption')}
|
||||
onUpdate={onCaptionUpdate}
|
||||
onSend={handleSendClick}
|
||||
canAutoFocus={Boolean(isReady && attachments.length)}
|
||||
captionLimit={leftChars}
|
||||
/>
|
||||
<div className="AttachmentModal--send-wrapper">
|
||||
<Button
|
||||
ref={mainButtonRef}
|
||||
className="AttachmentModal--send"
|
||||
onClick={handleSendClick}
|
||||
onContextMenu={canShowCustomSendMenu ? handleContextMenu : undefined}
|
||||
>
|
||||
{lang('Send')}
|
||||
</Button>
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
isOpen={isCustomSendMenuOpen}
|
||||
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||
onSendSchedule={handleScheduleClick}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
isSavedMessages={isChatWithSelf}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AttachmentModal);
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
118
src/components/middle/composer/AttachmentModalItem.tsx
Normal file
118
src/components/middle/composer/AttachmentModalItem.tsx
Normal file
@ -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<OwnProps> = ({
|
||||
attachment,
|
||||
className,
|
||||
isSingle,
|
||||
shouldDisplayCompressed,
|
||||
shouldDisplayGrouped,
|
||||
index,
|
||||
onDelete,
|
||||
}) => {
|
||||
const displayType = getDisplayType(attachment, shouldDisplayCompressed);
|
||||
|
||||
const content = useMemo(() => {
|
||||
switch (displayType) {
|
||||
case 'image':
|
||||
return (
|
||||
<img
|
||||
className={styles.preview}
|
||||
src={attachment.blobUrl}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
case 'video':
|
||||
return (
|
||||
<>
|
||||
{Boolean(attachment.quick?.duration) && (
|
||||
<div className={styles.duration}>{formatMediaDuration(attachment.quick!.duration)}</div>
|
||||
)}
|
||||
<video
|
||||
className={styles.preview}
|
||||
src={attachment.blobUrl}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
disablePictureInPicture
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<File
|
||||
className={styles.file}
|
||||
name={attachment.filename}
|
||||
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
||||
previewData={attachment.previewBlobUrl}
|
||||
size={attachment.size}
|
||||
smaller
|
||||
/>
|
||||
{onDelete && (
|
||||
<i
|
||||
className={buildClassName('icon-delete', styles.actionItem, styles.deleteFile)}
|
||||
onClick={() => onDelete(index)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [attachment, displayType, index, onDelete]);
|
||||
|
||||
const shouldSkipGrouping = displayType === 'file' || !shouldDisplayGrouped;
|
||||
const shouldRenderOverlay = displayType !== 'file' && onDelete;
|
||||
|
||||
const rootClassName = buildClassName(
|
||||
className, styles.root, isSingle && styles.single, shouldSkipGrouping && styles.noGrouping,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
{shouldRenderOverlay && (
|
||||
<div className={styles.overlay}>
|
||||
{onDelete && (
|
||||
<i className={buildClassName('icon-delete', styles.actionItem)} onClick={() => onDelete(index)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getDisplayType(attachment: ApiAttachment, shouldDisplayCompressed?: boolean) {
|
||||
if (shouldDisplayCompressed && attachment.quick) {
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'image';
|
||||
}
|
||||
if (SUPPORTED_VIDEO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'video';
|
||||
}
|
||||
}
|
||||
return 'file';
|
||||
}
|
||||
|
||||
export default memo(AttachmentModalItem);
|
||||
@ -456,6 +456,7 @@
|
||||
#message-input-text,
|
||||
#caption-input-text {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
.form-control {
|
||||
padding: calc((3.25rem - var(--composer-text-size, 1rem) * 1.375) / 2 - var(--border-width, 0) * 2)
|
||||
@ -558,8 +559,6 @@
|
||||
}
|
||||
|
||||
#message-input-text {
|
||||
flex-grow: 1;
|
||||
|
||||
.form-control {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.3125;
|
||||
@ -601,21 +600,21 @@
|
||||
}
|
||||
|
||||
#caption-input-text {
|
||||
border: 1px solid var(--color-borders-input);
|
||||
border-radius: var(--border-radius-default);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.input-scroller {
|
||||
min-height: 3.25rem;
|
||||
min-height: 3rem;
|
||||
max-height: 15rem;
|
||||
|
||||
margin-right: 0.5rem;
|
||||
margin-right: -5.5625rem;
|
||||
|
||||
&:has(.form-control:focus) {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.input-scroller-content {
|
||||
margin-right: 5rem;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
top: auto;
|
||||
bottom: 0.875rem;
|
||||
|
||||
@ -64,7 +64,7 @@ import {
|
||||
import { formatMediaDuration, formatVoiceRecordDuration } from '../../../util/dateFormat';
|
||||
import focusEditableElement from '../../../util/focusEditableElement';
|
||||
import parseMessageInput from '../../../util/parseMessageInput';
|
||||
import buildAttachment from './helpers/buildAttachment';
|
||||
import buildAttachment, { prepareAttachmentsToSend } from './helpers/buildAttachment';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { insertHtmlInSelection } from '../../../util/selection';
|
||||
import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection';
|
||||
@ -73,7 +73,6 @@ import windowSize from '../../../util/windowSize';
|
||||
import { isSelectionInsideInput } from './helpers/selection';
|
||||
import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { hasPreview } from '../../../util/files';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { buildCustomEmojiHtml } from './helpers/customEmoji';
|
||||
import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager';
|
||||
@ -98,6 +97,7 @@ import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
||||
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
||||
import useSchedule from '../../../hooks/useSchedule';
|
||||
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
|
||||
import useAttachmentModal from './hooks/useAttachmentModal';
|
||||
|
||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||
import Button from '../../ui/Button';
|
||||
@ -279,7 +279,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
loadSendAs,
|
||||
resetOpenChatWithDraft,
|
||||
callAttachBot,
|
||||
openLimitReachedModal,
|
||||
openPremiumModal,
|
||||
addRecentCustomEmoji,
|
||||
showNotification,
|
||||
@ -344,6 +343,21 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
|
||||
|
||||
const {
|
||||
shouldSuggestCompression,
|
||||
handleAppendFiles,
|
||||
handleFileSelect,
|
||||
onCaptionUpdate,
|
||||
handleClearAttachments,
|
||||
handleDeleteAttachment,
|
||||
handleSetAttachments,
|
||||
} = useAttachmentModal({
|
||||
attachments,
|
||||
setHtml,
|
||||
setAttachments,
|
||||
fileSizeLimit,
|
||||
});
|
||||
|
||||
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
|
||||
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
@ -352,19 +366,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
|
||||
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
||||
|
||||
const handleSetAttachments = useCallback(
|
||||
(newValue: ApiAttachment[] | ((current: ApiAttachment[]) => ApiAttachment[])) => {
|
||||
const newAttachments = typeof newValue === 'function' ? newValue(attachments) : newValue;
|
||||
if (newAttachments.some(({ size }) => size > fileSizeLimit)) {
|
||||
openLimitReachedModal({
|
||||
limit: 'uploadMaxFileparts',
|
||||
});
|
||||
} else {
|
||||
setAttachments(newAttachments);
|
||||
}
|
||||
}, [attachments, fileSizeLimit, openLimitReachedModal],
|
||||
);
|
||||
|
||||
const {
|
||||
startRecordingVoice,
|
||||
stopRecordingVoice,
|
||||
@ -612,20 +613,100 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [editingMessage, handleEditCancel]);
|
||||
|
||||
const handleFileSelect = useCallback(async (files: File[], isQuick: boolean) => {
|
||||
handleSetAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))));
|
||||
}, [handleSetAttachments]);
|
||||
const validateTextLength = useCallback((text: string, isAttachmentModal?: boolean) => {
|
||||
const maxLength = isAttachmentModal ? captionLimit : MESSAGE_MAX_LENGTH;
|
||||
if (text?.length > maxLength) {
|
||||
const extraLength = text.length - maxLength;
|
||||
showDialog({
|
||||
data: {
|
||||
message: 'MESSAGE_TOO_LONG_PLEASE_REMOVE_CHARACTERS',
|
||||
textParams: {
|
||||
'{EXTRA_CHARS_COUNT}': extraLength,
|
||||
'{PLURAL_S}': extraLength > 1 ? 's' : '',
|
||||
},
|
||||
hasErrorKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAppendFiles = useCallback(async (files: File[], isQuick: boolean) => {
|
||||
handleSetAttachments([
|
||||
...attachments,
|
||||
...await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))),
|
||||
]);
|
||||
}, [attachments, handleSetAttachments]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [captionLimit, showDialog]);
|
||||
|
||||
const handleClearAttachment = useCallback(() => {
|
||||
setAttachments(MEMO_EMPTY_ARRAY);
|
||||
}, []);
|
||||
const checkSlowMode = useCallback(() => {
|
||||
if (slowMode && !isAdmin) {
|
||||
// No need to subscribe on updates in `mapStateToProps`
|
||||
const { serverTimeOffset } = getGlobal();
|
||||
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
|
||||
const nowSeconds = getServerTime(serverTimeOffset);
|
||||
const secondsSinceLastMessage = lastMessageSendTimeSeconds.current
|
||||
&& Math.floor(nowSeconds - lastMessageSendTimeSeconds.current);
|
||||
const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds;
|
||||
|
||||
if (
|
||||
(secondsSinceLastMessage && secondsSinceLastMessage < slowMode.seconds)
|
||||
|| nextSendDateNotReached
|
||||
) {
|
||||
const secondsRemaining = nextSendDateNotReached
|
||||
? slowMode.nextSendDate! - nowSeconds
|
||||
: slowMode.seconds - secondsSinceLastMessage!;
|
||||
showDialog({
|
||||
data: {
|
||||
message: lang('SlowModeHint', formatMediaDuration(secondsRemaining)),
|
||||
isSlowMode: true,
|
||||
hasErrorKey: false,
|
||||
},
|
||||
});
|
||||
|
||||
messageInput?.blur();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, [isAdmin, lang, showDialog, slowMode]);
|
||||
|
||||
const handleSendAttachments = useCallback((
|
||||
sendCompressed: boolean, sendGrouped: boolean, isSilent?: boolean, scheduledAt?: number,
|
||||
) => {
|
||||
if (connectionState !== 'connectionStateReady') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { text, entities } = parseMessageInput(htmlRef.current!);
|
||||
if (!text && !attachments.length) {
|
||||
return;
|
||||
}
|
||||
if (!validateTextLength(text, true)) return;
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
sendMessage({
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetsOrder: true,
|
||||
attachments: prepareAttachmentsToSend(attachments, sendCompressed),
|
||||
shouldGroupMessages: sendGrouped,
|
||||
});
|
||||
|
||||
// No need to subscribe on updates in `mapStateToProps`
|
||||
const { serverTimeOffset } = getGlobal();
|
||||
|
||||
lastMessageSendTimeSeconds.current = getServerTime(serverTimeOffset);
|
||||
|
||||
clearDraft({ chatId, localOnly: true });
|
||||
|
||||
// Wait until message animation starts
|
||||
requestAnimationFrame(() => {
|
||||
resetComposer();
|
||||
});
|
||||
}, [
|
||||
attachments, chatId, checkSlowMode, clearDraft, htmlRef, resetComposer, sendMessage, validateTextLength,
|
||||
connectionState,
|
||||
]);
|
||||
|
||||
const handleSend = useCallback(async (isSilent = false, scheduledAt?: number) => {
|
||||
if (connectionState !== 'connectionStateReady') {
|
||||
@ -641,7 +722,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
currentAttachments = [await buildAttachment(
|
||||
VOICE_RECORDING_FILENAME,
|
||||
blob,
|
||||
false,
|
||||
{ voice: { duration, waveform } },
|
||||
)];
|
||||
}
|
||||
@ -649,64 +729,28 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const { text, entities } = parseMessageInput(htmlRef.current!);
|
||||
|
||||
if (!currentAttachments.length && !text && !isForwarding) {
|
||||
if (currentAttachments.length) {
|
||||
handleSendAttachments(false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text && !isForwarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to subscribe on updates in `mapStateToProps`
|
||||
const { serverTimeOffset } = getGlobal();
|
||||
|
||||
const maxLength = currentAttachments.length ? captionLimit : MESSAGE_MAX_LENGTH;
|
||||
if (text?.length > maxLength) {
|
||||
const extraLength = text.length - maxLength;
|
||||
showDialog({
|
||||
data: {
|
||||
message: 'MESSAGE_TOO_LONG_PLEASE_REMOVE_CHARACTERS',
|
||||
textParams: {
|
||||
'{EXTRA_CHARS_COUNT}': extraLength,
|
||||
'{PLURAL_S}': extraLength > 1 ? 's' : '',
|
||||
},
|
||||
hasErrorKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (!validateTextLength(text)) return;
|
||||
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
|
||||
if (currentAttachments.length || text) {
|
||||
if (slowMode && !isAdmin) {
|
||||
const nowSeconds = getServerTime(serverTimeOffset);
|
||||
const secondsSinceLastMessage = lastMessageSendTimeSeconds.current
|
||||
&& Math.floor(nowSeconds - lastMessageSendTimeSeconds.current);
|
||||
const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds;
|
||||
|
||||
if (
|
||||
(secondsSinceLastMessage && secondsSinceLastMessage < slowMode.seconds)
|
||||
|| nextSendDateNotReached
|
||||
) {
|
||||
const secondsRemaining = nextSendDateNotReached
|
||||
? slowMode.nextSendDate! - nowSeconds
|
||||
: slowMode.seconds - secondsSinceLastMessage!;
|
||||
showDialog({
|
||||
data: {
|
||||
message: lang('SlowModeHint', formatMediaDuration(secondsRemaining)),
|
||||
isSlowMode: true,
|
||||
hasErrorKey: false,
|
||||
},
|
||||
});
|
||||
|
||||
messageInput?.blur();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
sendMessage({
|
||||
text,
|
||||
entities,
|
||||
attachments: currentAttachments,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetsOrder: true,
|
||||
@ -733,8 +777,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
resetComposer();
|
||||
});
|
||||
}, [
|
||||
connectionState, attachments, activeVoiceRecording, isForwarding, clearDraft, chatId, captionLimit,
|
||||
resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, htmlRef,
|
||||
connectionState, attachments, activeVoiceRecording, htmlRef, isForwarding, validateTextLength, clearDraft,
|
||||
chatId, stopRecordingVoice, handleSendAttachments, checkSlowMode, sendMessage, forwardMessages, resetComposer,
|
||||
]);
|
||||
|
||||
const handleClickBotMenu = useCallback(() => {
|
||||
@ -778,13 +822,16 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (!args || Object.keys(restArgs).length === 0) {
|
||||
void handleSend(Boolean(isSilent), scheduledAt);
|
||||
} else if (args.sendCompressed !== undefined || args.sendGrouped !== undefined) {
|
||||
const { sendCompressed = false, sendGrouped = false } = args;
|
||||
void handleSendAttachments(sendCompressed, sendGrouped, isSilent, scheduledAt);
|
||||
} else {
|
||||
sendMessage({
|
||||
...args,
|
||||
scheduledAt,
|
||||
});
|
||||
}
|
||||
}, [handleSend, sendInlineBotResult, sendMessage]);
|
||||
}, [handleSendAttachments, handleSend, sendInlineBotResult, sendMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentToBeScheduled) {
|
||||
@ -807,8 +854,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (requestedDraftFiles?.length) {
|
||||
const isQuick = requestedDraftFiles.every((file) => hasPreview(file));
|
||||
handleFileSelect(requestedDraftFiles, isQuick);
|
||||
handleFileSelect(requestedDraftFiles);
|
||||
resetOpenChatWithDraft();
|
||||
}
|
||||
}, [handleFileSelect, requestedDraftFiles, resetOpenChatWithDraft]);
|
||||
@ -931,15 +977,18 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [closePollModal, handleMessageSchedule, requestCalendar, sendMessage, shouldSchedule]);
|
||||
|
||||
const handleSendSilent = useCallback(() => {
|
||||
const sendSilent = useCallback((additionalArgs?: ScheduledMessageArgs) => {
|
||||
if (shouldSchedule) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({ isSilent: true }, scheduledAt);
|
||||
handleMessageSchedule({ ...additionalArgs, isSilent: true }, scheduledAt);
|
||||
});
|
||||
} else if (additionalArgs && ('sendCompressed' in additionalArgs || 'sendGrouped' in additionalArgs)) {
|
||||
const { sendCompressed = false, sendGrouped = false } = additionalArgs;
|
||||
void handleSendAttachments(sendCompressed, sendGrouped, true);
|
||||
} else {
|
||||
void handleSend(true);
|
||||
}
|
||||
}, [handleMessageSchedule, handleSend, requestCalendar, shouldSchedule]);
|
||||
}, [handleMessageSchedule, handleSend, handleSendAttachments, requestCalendar, shouldSchedule]);
|
||||
|
||||
const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => {
|
||||
if (type === 'stickers') {
|
||||
@ -1087,12 +1136,28 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}, [handleMessageSchedule, requestCalendar]);
|
||||
|
||||
const handleSendSilent = useCallback(() => {
|
||||
sendSilent();
|
||||
}, [sendSilent]);
|
||||
|
||||
const handleSendScheduledAttachments = useCallback((sendCompressed: boolean, sendGrouped: boolean) => {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({ sendCompressed, sendGrouped }, scheduledAt);
|
||||
});
|
||||
}, [handleMessageSchedule, requestCalendar]);
|
||||
|
||||
const handleSendSilentAttachments = useCallback((sendCompressed: boolean, sendGrouped: boolean) => {
|
||||
sendSilent({ sendCompressed, sendGrouped });
|
||||
}, [sendSilent]);
|
||||
|
||||
const onSend = mainButtonState === MainButtonState.Edit
|
||||
? handleEditComplete
|
||||
: mainButtonState === MainButtonState.Schedule ? handleSendScheduled
|
||||
: handleSend;
|
||||
|
||||
const isBotMenuButtonCommands = botMenuButton && botMenuButton?.type === 'commands';
|
||||
const shouldDisplayBotCommands = isChatWithBot && isBotMenuButtonCommands && botCommands !== false
|
||||
&& !activeVoiceRecording && !editingMessage;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -1109,24 +1174,16 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
threadId={threadId}
|
||||
canShowCustomSendMenu={canShowCustomSendMenu}
|
||||
attachments={attachments}
|
||||
captionLimit={captionLimit}
|
||||
caption={attachments.length ? html : ''}
|
||||
groupChatMembers={groupChatMembers}
|
||||
currentUserId={currentUserId}
|
||||
recentEmojis={recentEmojis}
|
||||
isReady={isReady}
|
||||
isChatWithSelf={isChatWithSelf}
|
||||
onCaptionUpdate={setHtml}
|
||||
baseEmojiKeywords={baseEmojiKeywords}
|
||||
emojiKeywords={emojiKeywords}
|
||||
shouldSchedule={shouldSchedule}
|
||||
onSendSilent={handleSendSilent}
|
||||
onSend={handleSend}
|
||||
onSendScheduled={handleSendScheduled}
|
||||
shouldSuggestCompression={shouldSuggestCompression}
|
||||
onCaptionUpdate={onCaptionUpdate}
|
||||
onSendSilent={handleSendSilentAttachments}
|
||||
onSend={handleSendAttachments}
|
||||
onSendScheduled={handleSendScheduledAttachments}
|
||||
onFileAppend={handleAppendFiles}
|
||||
onClear={handleClearAttachment}
|
||||
shouldSuggestCustomEmoji={shouldSuggestCustomEmoji}
|
||||
customEmojiForEmoji={customEmojiForEmoji}
|
||||
onClear={handleClearAttachments}
|
||||
onDelete={handleDeleteAttachment}
|
||||
/>
|
||||
<PollModal
|
||||
isOpen={pollModal.isOpen}
|
||||
@ -1196,8 +1253,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isDisabled={Boolean(activeVoiceRecording)}
|
||||
/>
|
||||
)}
|
||||
{(isChatWithBot && isBotMenuButtonCommands
|
||||
&& botCommands !== false && !activeVoiceRecording && !editingMessage) && (
|
||||
{shouldDisplayBotCommands && (
|
||||
<ResponsiveHoverButton
|
||||
className={buildClassName('bot-commands', isBotCommandMenuOpen && 'activated')}
|
||||
round
|
||||
|
||||
@ -19,7 +19,7 @@ export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
withQuick?: boolean;
|
||||
onHide: NoneToVoidFunction;
|
||||
onFileSelect: (files: File[], isQuick: boolean) => void;
|
||||
onFileSelect: (files: File[], suggestCompression?: boolean) => void;
|
||||
};
|
||||
|
||||
export enum DropAreaState {
|
||||
@ -48,14 +48,14 @@ const DropArea: FC<OwnProps> = ({
|
||||
files = files.concat(Array.from(dt.files));
|
||||
} else if (dt.items && dt.items.length > 0) {
|
||||
const folderFiles = await getFilesFromDataTransferItems(dt.items);
|
||||
if (folderFiles.length) {
|
||||
if (folderFiles?.length) {
|
||||
files = files.concat(folderFiles);
|
||||
}
|
||||
}
|
||||
|
||||
onHide();
|
||||
onFileSelect(files, false);
|
||||
}, [onFileSelect, onHide]);
|
||||
onFileSelect(files, withQuick ? false : undefined);
|
||||
}, [onFileSelect, onHide, withQuick]);
|
||||
|
||||
const handleQuickFilesDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
const { dataTransfer: dt } = e;
|
||||
@ -89,6 +89,8 @@ const DropArea: FC<OwnProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const shouldRenderQuick = withQuick || prevWithQuick;
|
||||
|
||||
const className = buildClassName(
|
||||
'DropArea',
|
||||
transitionClassNames,
|
||||
@ -103,8 +105,8 @@ const DropArea: FC<OwnProps> = ({
|
||||
onDrop={onHide}
|
||||
onClick={onHide}
|
||||
>
|
||||
<DropTarget onFileSelect={handleFilesDrop} />
|
||||
{(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
|
||||
<DropTarget onFileSelect={handleFilesDrop} isGeneric={!shouldRenderQuick} />
|
||||
{shouldRenderQuick && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@ -8,10 +8,11 @@ import './DropTarget.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isQuick?: boolean;
|
||||
isGeneric?: boolean;
|
||||
onFileSelect: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
const DropTarget: FC<OwnProps> = ({ isQuick, onFileSelect }) => {
|
||||
const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
|
||||
const [isHovered, markHovered, unmarkHovered] = useFlag();
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -40,7 +41,7 @@ const DropTarget: FC<OwnProps> = ({ isQuick, onFileSelect }) => {
|
||||
<div className="target-content">
|
||||
<div className={`icon icon-${isQuick ? 'photo' : 'document'}`} />
|
||||
<div className="title">Drop files here to send them</div>
|
||||
<div className="description">{isQuick ? 'in a quick way' : 'without compression'}</div>
|
||||
{!isGeneric && <div className="description">{isQuick ? 'in a quick way' : 'without compression'}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -17,7 +17,7 @@ const MAX_QUICK_IMG_SIZE = 1280; // px
|
||||
const FILE_EXT_REGEX = /\.[^/.]+$/;
|
||||
|
||||
export default async function buildAttachment(
|
||||
filename: string, blob: Blob, isQuick: boolean, options?: Partial<ApiAttachment>,
|
||||
filename: string, blob: Blob, options?: Partial<ApiAttachment>,
|
||||
): Promise<ApiAttachment> {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const { type: mimeType, size } = blob;
|
||||
@ -26,28 +26,25 @@ export default async function buildAttachment(
|
||||
let previewBlobUrl;
|
||||
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) {
|
||||
if (isQuick) {
|
||||
const img = await preloadImage(blobUrl);
|
||||
const { width, height } = img;
|
||||
const shouldShrink = width > MAX_QUICK_IMG_SIZE || height > MAX_QUICK_IMG_SIZE;
|
||||
const img = await preloadImage(blobUrl);
|
||||
const { width, height } = img;
|
||||
const shouldShrink = Math.max(width, height) > MAX_QUICK_IMG_SIZE;
|
||||
|
||||
if (shouldShrink || mimeType !== 'image/jpeg') {
|
||||
const resizedUrl = await scaleImage(
|
||||
blobUrl, shouldShrink ? MAX_QUICK_IMG_SIZE / Math.max(width, height) : 1, 'image/jpeg',
|
||||
);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const newBlob = await fetchBlob(resizedUrl);
|
||||
return buildAttachment(filename, newBlob, true, options);
|
||||
}
|
||||
|
||||
if (mimeType === 'image/jpeg') {
|
||||
filename = filename.replace(FILE_EXT_REGEX, '.jpg');
|
||||
}
|
||||
|
||||
quick = { width, height };
|
||||
} else {
|
||||
previewBlobUrl = blobUrl;
|
||||
if (shouldShrink || mimeType !== 'image/jpeg') {
|
||||
const resizedUrl = await scaleImage(
|
||||
blobUrl, shouldShrink ? MAX_QUICK_IMG_SIZE / Math.max(width, height) : 1, 'image/jpeg',
|
||||
);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const newBlob = await fetchBlob(resizedUrl);
|
||||
return buildAttachment(filename, newBlob, options);
|
||||
}
|
||||
|
||||
if (mimeType === 'image/jpeg') {
|
||||
filename = filename.replace(FILE_EXT_REGEX, '.jpg');
|
||||
}
|
||||
|
||||
quick = { width, height };
|
||||
previewBlobUrl = blobUrl;
|
||||
} else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
|
||||
const { videoWidth: width, videoHeight: height, duration } = await preloadVideo(blobUrl);
|
||||
quick = { width, height, duration };
|
||||
@ -73,6 +70,13 @@ export default async function buildAttachment(
|
||||
quick,
|
||||
audio,
|
||||
previewBlobUrl,
|
||||
uniqueId: `${Date.now()}-${Math.random()}`,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareAttachmentsToSend(attachments: ApiAttachment[], shouldSendCompressed?: boolean) {
|
||||
return !shouldSendCompressed
|
||||
? attachments.map((attachment) => ({ ...attachment, shouldSendAsFile: true }))
|
||||
: attachments;
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { validateFiles } from '../../../../util/files';
|
||||
|
||||
export default async function getFilesFromDataTransferItems(dataTransferItems: DataTransferItemList) {
|
||||
const files: File[] = [];
|
||||
|
||||
@ -45,14 +47,5 @@ export default async function getFilesFromDataTransferItems(dataTransferItems: D
|
||||
|
||||
await Promise.all(entriesPromises);
|
||||
|
||||
return files.map(fixMovMime);
|
||||
}
|
||||
|
||||
// .mov MIME type not reported sometimes https://developer.mozilla.org/en-US/docs/Web/API/File/type#sect1
|
||||
function fixMovMime(file: File) {
|
||||
const ext = file.name.split('.').pop()!;
|
||||
if (!file.type && ext.toLowerCase() === 'mov') {
|
||||
return new File([file], file.name, { type: 'video/quicktime' });
|
||||
}
|
||||
return file;
|
||||
return validateFiles(files);
|
||||
}
|
||||
|
||||
66
src/components/middle/composer/hooks/useAttachmentModal.ts
Normal file
66
src/components/middle/composer/hooks/useAttachmentModal.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { useCallback, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiAttachment } from '../../../../api/types';
|
||||
|
||||
import buildAttachment from '../helpers/buildAttachment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
|
||||
export default function useAttachmentModal({
|
||||
attachments,
|
||||
fileSizeLimit,
|
||||
setHtml,
|
||||
setAttachments,
|
||||
}: {
|
||||
attachments: ApiAttachment[];
|
||||
fileSizeLimit: number;
|
||||
setHtml: (html: string) => void;
|
||||
setAttachments: (attachments: ApiAttachment[]) => void;
|
||||
}) {
|
||||
const { openLimitReachedModal } = getActions();
|
||||
const [shouldSuggestCompression, setShouldSuggestCompression] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const handleClearAttachments = useCallback(() => {
|
||||
setAttachments(MEMO_EMPTY_ARRAY);
|
||||
}, [setAttachments]);
|
||||
|
||||
const handleDeleteAttachment = useCallback((index: number) => {
|
||||
const newAttachments = attachments.filter((_, i) => i !== index);
|
||||
setAttachments(newAttachments?.length ? newAttachments : MEMO_EMPTY_ARRAY);
|
||||
}, [attachments, setAttachments]);
|
||||
|
||||
const handleSetAttachments = useCallback(
|
||||
(newValue: ApiAttachment[] | ((current: ApiAttachment[]) => ApiAttachment[])) => {
|
||||
const newAttachments = typeof newValue === 'function' ? newValue(attachments) : newValue;
|
||||
if (newAttachments.some(({ size }) => size > fileSizeLimit)) {
|
||||
openLimitReachedModal({
|
||||
limit: 'uploadMaxFileparts',
|
||||
});
|
||||
} else {
|
||||
setAttachments(newAttachments);
|
||||
}
|
||||
}, [attachments, fileSizeLimit, openLimitReachedModal, setAttachments],
|
||||
);
|
||||
|
||||
const handleAppendFiles = useCallback(async (files: File[]) => {
|
||||
handleSetAttachments([
|
||||
...attachments,
|
||||
...await Promise.all(files.map((file) => buildAttachment(file.name, file))),
|
||||
]);
|
||||
}, [attachments, handleSetAttachments]);
|
||||
|
||||
const handleFileSelect = useCallback(async (files: File[], suggestCompression?: boolean) => {
|
||||
handleSetAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file))));
|
||||
setShouldSuggestCompression(suggestCompression);
|
||||
}, [handleSetAttachments]);
|
||||
|
||||
return {
|
||||
shouldSuggestCompression,
|
||||
handleAppendFiles,
|
||||
handleFileSelect,
|
||||
onCaptionUpdate: setHtml,
|
||||
handleClearAttachments,
|
||||
handleDeleteAttachment,
|
||||
handleSetAttachments,
|
||||
};
|
||||
}
|
||||
@ -10,7 +10,6 @@ import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferIt
|
||||
import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
|
||||
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
||||
|
||||
const CLIPBOARD_ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||
const MAX_MESSAGE_LENGTH = 4096;
|
||||
|
||||
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
|
||||
@ -90,20 +89,20 @@ const useClipboardPaste = (
|
||||
}
|
||||
|
||||
const { items } = e.clipboardData;
|
||||
let files: File[] = [];
|
||||
let files: File[] | undefined = [];
|
||||
|
||||
e.preventDefault();
|
||||
if (items.length > 0) {
|
||||
files = await getFilesFromDataTransferItems(items);
|
||||
}
|
||||
|
||||
if (files.length === 0 && !pastedText) {
|
||||
if (!files?.length && !pastedText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length > 0 && !editedMessage) {
|
||||
if (files?.length && !editedMessage) {
|
||||
const newAttachments = await Promise.all(files.map((file) => {
|
||||
return buildAttachment(file.name, file, files.length === 1 && CLIPBOARD_ACCEPTED_TYPES.includes(file.type));
|
||||
return buildAttachment(file.name, file);
|
||||
}));
|
||||
setAttachments((attachments) => attachments.concat(newAttachments));
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
body.has-open-dialog &:not(.CustomSendMenu):not(.web-app-more-menu) .bubble {
|
||||
body.has-open-dialog &:not(.CustomSendMenu):not(.web-app-more-menu):not(.attachment-modal-more-menu) .bubble {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
|
||||
@ -194,4 +194,8 @@
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.dialog-button-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ type OwnProps = {
|
||||
header?: TeactNode;
|
||||
hasCloseButton?: boolean;
|
||||
noBackdrop?: boolean;
|
||||
noBackdropClose?: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: string;
|
||||
onClose: () => void;
|
||||
@ -48,6 +49,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
header,
|
||||
hasCloseButton,
|
||||
noBackdrop,
|
||||
noBackdropClose,
|
||||
children,
|
||||
style,
|
||||
onClose,
|
||||
@ -145,7 +147,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
role="dialog"
|
||||
>
|
||||
<div className="modal-container">
|
||||
<div className="modal-backdrop" onClick={onClose} />
|
||||
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
||||
<div className="modal-dialog" ref={dialogRef}>
|
||||
{renderHeader()}
|
||||
<div className="modal-content custom-scroll" style={style}>
|
||||
|
||||
@ -25,6 +25,9 @@ import {
|
||||
RE_TG_LINK,
|
||||
RE_TME_LINK,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
import { IS_IOS } from '../../../util/environment';
|
||||
import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
||||
@ -235,7 +238,7 @@ addActionHandler('sendMessage', (global, actions, payload) => {
|
||||
actions.clearWebPagePreview({ chatId, threadId, value: false });
|
||||
|
||||
const isSingle = !payload.attachments || payload.attachments.length <= 1;
|
||||
const isGrouped = !isSingle && payload.attachments && payload.attachments.length > 1;
|
||||
const isGrouped = !isSingle && payload.shouldGroupMessages;
|
||||
|
||||
if (isSingle) {
|
||||
const { attachments, ...restParams } = params;
|
||||
@ -247,27 +250,33 @@ addActionHandler('sendMessage', (global, actions, payload) => {
|
||||
const {
|
||||
text, entities, attachments, ...commonParams
|
||||
} = params;
|
||||
const groupedAttachments = split(attachments as ApiAttachment[], MAX_MEDIA_FILES_FOR_ALBUM);
|
||||
for (let i = 0; i < groupedAttachments.length; i++) {
|
||||
const [firstAttachment, ...restAttachments] = groupedAttachments[i];
|
||||
const groupedId = `${Date.now()}${i}`;
|
||||
const byType = splitAttachmentsByType(attachments);
|
||||
|
||||
sendMessage({
|
||||
...commonParams,
|
||||
text: i === 0 ? text : undefined,
|
||||
entities: i === 0 ? entities : undefined,
|
||||
attachment: firstAttachment,
|
||||
groupedId: restAttachments.length > 0 ? groupedId : undefined,
|
||||
});
|
||||
byType.forEach((group, groupIndex) => {
|
||||
const groupedAttachments = split(group as ApiAttachment[], MAX_MEDIA_FILES_FOR_ALBUM);
|
||||
for (let i = 0; i < groupedAttachments.length; i++) {
|
||||
const [firstAttachment, ...restAttachments] = groupedAttachments[i];
|
||||
const groupedId = `${Date.now()}${groupIndex}${i}`;
|
||||
|
||||
const isFirst = i === 0 && groupIndex === 0;
|
||||
|
||||
restAttachments.forEach((attachment: ApiAttachment) => {
|
||||
sendMessage({
|
||||
...commonParams,
|
||||
attachment,
|
||||
groupedId,
|
||||
text: isFirst ? text : undefined,
|
||||
entities: isFirst ? entities : undefined,
|
||||
attachment: firstAttachment,
|
||||
groupedId: restAttachments.length > 0 ? groupedId : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
restAttachments.forEach((attachment: ApiAttachment) => {
|
||||
sendMessage({
|
||||
...commonParams,
|
||||
attachment,
|
||||
groupedId,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const {
|
||||
text, entities, attachments, replyingTo, ...commonParams
|
||||
@ -1382,3 +1391,33 @@ function countSortedIds(ids: number[], from: number, to: number) {
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function splitAttachmentsByType(attachments: ApiAttachment[]) {
|
||||
return attachments.reduce((acc, attachment, index, arr) => {
|
||||
if (index === 0) {
|
||||
acc.push([attachment]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
const type = getAttachmentType(attachment);
|
||||
const previousType = getAttachmentType(arr[index - 1]);
|
||||
if (type === previousType) {
|
||||
acc[acc.length - 1].push(attachment);
|
||||
} else {
|
||||
acc.push([attachment]);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as ApiAttachment[][]);
|
||||
}
|
||||
|
||||
function getAttachmentType(attachment: ApiAttachment) {
|
||||
const {
|
||||
shouldSendAsFile, mimeType,
|
||||
} = attachment;
|
||||
if (shouldSendAsFile) return 'file';
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType) || SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) return 'media';
|
||||
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) return 'audio';
|
||||
if (attachment.voice) return 'voice';
|
||||
return 'file';
|
||||
}
|
||||
|
||||
@ -390,6 +390,20 @@ addActionHandler('requestConfetti', (global, actions, payload) => {
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('updateAttachmentSettings', (global, actions, payload) => {
|
||||
const {
|
||||
shouldCompress, shouldSendGrouped,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
attachmentSettings: {
|
||||
shouldCompress: shouldCompress ?? global.attachmentSettings.shouldCompress,
|
||||
shouldSendGrouped: shouldSendGrouped ?? global.attachmentSettings.shouldSendGrouped,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('openLimitReachedModal', (global, actions, payload) => {
|
||||
const { limit } = payload;
|
||||
|
||||
|
||||
@ -427,6 +427,7 @@ export function serializeGlobal(global: GlobalState) {
|
||||
'shouldShowContextMenuHint',
|
||||
'leftColumnWidth',
|
||||
'serviceNotifications',
|
||||
'attachmentSettings',
|
||||
]),
|
||||
audioPlayer: {
|
||||
volume: global.audioPlayer.volume,
|
||||
|
||||
@ -51,6 +51,11 @@ export const INITIAL_STATE: GlobalState = {
|
||||
byId: {},
|
||||
},
|
||||
|
||||
attachmentSettings: {
|
||||
shouldCompress: true,
|
||||
shouldSendGrouped: true,
|
||||
},
|
||||
|
||||
scheduledMessages: {
|
||||
byChatId: {},
|
||||
},
|
||||
|
||||
@ -229,6 +229,8 @@ export type GlobalState = {
|
||||
sticker?: ApiSticker;
|
||||
poll?: ApiNewPoll;
|
||||
isSilent?: boolean;
|
||||
sendGrouped?: boolean;
|
||||
sendCompressed?: boolean;
|
||||
};
|
||||
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
|
||||
};
|
||||
@ -250,6 +252,11 @@ export type GlobalState = {
|
||||
}>;
|
||||
};
|
||||
|
||||
attachmentSettings: {
|
||||
shouldCompress: boolean;
|
||||
shouldSendGrouped: boolean;
|
||||
};
|
||||
|
||||
chatFolders: {
|
||||
orderedIds?: number[];
|
||||
byId: Record<number, ApiChatFolder>;
|
||||
@ -1237,6 +1244,11 @@ export interface ActionPayloads {
|
||||
height: number;
|
||||
} | undefined;
|
||||
|
||||
updateAttachmentSettings: {
|
||||
shouldCompress?: boolean;
|
||||
shouldSendGrouped?: boolean;
|
||||
};
|
||||
|
||||
openUrl: {
|
||||
url: string;
|
||||
shouldSkipModal?: boolean;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"metadata": {
|
||||
"name": "Telegram T",
|
||||
"lastOpened": 0,
|
||||
"created": 1670592461603
|
||||
"created": 1674067291539
|
||||
},
|
||||
"iconSets": [
|
||||
{
|
||||
@ -2065,6 +2065,72 @@
|
||||
},
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"id": 101,
|
||||
"paths": [
|
||||
"M660.48 128c0 25.6-20.48 40.96-40.96 40.96-25.6 0-40.96-20.48-40.96-40.96 0-25.6 20.48-40.96 40.96-40.96s40.96 15.36 40.96 40.96zM619.52 363.52c30.72 0 51.2-25.6 51.2-51.2 0-30.72-25.6-51.2-51.2-51.2-30.72 0-51.2 25.6-51.2 51.2-5.12 25.6 20.48 51.2 51.2 51.2zM788.48 563.2c30.72 0 51.2-25.6 51.2-51.2 0-30.72-25.6-51.2-51.2-51.2-30.72 0-51.2 25.6-51.2 51.2 0 30.72 20.48 51.2 51.2 51.2zM983.040 512c0 25.6-20.48 40.96-40.96 40.96-25.6 0-40.96-20.48-40.96-40.96 0-25.6 20.48-40.96 40.96-40.96s40.96 15.36 40.96 40.96zM619.52 936.96c25.6 0 40.96-20.48 40.96-40.96s-20.48-40.96-40.96-40.96c-25.6 0-40.96 20.48-40.96 40.96s15.36 40.96 40.96 40.96zM788.48 353.28c25.6 0 40.96-20.48 40.96-40.96 0-25.6-20.48-40.96-40.96-40.96-25.6 0-40.96 20.48-40.96 40.96s20.48 40.96 40.96 40.96zM819.2 128c0 15.36-15.36 30.72-30.72 30.72s-30.72-15.36-30.72-30.72c0-15.36 15.36-30.72 30.72-30.72 20.48 0 30.72 15.36 30.72 30.72zM936.96 343.040c20.48 0 35.84-15.36 35.84-35.84s-15.36-30.72-30.72-30.72c-15.36 0-30.72 15.36-30.72 30.72s10.24 35.84 25.6 35.84zM972.8 727.040c0 15.36-15.36 30.72-30.72 30.72s-30.72-15.36-30.72-30.72c0-15.36 15.36-30.72 30.72-30.72 15.36-5.12 30.72 10.24 30.72 30.72zM936.96 148.48c10.24 0 20.48-10.24 20.48-20.48s-10.24-20.48-20.48-20.48c-10.24 0-20.48 10.24-20.48 20.48s10.24 20.48 20.48 20.48zM399.36 87.040v0 0c-46.080 0-87.040 0-117.76 0-25.6 5.12-56.32 10.24-81.92 20.48-30.72 15.36-56.32 40.96-76.8 71.68l61.44 61.44c0 0 0 0 0-5.12 10.24-25.6 30.72-46.080 56.32-56.32 10.24-5.12 25.6-10.24 51.2-10.24s61.44 0 107.52 0h66.56c25.6-0 46.080-15.36 46.080-40.96s-20.48-40.96-40.96-40.96h-71.68zM174.080 291.84c0 25.6 0 61.44 0 107.52v220.16c0 46.080 0 81.92 0 107.52 0 5.12 0 5.12 0 5.12l66.56-107.52c15.36-20.48 25.6-40.96 35.84-56.32s25.6-30.72 46.080-35.84c25.6-10.24 56.32-10.24 87.040 0 20.48 10.24 35.84 25.6 46.080 35.84 10.24 15.36 20.48 35.84 35.84 56.32v0l5.12 5.12c10.24 20.48 5.12 46.080-15.36 56.32s-46.080 5.12-56.32-15.36l-5.12-5.12c-15.36-25.6-25.6-40.96-30.72-51.2-15.36 5.12-15.36 0-20.48 0s-10.24 0-15.36 0c0 0-5.12 0-10.24 10.24-10.24 10.24-15.36 25.6-30.72 51.2l-92.16 148.48c5.12 5.12 15.36 10.24 20.48 15.36 10.24 5.12 25.6 10.24 51.2 10.24s61.44 0 107.52 0h66.56c25.6 0 40.96 20.48 40.96 40.96 0 25.6-20.48 40.96-40.96 40.96h-66.56c-46.080 0-81.92 0-112.64 0s-56.32-10.24-81.92-20.48c-40.96-20.48-71.68-51.2-92.16-92.16-15.36-25.6-20.48-51.2-20.48-81.92s0-66.56 0-112.64v0-225.28c0-46.080 0-81.92 0-112.64 0-25.6 5.12-46.080 10.24-66.56l71.68 71.68z",
|
||||
"M128 128l768 768z",
|
||||
"M896 936.96c-10.24 0-20.48-5.12-30.72-10.24l-768-768c-15.36-15.36-15.36-46.080 0-61.44s46.080-15.36 61.44 0l768 768c15.36 15.36 15.36 46.080 0 61.44-10.24 10.24-20.48 10.24-30.72 10.24z"
|
||||
],
|
||||
"attrs": [
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false,
|
||||
"grid": 24,
|
||||
"tags": [
|
||||
"spoiler-disable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"paths": [
|
||||
"M936.96 286.72c0-30.72-10.24-56.32-20.48-81.92-20.48-40.96-51.2-71.68-92.16-92.16-25.6-10.24-51.2-20.48-81.92-20.48s-66.56 0-112.64 0h-230.4c-46.080 0-81.92 0-112.64 0s-61.44 5.12-87.040 15.36c-40.96 20.48-71.68 51.2-92.16 92.16-10.24 25.6-15.36 56.32-20.48 87.040 0 30.72 0 66.56 0 112.64v225.28c0 46.080 0 81.92 0 112.64s10.24 56.32 20.48 81.92c20.48 40.96 51.2 71.68 92.16 92.16 25.6 10.24 51.2 20.48 81.92 20.48s66.56 0 112.64 0h225.28c46.080 0 81.92 0 112.64 0s56.32-10.24 81.92-20.48c40.96-20.48 71.68-51.2 92.16-92.16 10.24-25.6 20.48-51.2 20.48-81.92s0-66.56 0-112.64v-225.28c10.24-46.080 10.24-81.92 10.24-112.64zM552.96 168.96h66.56c46.080 0 81.92 0 107.52 0s40.96 5.12 51.2 10.24 46.080 30.72 56.32 56.32c5.12 10.24 10.24 25.6 10.24 51.2s0 61.44 0 107.52v66.56h-296.96v-291.84zM471.040 855.040h-71.68c-46.080 0-81.92 0-107.52 0s-40.96-5.12-51.2-10.24-46.080-30.72-56.32-56.32c-5.12-10.24-10.24-25.6-10.24-51.2s0-61.44 0-107.52v-230.4c0-46.080 0-81.92 0-107.52s5.12-40.96 10.24-51.2 30.72-46.080 56.32-56.32c10.24-5.12 25.6-10.24 51.2-10.24s61.44 0 107.52 0h66.56v680.96zM855.040 624.64c0 46.080 0 81.92 0 107.52s-5.12 40.96-10.24 51.2-30.72 46.080-56.32 56.32c-10.24 5.12-25.6 10.24-51.2 10.24s-61.44 0-107.52 0h-66.56v-296.96h296.96v71.68z"
|
||||
],
|
||||
"attrs": [
|
||||
{}
|
||||
],
|
||||
"grid": 24,
|
||||
"tags": [
|
||||
"grouped"
|
||||
],
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"paths": [
|
||||
"M732.16 849.92c-25.6 0-61.44 0-107.52 0h-66.56v-179.2l-87.040-87.040v261.12h-71.68c-46.080 0-81.92 0-107.52 0s-40.96-5.12-51.2-10.24-46.080-30.72-56.32-56.32c-5.12-10.24-10.24-25.6-10.24-51.2s0-61.44 0-107.52v-220.16c0-46.080 0-81.92 0-107.52l-71.68-71.68c-10.24 20.48-10.24 40.96-15.36 66.56 0 30.72 0 66.56 0 112.64v225.28c0 46.080 0 81.92 0 112.64s10.24 56.32 20.48 81.92c20.48 40.96 51.2 71.68 92.16 92.16 25.6 10.24 51.2 20.48 81.92 20.48s66.56 0 112.64 0h225.28c46.080 0 81.92 0 112.64 0 25.6 0 46.080-5.12 66.56-10.24l-66.56-71.68z",
|
||||
"M926.72 865.28l-25.6-20.48c5.12-5.12 10.24-15.36 10.24-20.48 10.24-25.6 20.48-51.2 20.48-81.92s0-66.56 0-112.64v-230.4c0-46.080 0-81.92 0-112.64s-10.24-56.32-20.48-81.92c-20.48-40.96-51.2-71.68-92.16-92.16-25.6-10.24-51.2-20.48-81.92-20.48s-66.56 0-112.64 0h-225.28c-46.080 0-81.92 0-112.64 0s-61.44 5.12-87.040 15.36c-5.12 5.12-10.24 10.24-20.48 15.36l-20.48-25.6c-15.36-15.36-46.080-15.36-61.44 0s-15.36 46.080 0 61.44l25.6 20.48 721.92 721.92 20.48 25.6c10.24 10.24 20.48 10.24 30.72 10.24s20.48-5.12 30.72-10.24c15.36-15.36 15.36-46.080 0-61.44zM471.040 409.6l-225.28-225.28c10.24-5.12 25.6-10.24 51.2-10.24s61.44 0 107.52 0h66.56v235.52zM855.040 624.64c0 46.080 0 81.92 0 107.52s-5.12 35.84-10.24 51.2l-230.4-230.4h240.64v71.68zM855.040 471.040h-296.96v-302.080h66.56c46.080 0 81.92 0 107.52 0s40.96 5.12 51.2 10.24 46.080 30.72 56.32 56.32c5.12 10.24 10.24 25.6 10.24 51.2s0 61.44 0 107.52v76.8z"
|
||||
],
|
||||
"attrs": [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"grid": 24,
|
||||
"tags": [
|
||||
"grouped-disable"
|
||||
],
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"paths": [
|
||||
"M661.335 128.001c0 23.564-19.103 42.666-42.67 42.666-23.562 0-42.665-19.103-42.665-42.666s19.103-42.667 42.665-42.667c23.567 0 42.67 19.103 42.67 42.667zM672 309.334c0 29.455-23.88 53.334-53.335 53.334s-53.33-23.878-53.33-53.334c0-29.455 23.875-53.333 53.33-53.333s53.335 23.878 53.335 53.333zM618.665 576c35.348 0 64-28.652 64-64 0-35.346-28.652-63.999-64-63.999-35.343 0-64 28.654-64 63.999 0 35.348 28.657 64 64 64zM789.335 565.335c29.455 0 53.33-23.88 53.33-53.335s-23.875-53.333-53.33-53.333c-29.455 0-53.335 23.878-53.335 53.333s23.88 53.335 53.335 53.335zM618.665 768c29.455 0 53.335-23.88 53.335-53.335s-23.88-53.33-53.335-53.33c-29.455 0-53.33 23.875-53.33 53.33s23.875 53.335 53.33 53.335zM981.335 512c0 23.562-19.103 42.665-42.67 42.665-23.562 0-42.665-19.103-42.665-42.665 0-23.564 19.103-42.666 42.665-42.666 23.567 0 42.67 19.103 42.67 42.666zM618.665 938.665c23.567 0 42.67-19.103 42.67-42.665s-19.103-42.665-42.67-42.665c-23.562 0-42.665 19.103-42.665 42.665s19.103 42.665 42.665 42.665zM789.335 768c23.562 0 42.665-19.103 42.665-42.665 0-23.567-19.103-42.67-42.665-42.67-23.567 0-42.67 19.103-42.67 42.67 0 23.562 19.103 42.665 42.67 42.665zM832 309.334c0 23.564-19.103 42.667-42.665 42.667-23.567 0-42.67-19.103-42.67-42.667s19.103-42.666 42.67-42.666c23.562 0 42.665 19.102 42.665 42.666zM789.335 160.001c17.669 0 32-14.327 32-32s-14.331-32-32-32c-17.674 0-32 14.327-32 32s14.326 32 32 32zM970.665 309.334c0 17.673-14.326 32-32 32-17.669 0-32-14.327-32-32s14.331-32 32-32c17.674 0 32 14.327 32 32zM938.665 757.335c17.674 0 32-14.331 32-32 0-17.674-14.326-32-32-32-17.669 0-32 14.326-32 32 0 17.669 14.331 32 32 32zM960 896c0 11.781-9.549 21.335-21.335 21.335-11.781 0-21.33-9.554-21.33-21.335s9.549-21.335 21.33-21.335c11.786 0 21.335 9.554 21.335 21.335zM938.665 149.334c11.786 0 21.335-9.551 21.335-21.333s-9.549-21.334-21.335-21.334c-11.781 0-21.33 9.551-21.33 21.334s9.549 21.333 21.33 21.333zM821.335 896c0 17.674-14.331 32-32 32-17.674 0-32-14.326-32-32s14.326-32 32-32c17.669 0 32 14.326 32 32zM399.241 85.334h70.093c23.564 0 42.666 19.102 42.666 42.666s-19.102 42.666-42.666 42.666h-68.266c-48.495 0-82.3 0.033-108.619 2.184-25.82 2.109-40.655 6.043-51.892 11.768-24.084 12.272-43.666 31.853-55.938 55.938-5.725 11.236-9.658 26.071-11.768 51.892-2.15 26.318-2.183 60.124-2.183 108.619v221.868c0 48.492 0.033 82.299 2.183 108.616 0.21 2.57 0.438 5.028 0.684 7.388l66.466-108.447c13.569-22.139 25.257-41.211 35.977-55.572 10.939-14.659 24.677-29.834 44.331-38.339 27.029-11.699 57.691-11.699 84.719 0 19.654 8.504 33.393 23.68 44.331 38.339 10.72 14.362 22.408 33.434 35.977 55.572l5.237 8.545c12.314 20.091 6.009 46.362-14.082 58.675s-46.36 6.006-58.674-14.085l-4.22-6.881c-14.874-24.269-24.528-39.941-32.624-50.79-6.471-8.668-9.547-10.839-9.997-11.131-5.31-2.248-11.304-2.248-16.614 0-0.45 0.292-3.527 2.463-9.997 11.131-8.096 10.849-17.75 26.522-32.624 50.79l-91.69 149.596c6.374 4.961 13.236 9.334 20.508 13.041 11.237 5.724 26.072 9.656 51.892 11.766 26.319 2.15 60.124 2.186 108.619 2.186h68.266c23.564 0 42.666 19.103 42.666 42.665s-19.102 42.665-42.666 42.665h-70.094c-46.241 0.005-83.537 0.005-113.74-2.463-31.097-2.545-58.412-7.91-83.683-20.787-40.141-20.454-72.777-53.089-93.23-93.23-12.876-25.272-18.244-52.588-20.785-83.681-2.467-30.203-2.467-67.502-2.467-113.746v-225.517c-0.001-46.241-0.001-83.538 2.467-113.741 2.541-31.098 7.909-58.413 20.785-83.683 20.453-40.141 53.089-72.777 93.23-93.23 25.271-12.876 52.586-18.245 83.683-20.785 30.203-2.468 67.5-2.467 113.741-2.467h0.001z"
|
||||
],
|
||||
"attrs": [
|
||||
{}
|
||||
],
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false,
|
||||
"grid": 24,
|
||||
"tags": [
|
||||
"spoiler"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 97,
|
||||
"paths": [
|
||||
@ -2453,19 +2519,19 @@
|
||||
},
|
||||
{
|
||||
"paths": [
|
||||
"M519.32 85.333c33.948 0 66.505 13.486 90.51 37.49l291.346 291.346c24.005 24.005 37.49 56.562 37.49 90.51v305.987c0 70.692-57.308 128-128 128h-597.333c-70.692 0-128-57.308-128-128v-597.333c0-70.692 57.308-128 128-128h305.987zM469.333 170.667h-256c-23.564 0-42.667 19.103-42.667 42.667v597.333c0 23.564 19.103 42.667 42.667 42.667h597.333c23.564 0 42.667-19.103 42.667-42.667v-256h-256c-68.168 0-123.89-53.287-127.783-120.479l-0.217-7.521v-256zM835.669 469.333l-281.003-281.003v238.336c0 21.881 16.471 39.915 37.691 42.38l4.976 0.287h238.336z"
|
||||
"M469.334 170.76v138.202c-0.001 34.345-0.001 62.691 1.886 85.783 1.959 23.983 6.165 46.029 16.716 66.737 16.362 32.113 42.471 58.22 74.584 74.583 20.71 10.552 42.752 14.756 66.739 16.717 23.086 1.884 51.43 1.884 85.775 1.884h138.209c0.072 7.752 0.092 16.87 0.092 27.776v40.494c0 48.492-0.036 82.299-2.186 108.616-2.109 25.82-6.042 40.658-11.766 51.891-12.273 24.084-31.852 43.668-55.941 55.941-11.233 5.724-26.071 9.656-51.891 11.766-26.317 2.15-60.124 2.186-108.616 2.186h-221.868c-48.495 0-82.3-0.036-108.619-2.186-25.82-2.109-40.655-6.042-51.892-11.766-24.084-12.273-43.666-31.857-55.938-55.941-5.725-11.233-9.658-26.071-11.767-51.891-2.15-26.317-2.184-60.124-2.184-108.616v-221.869c0-48.495 0.033-82.3 2.184-108.619 2.109-25.82 6.042-40.655 11.767-51.892 12.272-24.084 31.854-43.666 55.938-55.938 11.237-5.725 26.072-9.658 51.892-11.768 26.319-2.15 60.124-2.183 108.619-2.183h40.492c10.907 0 20.023 0.017 27.774 0.093zM832 469.333h-115.2c-36.547 0-61.394-0.033-80.594-1.602-18.708-1.528-28.273-4.299-34.944-7.698-16.056-8.181-29.112-21.236-37.294-37.292-3.4-6.673-6.17-16.239-7.7-34.945-1.567-19.204-1.603-44.048-1.603-80.595v-115.2c10.639 7.285 22.17 18.509 49.812 46.149l181.376 181.374c27.638 27.64 38.861 39.173 46.147 49.809zM533.775 91.228c-24.631-5.914-50.288-5.906-87.354-5.896l-47.18 0.001c-46.242-0.001-83.539-0.001-113.742 2.467-31.097 2.541-58.412 7.909-83.683 20.785-40.141 20.453-72.777 53.089-93.23 93.23-12.876 25.271-18.244 52.586-20.785 83.683-2.467 30.203-2.467 67.5-2.467 113.742v225.518c-0.001 46.244-0.001 83.538 2.467 113.741 2.541 31.099 7.909 58.414 20.785 83.686 20.453 40.141 53.089 72.776 93.23 93.23 25.271 12.877 52.586 18.243 83.683 20.782 30.203 2.468 67.5 2.468 113.741 2.468h225.523c46.239 0 83.538 0 113.741-2.468 31.094-2.54 58.409-7.905 83.681-20.782 40.141-20.454 72.776-53.089 93.23-93.23 12.877-25.272 18.243-52.588 20.787-83.686 2.468-30.198 2.463-67.497 2.463-113.736v-42.322l0.005-4.864c0.010-37.064 0.015-62.72-5.898-87.353-5.228-21.768-13.844-42.577-25.544-61.664-13.235-21.599-31.38-39.736-57.6-65.938l-188.247-188.25c-26.204-26.217-44.339-44.365-65.94-57.601-19.087-11.697-39.895-20.316-61.665-25.542z"
|
||||
],
|
||||
"attrs": [
|
||||
{}
|
||||
],
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false,
|
||||
"tags": [
|
||||
"document"
|
||||
],
|
||||
"defaultCode": 59679,
|
||||
"grid": 24,
|
||||
"id": 25
|
||||
"id": 25,
|
||||
"isMulticolor": false,
|
||||
"isMulticolor2": false
|
||||
},
|
||||
{
|
||||
"paths": [
|
||||
@ -3568,8 +3634,8 @@
|
||||
"metadata": {
|
||||
"name": "icomoon",
|
||||
"importSize": {
|
||||
"width": 24,
|
||||
"height": 24
|
||||
"width": 20,
|
||||
"height": 20
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
@ -3610,13 +3676,45 @@
|
||||
"showGrid": true
|
||||
},
|
||||
"selection": [
|
||||
{
|
||||
"order": 753,
|
||||
"id": 67,
|
||||
"name": "spoiler-disable",
|
||||
"prevSize": 32,
|
||||
"code": 59829,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 752,
|
||||
"id": 66,
|
||||
"name": "grouped",
|
||||
"prevSize": 32,
|
||||
"code": 59830,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 751,
|
||||
"id": 65,
|
||||
"name": "grouped-disable",
|
||||
"prevSize": 32,
|
||||
"code": 59831,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 749,
|
||||
"id": 64,
|
||||
"name": "spoiler",
|
||||
"prevSize": 32,
|
||||
"code": 59832,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 576,
|
||||
"id": 63,
|
||||
"name": "select",
|
||||
"prevSize": 32,
|
||||
"code": 59744,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 480,
|
||||
@ -3624,7 +3722,7 @@
|
||||
"name": "folder",
|
||||
"prevSize": 32,
|
||||
"code": 59667,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 481,
|
||||
@ -3632,7 +3730,7 @@
|
||||
"name": "bots",
|
||||
"prevSize": 32,
|
||||
"code": 59669,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 482,
|
||||
@ -3640,7 +3738,7 @@
|
||||
"name": "calendar",
|
||||
"prevSize": 32,
|
||||
"code": 59670,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 483,
|
||||
@ -3648,7 +3746,7 @@
|
||||
"name": "cloud-download",
|
||||
"prevSize": 32,
|
||||
"code": 59671,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 484,
|
||||
@ -3656,7 +3754,7 @@
|
||||
"name": "colorize",
|
||||
"prevSize": 32,
|
||||
"code": 59672,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 651,
|
||||
@ -3664,7 +3762,7 @@
|
||||
"name": "forward",
|
||||
"prevSize": 32,
|
||||
"code": 59687,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 650,
|
||||
@ -3672,7 +3770,7 @@
|
||||
"name": "reply",
|
||||
"prevSize": 32,
|
||||
"code": 59719,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 487,
|
||||
@ -3680,7 +3778,7 @@
|
||||
"name": "help",
|
||||
"prevSize": 32,
|
||||
"code": 59690,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 488,
|
||||
@ -3688,7 +3786,7 @@
|
||||
"name": "info",
|
||||
"prevSize": 32,
|
||||
"code": 59691,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 489,
|
||||
@ -3696,7 +3794,7 @@
|
||||
"name": "info-filled",
|
||||
"prevSize": 32,
|
||||
"code": 59675,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 490,
|
||||
@ -3704,7 +3802,7 @@
|
||||
"name": "delete-filled",
|
||||
"prevSize": 32,
|
||||
"code": 59676,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 491,
|
||||
@ -3712,7 +3810,7 @@
|
||||
"name": "delete",
|
||||
"prevSize": 32,
|
||||
"code": 59677,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 492,
|
||||
@ -3720,7 +3818,7 @@
|
||||
"name": "edit",
|
||||
"prevSize": 32,
|
||||
"code": 59683,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 493,
|
||||
@ -3728,7 +3826,7 @@
|
||||
"name": "new-chat-filled",
|
||||
"prevSize": 32,
|
||||
"code": 59705,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 494,
|
||||
@ -3736,7 +3834,7 @@
|
||||
"name": "send",
|
||||
"prevSize": 32,
|
||||
"code": 59722,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 495,
|
||||
@ -3744,7 +3842,7 @@
|
||||
"name": "send-outline",
|
||||
"prevSize": 32,
|
||||
"code": 59723,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 496,
|
||||
@ -3752,7 +3850,7 @@
|
||||
"name": "add-user-filled",
|
||||
"prevSize": 32,
|
||||
"code": 59652,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 497,
|
||||
@ -3760,7 +3858,7 @@
|
||||
"name": "add-user",
|
||||
"prevSize": 32,
|
||||
"code": 59653,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 498,
|
||||
@ -3768,7 +3866,7 @@
|
||||
"name": "delete-user",
|
||||
"prevSize": 32,
|
||||
"code": 59678,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 499,
|
||||
@ -3776,7 +3874,7 @@
|
||||
"name": "microphone",
|
||||
"prevSize": 32,
|
||||
"code": 59701,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 500,
|
||||
@ -3784,7 +3882,7 @@
|
||||
"name": "microphone-alt",
|
||||
"prevSize": 32,
|
||||
"code": 59707,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 501,
|
||||
@ -3792,7 +3890,7 @@
|
||||
"name": "poll",
|
||||
"prevSize": 32,
|
||||
"code": 59704,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 502,
|
||||
@ -3800,7 +3898,7 @@
|
||||
"name": "revote",
|
||||
"prevSize": 32,
|
||||
"code": 59706,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 503,
|
||||
@ -3808,15 +3906,15 @@
|
||||
"name": "photo",
|
||||
"prevSize": 32,
|
||||
"code": 59712,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 504,
|
||||
"order": 748,
|
||||
"id": 18,
|
||||
"name": "document",
|
||||
"prevSize": 32,
|
||||
"code": 59679,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 505,
|
||||
@ -3824,7 +3922,7 @@
|
||||
"name": "camera",
|
||||
"prevSize": 32,
|
||||
"code": 59662,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 506,
|
||||
@ -3832,7 +3930,7 @@
|
||||
"name": "camera-add",
|
||||
"prevSize": 32,
|
||||
"code": 59663,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 507,
|
||||
@ -3840,7 +3938,7 @@
|
||||
"name": "logout",
|
||||
"prevSize": 32,
|
||||
"code": 59698,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 508,
|
||||
@ -3848,7 +3946,7 @@
|
||||
"name": "saved-messages",
|
||||
"prevSize": 32,
|
||||
"code": 59720,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 509,
|
||||
@ -3856,7 +3954,7 @@
|
||||
"name": "settings",
|
||||
"prevSize": 32,
|
||||
"code": 59726,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 652,
|
||||
@ -3864,7 +3962,7 @@
|
||||
"name": "phone",
|
||||
"prevSize": 32,
|
||||
"code": 59711,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 653,
|
||||
@ -3872,7 +3970,7 @@
|
||||
"name": "attach",
|
||||
"prevSize": 32,
|
||||
"code": 59657,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 512,
|
||||
@ -3880,7 +3978,7 @@
|
||||
"name": "copy",
|
||||
"prevSize": 32,
|
||||
"code": 59674,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 513,
|
||||
@ -3888,7 +3986,7 @@
|
||||
"name": "channel",
|
||||
"prevSize": 32,
|
||||
"code": 59665,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 514,
|
||||
@ -3896,7 +3994,7 @@
|
||||
"name": "group",
|
||||
"prevSize": 32,
|
||||
"code": 59689,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 515,
|
||||
@ -3904,7 +4002,7 @@
|
||||
"name": "user",
|
||||
"prevSize": 32,
|
||||
"code": 59737,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 516,
|
||||
@ -3912,7 +4010,7 @@
|
||||
"name": "non-contacts",
|
||||
"prevSize": 32,
|
||||
"code": 59688,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 517,
|
||||
@ -3920,7 +4018,7 @@
|
||||
"name": "active-sessions",
|
||||
"prevSize": 32,
|
||||
"code": 59650,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 518,
|
||||
@ -3928,7 +4026,7 @@
|
||||
"name": "admin",
|
||||
"prevSize": 32,
|
||||
"code": 59654,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 519,
|
||||
@ -3936,7 +4034,7 @@
|
||||
"name": "download",
|
||||
"prevSize": 32,
|
||||
"code": 59681,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 520,
|
||||
@ -3944,7 +4042,7 @@
|
||||
"name": "location",
|
||||
"prevSize": 32,
|
||||
"code": 59696,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 521,
|
||||
@ -3952,7 +4050,7 @@
|
||||
"name": "stop",
|
||||
"prevSize": 32,
|
||||
"code": 59730,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 523,
|
||||
@ -3960,7 +4058,7 @@
|
||||
"name": "archive",
|
||||
"prevSize": 32,
|
||||
"code": 59656,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 524,
|
||||
@ -3968,7 +4066,7 @@
|
||||
"name": "unarchive",
|
||||
"prevSize": 32,
|
||||
"code": 59731,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 525,
|
||||
@ -3976,7 +4074,7 @@
|
||||
"name": "readchats",
|
||||
"prevSize": 32,
|
||||
"code": 59699,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 526,
|
||||
@ -3984,7 +4082,7 @@
|
||||
"name": "unread",
|
||||
"prevSize": 32,
|
||||
"code": 59735,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 654,
|
||||
@ -3992,7 +4090,7 @@
|
||||
"name": "message",
|
||||
"prevSize": 32,
|
||||
"code": 59700,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 659,
|
||||
@ -4000,7 +4098,7 @@
|
||||
"name": "lock",
|
||||
"prevSize": 32,
|
||||
"code": 59697,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 529,
|
||||
@ -4008,7 +4106,7 @@
|
||||
"name": "unlock",
|
||||
"prevSize": 32,
|
||||
"code": 59732,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 530,
|
||||
@ -4016,7 +4114,7 @@
|
||||
"name": "mute",
|
||||
"prevSize": 32,
|
||||
"code": 59703,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 531,
|
||||
@ -4024,7 +4122,7 @@
|
||||
"name": "unmute",
|
||||
"prevSize": 32,
|
||||
"code": 59733,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 532,
|
||||
@ -4032,7 +4130,7 @@
|
||||
"name": "pin",
|
||||
"prevSize": 32,
|
||||
"code": 59713,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 533,
|
||||
@ -4040,7 +4138,7 @@
|
||||
"name": "unpin",
|
||||
"prevSize": 32,
|
||||
"code": 59734,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 534,
|
||||
@ -4048,7 +4146,7 @@
|
||||
"name": "smallscreen",
|
||||
"prevSize": 32,
|
||||
"code": 59742,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 535,
|
||||
@ -4056,7 +4154,7 @@
|
||||
"name": "fullscreen",
|
||||
"prevSize": 32,
|
||||
"code": 59743,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 536,
|
||||
@ -4064,7 +4162,7 @@
|
||||
"name": "large-pause",
|
||||
"prevSize": 32,
|
||||
"code": 59694,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 537,
|
||||
@ -4072,7 +4170,7 @@
|
||||
"name": "large-play",
|
||||
"prevSize": 32,
|
||||
"code": 59695,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 538,
|
||||
@ -4080,7 +4178,7 @@
|
||||
"name": "pause",
|
||||
"prevSize": 32,
|
||||
"code": 59709,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 539,
|
||||
@ -4088,7 +4186,7 @@
|
||||
"name": "play",
|
||||
"prevSize": 32,
|
||||
"code": 59715,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 540,
|
||||
@ -4096,7 +4194,7 @@
|
||||
"name": "channelviews",
|
||||
"prevSize": 32,
|
||||
"code": 59666,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 541,
|
||||
@ -4104,7 +4202,7 @@
|
||||
"name": "message-succeeded",
|
||||
"prevSize": 32,
|
||||
"code": 59648,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 657,
|
||||
@ -4112,7 +4210,7 @@
|
||||
"name": "message-read",
|
||||
"prevSize": 32,
|
||||
"code": 59649,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 543,
|
||||
@ -4120,7 +4218,7 @@
|
||||
"name": "message-pending",
|
||||
"prevSize": 32,
|
||||
"code": 59724,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 544,
|
||||
@ -4128,7 +4226,7 @@
|
||||
"name": "message-failed",
|
||||
"prevSize": 32,
|
||||
"code": 59725,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 545,
|
||||
@ -4136,7 +4234,7 @@
|
||||
"name": "favorite",
|
||||
"prevSize": 32,
|
||||
"code": 59710,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 546,
|
||||
@ -4144,7 +4242,7 @@
|
||||
"name": "keyboard",
|
||||
"prevSize": 32,
|
||||
"code": 59716,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 547,
|
||||
@ -4152,7 +4250,7 @@
|
||||
"name": "delete-left",
|
||||
"prevSize": 32,
|
||||
"code": 59717,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 548,
|
||||
@ -4160,7 +4258,7 @@
|
||||
"name": "recent",
|
||||
"prevSize": 32,
|
||||
"code": 59718,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 549,
|
||||
@ -4168,7 +4266,7 @@
|
||||
"name": "gifs",
|
||||
"prevSize": 32,
|
||||
"code": 59727,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 550,
|
||||
@ -4176,7 +4274,7 @@
|
||||
"name": "stickers",
|
||||
"prevSize": 32,
|
||||
"code": 59739,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 551,
|
||||
@ -4184,7 +4282,7 @@
|
||||
"name": "smile",
|
||||
"prevSize": 32,
|
||||
"code": 59728,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 552,
|
||||
@ -4192,7 +4290,7 @@
|
||||
"name": "animals",
|
||||
"prevSize": 32,
|
||||
"code": 59655,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 553,
|
||||
@ -4200,7 +4298,7 @@
|
||||
"name": "eats",
|
||||
"prevSize": 32,
|
||||
"code": 59682,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 554,
|
||||
@ -4208,7 +4306,7 @@
|
||||
"name": "sport",
|
||||
"prevSize": 32,
|
||||
"code": 59729,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 555,
|
||||
@ -4216,7 +4314,7 @@
|
||||
"name": "car",
|
||||
"prevSize": 32,
|
||||
"code": 59664,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 556,
|
||||
@ -4224,7 +4322,7 @@
|
||||
"name": "lamp",
|
||||
"prevSize": 32,
|
||||
"code": 59692,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 557,
|
||||
@ -4232,7 +4330,7 @@
|
||||
"name": "language",
|
||||
"prevSize": 32,
|
||||
"code": 59693,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 558,
|
||||
@ -4240,7 +4338,7 @@
|
||||
"name": "flag",
|
||||
"prevSize": 32,
|
||||
"code": 59686,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 559,
|
||||
@ -4248,7 +4346,7 @@
|
||||
"name": "more",
|
||||
"prevSize": 32,
|
||||
"code": 59702,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 560,
|
||||
@ -4256,7 +4354,7 @@
|
||||
"name": "search",
|
||||
"prevSize": 32,
|
||||
"code": 59721,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 561,
|
||||
@ -4264,7 +4362,7 @@
|
||||
"name": "remove",
|
||||
"prevSize": 32,
|
||||
"code": 59740,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 562,
|
||||
@ -4272,7 +4370,7 @@
|
||||
"name": "add",
|
||||
"prevSize": 32,
|
||||
"code": 59651,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 563,
|
||||
@ -4280,7 +4378,7 @@
|
||||
"name": "check",
|
||||
"prevSize": 32,
|
||||
"code": 59668,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 564,
|
||||
@ -4288,7 +4386,7 @@
|
||||
"name": "close",
|
||||
"prevSize": 32,
|
||||
"code": 59673,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 610,
|
||||
@ -4296,7 +4394,7 @@
|
||||
"name": "arrow-left",
|
||||
"prevSize": 32,
|
||||
"code": 59661,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 566,
|
||||
@ -4304,7 +4402,7 @@
|
||||
"name": "arrow-right",
|
||||
"prevSize": 32,
|
||||
"code": 59708,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 730,
|
||||
@ -4312,7 +4410,7 @@
|
||||
"name": "down",
|
||||
"prevSize": 32,
|
||||
"code": 59680,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 568,
|
||||
@ -4320,7 +4418,7 @@
|
||||
"name": "up",
|
||||
"prevSize": 32,
|
||||
"code": 59736,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 569,
|
||||
@ -4328,7 +4426,7 @@
|
||||
"name": "eye-closed",
|
||||
"prevSize": 32,
|
||||
"code": 59685,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 570,
|
||||
@ -4336,7 +4434,7 @@
|
||||
"name": "eye",
|
||||
"prevSize": 32,
|
||||
"code": 59684,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 571,
|
||||
@ -4344,7 +4442,7 @@
|
||||
"name": "muted",
|
||||
"prevSize": 32,
|
||||
"code": 59741,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 572,
|
||||
@ -4352,7 +4450,7 @@
|
||||
"name": "avatar-archived-chats",
|
||||
"prevSize": 32,
|
||||
"code": 59658,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 573,
|
||||
@ -4360,7 +4458,7 @@
|
||||
"name": "avatar-deleted-account",
|
||||
"prevSize": 32,
|
||||
"code": 59659,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 747,
|
||||
@ -4368,7 +4466,7 @@
|
||||
"name": "avatar-saved-messages",
|
||||
"prevSize": 32,
|
||||
"code": 59660,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"order": 575,
|
||||
@ -4376,7 +4474,7 @@
|
||||
"name": "pinned-chat",
|
||||
"prevSize": 32,
|
||||
"code": 59714,
|
||||
"tempChar": ""
|
||||
"tempChar": ""
|
||||
}
|
||||
],
|
||||
"prevSize": 32,
|
||||
|
||||
@ -288,6 +288,18 @@
|
||||
.icon-zoom-out:before {
|
||||
content: "\e975";
|
||||
}
|
||||
.icon-spoiler-disable:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
.icon-grouped:before {
|
||||
content: "\e9b6";
|
||||
}
|
||||
.icon-grouped-disable:before {
|
||||
content: "\e9b7";
|
||||
}
|
||||
.icon-spoiler:before {
|
||||
content: "\e9b8";
|
||||
}
|
||||
.icon-select:before {
|
||||
content: "\e960";
|
||||
}
|
||||
|
||||
@ -127,3 +127,19 @@ export function imgToCanvas(img: HTMLImageElement) {
|
||||
export function hasPreview(file: File) {
|
||||
return CONTENT_TYPES_WITH_PREVIEW.has(file.type);
|
||||
}
|
||||
|
||||
export function validateFiles(files: File[] | FileList | null): File[] | undefined {
|
||||
if (!files?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return Array.from(files).map(fixMovMime).filter((file) => file.size);
|
||||
}
|
||||
|
||||
// .mov MIME type not reported sometimes https://developer.mozilla.org/en-US/docs/Web/API/File/type#sect1
|
||||
function fixMovMime(file: File) {
|
||||
const ext = file.name.split('.').pop()!;
|
||||
if (!file.type && ext.toLowerCase() === 'mov') {
|
||||
return new File([file], file.name, { type: 'video/quicktime' });
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { DEBUG, DEBUG_MORE, IS_TEST } from '../config';
|
||||
import { getActions } from '../global';
|
||||
import { formatShareText } from './deeplink';
|
||||
import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||
import { validateFiles } from './files';
|
||||
import { notifyClientReady, playNotifySoundDebounced } from './notifications';
|
||||
|
||||
type WorkerAction = {
|
||||
@ -30,7 +31,7 @@ function handleWorkerMessage(e: MessageEvent) {
|
||||
case 'share':
|
||||
dispatch.openChatWithDraft({
|
||||
text: formatShareText(payload.url, payload.text, payload.title),
|
||||
files: payload.files,
|
||||
files: validateFiles(payload.files),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user