Message Media: Editing message media (#4202)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com> Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
parent
64799a8818
commit
c3cc8b634f
@ -917,7 +917,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
|
||||
};
|
||||
}
|
||||
|
||||
function buildUploadingMedia(
|
||||
export function buildUploadingMedia(
|
||||
attachment: ApiAttachment,
|
||||
): MediaContent {
|
||||
const {
|
||||
|
||||
@ -4,13 +4,11 @@ import type {
|
||||
ApiEmojiInteraction, ApiSticker, ApiStickerSet, ApiStickerSetInfo, GramJsEmojiInteraction,
|
||||
} from '../../types';
|
||||
|
||||
import { LOTTIE_STICKER_MIME_TYPE, VIDEO_STICKER_MIME_TYPE } from '../../../config';
|
||||
import { compact } from '../../../util/iteratees';
|
||||
import localDb from '../localDb';
|
||||
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||
|
||||
const LOTTIE_STICKER_MIME_TYPE = 'application/x-tgsticker';
|
||||
const VIDEO_STICKER_MIME_TYPE = 'video/webm';
|
||||
|
||||
export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPremium?: boolean): ApiSticker | undefined {
|
||||
if (document instanceof GramJs.DocumentEmpty) {
|
||||
return undefined;
|
||||
|
||||
@ -24,35 +24,36 @@ import type {
|
||||
MediaContent,
|
||||
OnApiUpdate,
|
||||
} from '../../types';
|
||||
import {
|
||||
MAIN_THREAD_ID,
|
||||
MESSAGE_DELETED,
|
||||
} from '../../types';
|
||||
import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../types';
|
||||
|
||||
import {
|
||||
ALL_FOLDER_ID,
|
||||
API_GENERAL_ID_LIMIT,
|
||||
DEBUG, GIF_MIME_TYPE, MAX_INT_32, MENTION_UNREAD_SLICE,
|
||||
PINNED_MESSAGES_LIMIT, REACTION_UNREAD_SLICE,
|
||||
DEBUG,
|
||||
GIF_MIME_TYPE,
|
||||
MAX_INT_32,
|
||||
MENTION_UNREAD_SLICE,
|
||||
PINNED_MESSAGES_LIMIT,
|
||||
REACTION_UNREAD_SLICE,
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
||||
import { fetchFile } from '../../../util/files';
|
||||
import { compact, split } from '../../../util/iteratees';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { interpolateArray } from '../../../util/waveform';
|
||||
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
||||
import { buildApiFormattedText } from '../apiBuilders/common';
|
||||
import {
|
||||
buildMessageMediaContent, buildMessageTextContent, buildWebPage,
|
||||
} from '../apiBuilders/messageContent';
|
||||
import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent';
|
||||
import {
|
||||
buildApiMessage,
|
||||
buildApiSponsoredMessage,
|
||||
buildApiThreadInfo,
|
||||
buildLocalForwardedMessage,
|
||||
buildLocalMessage,
|
||||
buildUploadingMedia,
|
||||
} from '../apiBuilders/messages';
|
||||
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
@ -560,31 +561,34 @@ export async function editMessage({
|
||||
message,
|
||||
text,
|
||||
entities,
|
||||
attachment,
|
||||
noWebPage,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
message: ApiMessage;
|
||||
text: string;
|
||||
entities?: ApiMessageEntity[];
|
||||
attachment?: ApiAttachment;
|
||||
noWebPage?: boolean;
|
||||
}) {
|
||||
}, onProgress?: ApiOnProgress) {
|
||||
const isScheduled = message.date * 1000 > Date.now() + getServerTimeOffset() * 1000;
|
||||
let messageUpdate: Partial<ApiMessage> = {
|
||||
content: {
|
||||
...message.content,
|
||||
...(text && {
|
||||
text: {
|
||||
text,
|
||||
entities,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
const media = attachment && buildUploadingMedia(attachment);
|
||||
|
||||
const newContent = {
|
||||
...(media || message.content),
|
||||
...(text && {
|
||||
text: {
|
||||
text,
|
||||
entities,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const emojiOnlyCount = getEmojiOnlyCountForMessage(messageUpdate.content!, messageUpdate.groupedId);
|
||||
messageUpdate = {
|
||||
...messageUpdate,
|
||||
emojiOnlyCount,
|
||||
const messageUpdate: Partial<ApiMessage> = {
|
||||
...message,
|
||||
content: newContent,
|
||||
emojiOnlyCount: getEmojiOnlyCountForMessage(newContent, message.groupedId),
|
||||
};
|
||||
|
||||
onUpdate({
|
||||
@ -594,16 +598,47 @@ export async function editMessage({
|
||||
message: messageUpdate,
|
||||
});
|
||||
|
||||
const mtpEntities = entities && entities.map(buildMtpMessageEntity);
|
||||
try {
|
||||
let mediaUpdate: GramJs.TypeInputMedia | undefined;
|
||||
if (attachment) {
|
||||
mediaUpdate = await uploadMedia(message, attachment, onProgress!);
|
||||
}
|
||||
|
||||
await invokeRequest(new GramJs.messages.EditMessage({
|
||||
message: text || '',
|
||||
entities: mtpEntities,
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: message.id,
|
||||
...(isScheduled && { scheduleDate: message.date }),
|
||||
...(noWebPage && { noWebpage: noWebPage }),
|
||||
}));
|
||||
const mtpEntities = entities && entities.map(buildMtpMessageEntity);
|
||||
|
||||
await invokeRequest(new GramJs.messages.EditMessage({
|
||||
message: text || '',
|
||||
entities: mtpEntities,
|
||||
media: mediaUpdate,
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: message.id,
|
||||
...(isScheduled && { scheduleDate: message.date }),
|
||||
...(noWebPage && { noWebpage: noWebPage }),
|
||||
}), { shouldThrow: true });
|
||||
} catch (err) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
const { message: messageErr } = err as Error;
|
||||
|
||||
onUpdate({
|
||||
'@type': 'error',
|
||||
error: {
|
||||
message: messageErr,
|
||||
hasErrorKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Rollback changes
|
||||
onUpdate({
|
||||
'@type': isScheduled ? 'updateScheduledMessage' : 'updateMessage',
|
||||
id: message.id,
|
||||
chatId: chat.id,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function rescheduleMessage({
|
||||
@ -622,7 +657,7 @@ export async function rescheduleMessage({
|
||||
}));
|
||||
}
|
||||
|
||||
async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) {
|
||||
async function uploadMedia(message: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) {
|
||||
const {
|
||||
filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, shouldSendAsFile, shouldSendAsSpoiler, ttlSeconds,
|
||||
} = attachment;
|
||||
@ -631,7 +666,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment,
|
||||
if (onProgress.isCanceled) {
|
||||
patchedOnProgress.isCanceled = true;
|
||||
} else {
|
||||
onProgress(progress, localMessage.id);
|
||||
onProgress(progress, getMessageKey(message));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -205,6 +205,7 @@ const Audio: FC<OwnProps> = ({
|
||||
message,
|
||||
uploadProgress || downloadProgress,
|
||||
isLoadingForPlaying || isDownloading,
|
||||
uploadProgress !== undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@ -49,6 +49,7 @@ import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterd
|
||||
import {
|
||||
getAllowedAttachmentOptions,
|
||||
getStoryKey,
|
||||
hasReplaceableMedia,
|
||||
isChatAdmin,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
@ -373,6 +374,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
openStoryReactionPicker,
|
||||
closeReactionPicker,
|
||||
sendStoryReaction,
|
||||
editMessage,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
@ -401,6 +403,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const [isInputHasFocus, markInputHasFocus, unmarkInputHasFocus] = useFlag();
|
||||
const [isAttachMenuOpen, onAttachMenuOpen, onAttachMenuClose] = useFlag();
|
||||
|
||||
const canMediaBeReplaced = editingMessage && hasReplaceableMedia(editingMessage);
|
||||
|
||||
const isSentStoryReactionHeart = sentStoryReaction && 'emoticon' in sentStoryReaction
|
||||
? sentStoryReaction.emoticon === HEART_REACTION.emoticon : false;
|
||||
|
||||
@ -532,6 +536,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
canSendPhotos,
|
||||
canSendDocuments,
|
||||
insertNextText,
|
||||
editedMessage: editingMessage,
|
||||
});
|
||||
|
||||
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
|
||||
@ -891,16 +896,25 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
if (!validateTextLength(text, true)) return;
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed),
|
||||
shouldGroupMessages: sendGrouped,
|
||||
});
|
||||
if (editingMessage) {
|
||||
editMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
entities,
|
||||
attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed),
|
||||
});
|
||||
} else {
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed),
|
||||
shouldGroupMessages: sendGrouped,
|
||||
});
|
||||
}
|
||||
|
||||
lastMessageSendTimeSeconds.current = getServerTime();
|
||||
|
||||
@ -1495,6 +1509,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
withQuick={dropAreaState === DropAreaState.QuickFile || prevDropAreaState === DropAreaState.QuickFile}
|
||||
onHide={onDropHide!}
|
||||
onFileSelect={handleFileSelect}
|
||||
editingMessage={editingMessage}
|
||||
/>
|
||||
)}
|
||||
{shouldRenderReactionSelector && (
|
||||
@ -1535,6 +1550,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onCustomEmojiSelect={handleCustomEmojiSelectAttachmentModal}
|
||||
onRemoveSymbol={removeSymbolAttachmentModal}
|
||||
onEmojiSelect={insertTextAndUpdateCursorAttachmentModal}
|
||||
editingMessage={editingMessage}
|
||||
/>
|
||||
<PollModal
|
||||
isOpen={pollModal.isOpen}
|
||||
@ -1763,7 +1779,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
<AttachMenu
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isButtonVisible={!activeVoiceRecording && !editingMessage}
|
||||
editingMessage={editingMessage}
|
||||
hasReplaceableMedia={canMediaBeReplaced}
|
||||
isButtonVisible={!activeVoiceRecording}
|
||||
canAttachMedia={canAttachMedia}
|
||||
canAttachPolls={canAttachPolls}
|
||||
canSendPhotos={canSendPhotos}
|
||||
|
||||
@ -7,10 +7,7 @@ import { getActions } from '../../global';
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../config';
|
||||
import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../config';
|
||||
import {
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
@ -109,7 +106,12 @@ const Document: FC<OwnProps> = ({
|
||||
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(message, uploadProgress || downloadProgress, shouldDownload && !isLoaded);
|
||||
} = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || downloadProgress,
|
||||
shouldDownload && !isLoaded,
|
||||
uploadProgress !== undefined,
|
||||
);
|
||||
|
||||
const hasPreview = getDocumentHasPreview(document);
|
||||
const thumbDataUri = hasPreview ? getMessageMediaThumbDataUri(message) : undefined;
|
||||
|
||||
@ -4,13 +4,13 @@ import type { TextPart } from '../../../types';
|
||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||
|
||||
import {
|
||||
getMessageKey,
|
||||
getMessageSummaryDescription,
|
||||
getMessageSummaryEmoji,
|
||||
getMessageSummaryText,
|
||||
getMessageText,
|
||||
TRUNCATED_SUMMARY_LENGTH,
|
||||
} from '../../../global/helpers';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import trimText from '../../../util/trimText';
|
||||
import renderText from './renderText';
|
||||
import { renderTextWithEntities } from './renderTextWithEntities';
|
||||
|
||||
@ -25,7 +25,6 @@ import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getChatTitle,
|
||||
getIsSavedDialog,
|
||||
getMessageKey,
|
||||
getSenderTitle,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
@ -51,6 +50,7 @@ import {
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import cycleRestrict from '../../util/cycleRestrict';
|
||||
import { getMessageKey } from '../../util/messageKey';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
useMemo,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAttachMenuPeerType } from '../../../api/types';
|
||||
import type { ApiAttachMenuPeerType, ApiMessage } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { ISettings, ThreadId } from '../../../types';
|
||||
|
||||
@ -13,6 +13,13 @@ import {
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
import {
|
||||
getMessageAudio, getMessageDocument,
|
||||
getMessagePhoto,
|
||||
getMessageVideo, getMessageVoice,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
} from '../../../global/helpers';
|
||||
import { getDebugLogs } from '../../../util/debugConsole';
|
||||
import { validateFiles } from '../../../util/files';
|
||||
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
|
||||
@ -23,6 +30,7 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMouseInside from '../../../hooks/useMouseInside';
|
||||
|
||||
import Icon from '../../common/Icon';
|
||||
import Menu from '../../ui/Menu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||
@ -49,6 +57,8 @@ export type OwnProps = {
|
||||
onPollCreate: NoneToVoidFunction;
|
||||
onMenuOpen: NoneToVoidFunction;
|
||||
onMenuClose: NoneToVoidFunction;
|
||||
hasReplaceableMedia?: boolean;
|
||||
editingMessage?: ApiMessage;
|
||||
};
|
||||
|
||||
const AttachMenu: FC<OwnProps> = ({
|
||||
@ -70,6 +80,8 @@ const AttachMenu: FC<OwnProps> = ({
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
onPollCreate,
|
||||
hasReplaceableMedia,
|
||||
editingMessage,
|
||||
}) => {
|
||||
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
|
||||
const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isAttachMenuOpen, closeAttachMenu);
|
||||
@ -80,6 +92,12 @@ const AttachMenu: FC<OwnProps> = ({
|
||||
const [isAttachmentBotMenuOpen, markAttachmentBotMenuOpen, unmarkAttachmentBotMenuOpen] = useFlag();
|
||||
const isMenuOpen = isAttachMenuOpen || isAttachmentBotMenuOpen;
|
||||
|
||||
const isPhotoOrVideo = editingMessage && editingMessage?.groupedId
|
||||
&& Boolean(getMessagePhoto(editingMessage) || getMessageWebPagePhoto(editingMessage)
|
||||
|| Boolean(getMessageVideo(editingMessage) || getMessageWebPageVideo(editingMessage)));
|
||||
const isFile = editingMessage && editingMessage?.groupedId && Boolean(getMessageAudio(editingMessage)
|
||||
|| getMessageVoice(editingMessage) || getMessageDocument(editingMessage));
|
||||
|
||||
useEffect(() => {
|
||||
if (isAttachMenuOpen) {
|
||||
markMouseInside();
|
||||
@ -152,18 +170,36 @@ const AttachMenu: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div className="AttachMenu">
|
||||
<ResponsiveHoverButton
|
||||
id="attach-menu-button"
|
||||
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
|
||||
round
|
||||
color="translucent"
|
||||
onActivate={handleToggleAttachMenu}
|
||||
ariaLabel="Add an attachment"
|
||||
ariaControls="attach-menu-controls"
|
||||
hasPopup
|
||||
>
|
||||
<i className="icon icon-attach" />
|
||||
</ResponsiveHoverButton>
|
||||
{
|
||||
editingMessage && hasReplaceableMedia ? (
|
||||
<ResponsiveHoverButton
|
||||
id="replace-menu-button"
|
||||
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
|
||||
round
|
||||
color="translucent"
|
||||
onActivate={handleToggleAttachMenu}
|
||||
ariaLabel="Replace an attachment"
|
||||
ariaControls="replace-menu-controls"
|
||||
hasPopup
|
||||
>
|
||||
<Icon name="replace" />
|
||||
</ResponsiveHoverButton>
|
||||
) : (
|
||||
<ResponsiveHoverButton
|
||||
id="attach-menu-button"
|
||||
disabled={Boolean(editingMessage)}
|
||||
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
|
||||
round
|
||||
color="translucent"
|
||||
onActivate={handleToggleAttachMenu}
|
||||
ariaLabel="Add an attachment"
|
||||
ariaControls="attach-menu-controls"
|
||||
hasPopup
|
||||
>
|
||||
<Icon name="attach" />
|
||||
</ResponsiveHoverButton>
|
||||
)
|
||||
}
|
||||
<Menu
|
||||
id="attach-menu-controls"
|
||||
isOpen={isMenuOpen}
|
||||
@ -187,13 +223,13 @@ const AttachMenu: FC<OwnProps> = ({
|
||||
)}
|
||||
{canAttachMedia && (
|
||||
<>
|
||||
{canSendVideoOrPhoto && (
|
||||
{canSendVideoOrPhoto && !isFile && (
|
||||
<MenuItem icon="photo" onClick={handleQuickSelect}>
|
||||
{lang(canSendVideoAndPhoto ? 'AttachmentMenu.PhotoOrVideo'
|
||||
: (canSendPhotos ? 'InputAttach.Popover.Photo' : 'InputAttach.Popover.Video'))}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(canSendDocuments || canSendAudios)
|
||||
{((canSendDocuments || canSendAudios) && !isPhotoOrVideo)
|
||||
&& (
|
||||
<MenuItem icon="document" onClick={handleDocumentSelect}>
|
||||
{lang(!canSendDocuments && canSendAudios ? 'InputAttach.Popover.Music' : 'AttachDocument')}
|
||||
@ -206,11 +242,11 @@ const AttachMenu: FC<OwnProps> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canAttachPolls && (
|
||||
{canAttachPolls && !editingMessage && (
|
||||
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
|
||||
)}
|
||||
|
||||
{canAttachMedia && !isScheduled && bots?.map((bot) => (
|
||||
{!editingMessage && !hasReplaceableMedia && !isScheduled && bots?.map((bot) => (
|
||||
<AttachBotItem
|
||||
bot={bot}
|
||||
chatId={chatId}
|
||||
|
||||
@ -5,9 +5,9 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiAttachment, ApiChatMember, ApiSticker,
|
||||
ApiAttachment, ApiChatMember, ApiMessage, ApiSticker,
|
||||
} from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { GlobalState, MessageListType } from '../../../global/types';
|
||||
import type { ThreadId } from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { isUserId } from '../../../global/helpers';
|
||||
import { getAttachmentType, isUserId } from '../../../global/helpers';
|
||||
import { selectChatFullInfo, selectIsChatWithSelf } from '../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -62,6 +62,8 @@ export type OwnProps = {
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
attachments: ApiAttachment[];
|
||||
editingMessage?: ApiMessage;
|
||||
messageListType?: MessageListType;
|
||||
getHtml: Signal<string>;
|
||||
canShowCustomSendMenu?: boolean;
|
||||
isReady: boolean;
|
||||
@ -89,6 +91,7 @@ type StateProps = {
|
||||
currentUserId?: string;
|
||||
groupChatMembers?: ApiChatMember[];
|
||||
recentEmojis: string[];
|
||||
editingMessage?: ApiMessage;
|
||||
baseEmojiKeywords?: Record<string, string[]>;
|
||||
emojiKeywords?: Record<string, string[]>;
|
||||
shouldSuggestCustomEmoji?: boolean;
|
||||
@ -106,6 +109,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
threadId,
|
||||
attachments,
|
||||
getHtml,
|
||||
editingMessage,
|
||||
canShowCustomSendMenu,
|
||||
captionLimit,
|
||||
isReady,
|
||||
@ -150,12 +154,19 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
const renderingAttachments = attachments.length ? attachments : prevAttachments;
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const isEditing = editingMessage && Boolean(editingMessage);
|
||||
const isInAlbum = editingMessage && editingMessage?.groupedId;
|
||||
const isEditingMessageFile = attachments?.length && getAttachmentType(attachments[0]);
|
||||
const notEditingFile = isEditingMessageFile !== 'file';
|
||||
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
|
||||
const [shouldSendCompressed, setShouldSendCompressed] = useState(
|
||||
shouldSuggestCompression ?? attachmentSettings.shouldCompress,
|
||||
);
|
||||
const isSendingCompressed = Boolean((shouldSendCompressed || shouldForceCompression) && !shouldForceAsFile);
|
||||
const isSendingCompressed = Boolean(
|
||||
(shouldSendCompressed || shouldForceCompression || isInAlbum) && !shouldForceAsFile,
|
||||
);
|
||||
const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped);
|
||||
|
||||
const {
|
||||
@ -261,7 +272,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const sendAttachments = useLastCallback((isSilent?: boolean, shouldSendScheduled?: boolean) => {
|
||||
if (isOpen) {
|
||||
const send = ((shouldSchedule || shouldSendScheduled) && isForMessage) ? onSendScheduled
|
||||
const send = ((shouldSchedule || shouldSendScheduled) && isForMessage && !editingMessage) ? onSendScheduled
|
||||
: isSilent ? onSendSilent : onSend;
|
||||
send(isSendingCompressed, shouldSendGrouped);
|
||||
updateAttachmentSettings({
|
||||
@ -425,13 +436,13 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
let title = '';
|
||||
if (areAllPhotos) {
|
||||
title = lang('PreviewSender.SendPhoto', renderingAttachments.length, 'i');
|
||||
title = lang(isEditing ? 'EditMessageReplacePhoto' : 'PreviewSender.SendPhoto', renderingAttachments.length, 'i');
|
||||
} else if (areAllVideos) {
|
||||
title = lang('PreviewSender.SendVideo', renderingAttachments.length, 'i');
|
||||
title = lang(isEditing ? 'EditMessageReplaceVideo' : 'PreviewSender.SendVideo', renderingAttachments.length, 'i');
|
||||
} else if (areAllAudios) {
|
||||
title = lang('PreviewSender.SendAudio', renderingAttachments.length, 'i');
|
||||
title = lang(isEditing ? 'EditMessageReplaceAudio' : 'PreviewSender.SendAudio', renderingAttachments.length, 'i');
|
||||
} else {
|
||||
title = lang('PreviewSender.SendFile', renderingAttachments.length, 'i');
|
||||
title = lang(isEditing ? 'EditMessageReplaceFile' : 'PreviewSender.SendFile', renderingAttachments.length, 'i');
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
@ -445,57 +456,62 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
<div className="modal-title">{title}</div>
|
||||
<DropdownMenu
|
||||
className="attachment-modal-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="add" onClick={handleDocumentSelect}>{lang('Add')}</MenuItem>
|
||||
{hasMedia && (
|
||||
<>
|
||||
{
|
||||
!shouldForceAsFile && !shouldForceCompression && (isSendingCompressed ? (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="document" onClick={() => setShouldSendCompressed(false)}>
|
||||
{lang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')}
|
||||
{notEditingFile && !isInAlbum
|
||||
&& (
|
||||
<DropdownMenu
|
||||
className="attachmeneditingMessaget-modal-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
{Boolean(!editingMessage) && (
|
||||
<MenuItem icon="add" onClick={handleDocumentSelect}>{lang('Add')}</MenuItem>
|
||||
)}
|
||||
{hasMedia && (
|
||||
<>
|
||||
{
|
||||
!shouldForceAsFile && !shouldForceCompression && (isSendingCompressed ? (
|
||||
// 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>
|
||||
))
|
||||
}
|
||||
{isSendingCompressed && hasAnySpoilerable && Boolean(!editingMessage) && (
|
||||
hasSpoiler ? (
|
||||
<MenuItem icon="spoiler-disable" onClick={handleDisableSpoilers}>
|
||||
{lang('Attachment.DisableSpoiler')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem icon="spoiler" onClick={handleEnableSpoilers}>
|
||||
{lang('Attachment.EnableSpoiler')}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{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="photo" onClick={() => setShouldSendCompressed(true)}>
|
||||
{isMultiple ? 'Send All as Media' : 'Send as Media'}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
{isSendingCompressed && hasAnySpoilerable && (
|
||||
hasSpoiler ? (
|
||||
<MenuItem icon="spoiler-disable" onClick={handleDisableSpoilers}>
|
||||
{lang('Attachment.DisableSpoiler')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem icon="spoiler" onClick={handleEnableSpoilers}>
|
||||
{lang('Attachment.EnableSpoiler')}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="grouped" onClick={() => setShouldSendGrouped(true)}>
|
||||
Group All Media
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@ -622,7 +638,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleSendClick}
|
||||
onContextMenu={canShowCustomSendMenu ? handleContextMenu : undefined}
|
||||
>
|
||||
{shouldSchedule ? lang('Next') : lang('Send')}
|
||||
{shouldSchedule && !editingMessage ? lang('Next') : editingMessage ? lang('Save') : lang('Send')}
|
||||
</Button>
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
|
||||
@ -137,7 +137,7 @@ const AttachmentModalItem: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getDisplayType(attachment: ApiAttachment, shouldDisplayCompressed?: boolean) {
|
||||
export function getDisplayType(attachment: ApiAttachment, shouldDisplayCompressed?: boolean) {
|
||||
if (shouldDisplayCompressed && attachment.quick) {
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'image';
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
|
||||
import { canReplaceMessageMedia, isUploadingFileSticker } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import buildAttachment from './helpers/buildAttachment';
|
||||
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
@ -19,6 +25,7 @@ export type OwnProps = {
|
||||
withQuick?: boolean;
|
||||
onHide: NoneToVoidFunction;
|
||||
onFileSelect: (files: File[], suggestCompression?: boolean) => void;
|
||||
editingMessage?: ApiMessage | undefined;
|
||||
};
|
||||
|
||||
export enum DropAreaState {
|
||||
@ -30,12 +37,15 @@ export enum DropAreaState {
|
||||
const DROP_LEAVE_TIMEOUT_MS = 150;
|
||||
|
||||
const DropArea: FC<OwnProps> = ({
|
||||
isOpen, withQuick, onHide, onFileSelect,
|
||||
isOpen, withQuick, onHide, onFileSelect, editingMessage,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const { showNotification } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const hideTimeoutRef = useRef<number>(null);
|
||||
const prevWithQuick = usePrevious(withQuick);
|
||||
const { shouldRender, transitionClassNames } = useShowTransition(isOpen);
|
||||
const isInAlbum = editingMessage && editingMessage?.groupedId;
|
||||
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(onHide) : undefined), [isOpen, onHide]);
|
||||
|
||||
@ -47,6 +57,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);
|
||||
const newAttachment = folderFiles && await buildAttachment(folderFiles[0].name, folderFiles[0]);
|
||||
const canReplace = editingMessage && newAttachment && canReplaceMessageMedia(editingMessage, newAttachment);
|
||||
const isFileSticker = newAttachment && isUploadingFileSticker(newAttachment);
|
||||
|
||||
if (canReplace || isFileSticker) {
|
||||
showNotification({ message: lang(isInAlbum ? 'lng_edit_media_album_error' : 'lng_edit_media_invalid_file') });
|
||||
return;
|
||||
}
|
||||
if (folderFiles?.length) {
|
||||
files = files.concat(folderFiles);
|
||||
}
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import { useState } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiAttachment } from '../../../../api/types';
|
||||
import type { ApiAttachment, ApiMessage } from '../../../../api/types';
|
||||
|
||||
import {
|
||||
SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../../config';
|
||||
import { canReplaceMessageMedia, getAttachmentType } from '../../../../global/helpers';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
import buildAttachment from '../helpers/buildAttachment';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
export default function useAttachmentModal({
|
||||
@ -24,6 +21,7 @@ export default function useAttachmentModal({
|
||||
canSendPhotos,
|
||||
canSendDocuments,
|
||||
insertNextText,
|
||||
editedMessage,
|
||||
}: {
|
||||
attachments: ApiAttachment[];
|
||||
fileSizeLimit: number;
|
||||
@ -35,8 +33,10 @@ export default function useAttachmentModal({
|
||||
canSendPhotos?: boolean;
|
||||
canSendDocuments?: boolean;
|
||||
insertNextText: VoidFunction;
|
||||
editedMessage: ApiMessage | undefined;
|
||||
}) {
|
||||
const { openLimitReachedModal, showAllowedMessageTypesNotification } = getActions();
|
||||
const lang = useLang();
|
||||
const { openLimitReachedModal, showAllowedMessageTypesNotification, showNotification } = getActions();
|
||||
const [shouldForceAsFile, setShouldForceAsFile] = useState<boolean>(false);
|
||||
const [shouldForceCompression, setShouldForceCompression] = useState<boolean>(false);
|
||||
const [shouldSuggestCompression, setShouldSuggestCompression] = useState<boolean | undefined>(undefined);
|
||||
@ -85,16 +85,45 @@ export default function useAttachmentModal({
|
||||
);
|
||||
|
||||
const handleAppendFiles = useLastCallback(async (files: File[], isSpoiler?: boolean) => {
|
||||
handleSetAttachments([
|
||||
...attachments,
|
||||
...await Promise.all(files.map((file) => (
|
||||
if (editedMessage) {
|
||||
const newAttachment = await buildAttachment(files[0].name, files[0]);
|
||||
const canReplace = editedMessage && canReplaceMessageMedia(editedMessage, newAttachment);
|
||||
|
||||
if (editedMessage?.groupedId) {
|
||||
if (canReplace) {
|
||||
handleSetAttachments([newAttachment]);
|
||||
} else {
|
||||
showNotification({ message: lang('lng_edit_media_album_error') });
|
||||
}
|
||||
} else {
|
||||
handleSetAttachments([newAttachment]);
|
||||
}
|
||||
} else {
|
||||
const newAttachments = await Promise.all(files.map((file) => (
|
||||
buildAttachment(file.name, file, { shouldSendAsSpoiler: isSpoiler || undefined })
|
||||
))),
|
||||
]);
|
||||
)));
|
||||
handleSetAttachments([...attachments, ...newAttachments]);
|
||||
}
|
||||
});
|
||||
|
||||
const handleFileSelect = useLastCallback(async (files: File[], suggestCompression?: boolean) => {
|
||||
handleSetAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file))));
|
||||
if (editedMessage) {
|
||||
const newAttachment = await buildAttachment(files[0].name, files[0]);
|
||||
const canReplace = editedMessage && canReplaceMessageMedia(editedMessage, newAttachment);
|
||||
|
||||
if (editedMessage?.groupedId) {
|
||||
if (canReplace) {
|
||||
handleSetAttachments([newAttachment]);
|
||||
} else {
|
||||
showNotification({ message: lang('lng_edit_media_album_error') });
|
||||
}
|
||||
} else {
|
||||
handleSetAttachments([newAttachment]);
|
||||
}
|
||||
} else {
|
||||
const newAttachments = await Promise.all(files.map((file) => buildAttachment(file.name, file)));
|
||||
handleSetAttachments(newAttachments);
|
||||
}
|
||||
setShouldSuggestCompression(suggestCompression);
|
||||
});
|
||||
|
||||
@ -109,21 +138,3 @@ export default function useAttachmentModal({
|
||||
shouldForceAsFile,
|
||||
};
|
||||
}
|
||||
|
||||
function getAttachmentType(attachment: ApiAttachment) {
|
||||
if (attachment.shouldSendAsFile) return 'file';
|
||||
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (SUPPORTED_VIDEO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'audio';
|
||||
}
|
||||
|
||||
return 'file';
|
||||
}
|
||||
|
||||
@ -1,17 +1,21 @@
|
||||
import type { StateHookSetter } from '../../../../lib/teact/teact';
|
||||
import { useEffect } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
|
||||
|
||||
import {
|
||||
EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
|
||||
} from '../../../../config';
|
||||
import { canReplaceMessageMedia, isUploadingFileSticker } from '../../../../global/helpers';
|
||||
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
||||
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||
import buildAttachment from '../helpers/buildAttachment';
|
||||
import { preparePastedHtml } from '../helpers/cleanHtml';
|
||||
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4096;
|
||||
|
||||
const TYPE_HTML = 'text/html';
|
||||
@ -27,6 +31,9 @@ const useClipboardPaste = (
|
||||
shouldStripCustomEmoji?: boolean,
|
||||
onCustomEmojiStripped?: VoidFunction,
|
||||
) => {
|
||||
const { showNotification } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return undefined;
|
||||
@ -60,6 +67,9 @@ const useClipboardPaste = (
|
||||
e.preventDefault();
|
||||
if (items.length > 0) {
|
||||
files = await getFilesFromDataTransferItems(items);
|
||||
if (editedMessage) {
|
||||
files = files?.slice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!files?.length && !pastedText) {
|
||||
@ -79,13 +89,29 @@ const useClipboardPaste = (
|
||||
}
|
||||
|
||||
const hasText = textToPaste && textToPaste.text;
|
||||
const shouldSetAttachments = files?.length && !editedMessage && !isWordDocument;
|
||||
let shouldSetAttachments = files?.length && !isWordDocument;
|
||||
|
||||
const newAttachments = files ? await Promise.all(files.map((file) => buildAttachment(file.name, file))) : [];
|
||||
const canReplace = (editedMessage && newAttachments?.length
|
||||
&& canReplaceMessageMedia(editedMessage, newAttachments[0])) || Boolean(hasText);
|
||||
const isUploadingDocumentSticker = isUploadingFileSticker(newAttachments[0]);
|
||||
const isInAlbum = editedMessage && editedMessage?.groupedId;
|
||||
|
||||
if (editedMessage && isUploadingDocumentSticker) {
|
||||
showNotification({ message: lang(isInAlbum ? 'lng_edit_media_album_error' : 'lng_edit_media_invalid_file') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInAlbum) {
|
||||
shouldSetAttachments = canReplace;
|
||||
if (!shouldSetAttachments) {
|
||||
showNotification({ message: lang('lng_edit_media_album_error') });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSetAttachments) {
|
||||
const newAttachments = await Promise.all(files!.map((file) => {
|
||||
return buildAttachment(file.name, file);
|
||||
}));
|
||||
setAttachments((attachments) => attachments.concat(newAttachments));
|
||||
setAttachments(editedMessage ? newAttachments : (attachments) => attachments.concat(newAttachments));
|
||||
}
|
||||
|
||||
if (hasText) {
|
||||
@ -103,8 +129,8 @@ const useClipboardPaste = (
|
||||
document.removeEventListener('paste', handlePaste, false);
|
||||
};
|
||||
}, [
|
||||
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji, onCustomEmojiStripped,
|
||||
setNextText,
|
||||
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji,
|
||||
onCustomEmojiStripped, setNextText, lang,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@ -8,13 +8,14 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { IAlbum, ISettings } from '../../../types';
|
||||
import type { IAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
|
||||
import { getMessageContent, getMessageHtmlId, getMessageOriginalId } from '../../../global/helpers';
|
||||
import { getMessageContent, getMessageHtmlId } from '../../../global/helpers';
|
||||
import {
|
||||
selectActiveDownloads,
|
||||
selectCanAutoLoadMedia,
|
||||
selectCanAutoPlayMedia,
|
||||
selectTheme,
|
||||
} from '../../../global/selectors';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { AlbumRectPart } from './helpers/calculateAlbumLayout';
|
||||
import withSelectControl from './hocs/withSelectControl';
|
||||
|
||||
@ -40,7 +41,7 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
theme: ISettings['theme'];
|
||||
uploadsById: GlobalState['fileUploads']['byMessageLocalId'];
|
||||
uploadsByKey: GlobalState['fileUploads']['byMessageKey'];
|
||||
activeDownloadIds?: number[];
|
||||
};
|
||||
|
||||
@ -52,21 +53,21 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
isProtected,
|
||||
albumLayout,
|
||||
onMediaClick,
|
||||
uploadsById,
|
||||
uploadsByKey,
|
||||
activeDownloadIds,
|
||||
theme,
|
||||
}) => {
|
||||
const { cancelSendingMessage } = getActions();
|
||||
const { cancelUploadMedia } = getActions();
|
||||
|
||||
const mediaCount = album.messages.length;
|
||||
|
||||
const handleCancelUpload = useLastCallback((message: ApiMessage) => {
|
||||
cancelSendingMessage({ chatId: message.chatId, messageId: message.id });
|
||||
cancelUploadMedia({ chatId: message.chatId, messageId: message.id });
|
||||
});
|
||||
|
||||
function renderAlbumMessage(message: ApiMessage, index: number) {
|
||||
const { photo, video } = getMessageContent(message);
|
||||
const fileUpload = uploadsById[getMessageOriginalId(message)];
|
||||
const fileUpload = uploadsByKey[getMessageKey(message)];
|
||||
const uploadProgress = fileUpload?.progress;
|
||||
const { dimensions, sides } = albumLayout.layout[index];
|
||||
|
||||
@ -139,7 +140,7 @@ export default withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
theme,
|
||||
uploadsById: global.fileUploads.byMessageLocalId,
|
||||
uploadsByKey: global.fileUploads.byMessageKey,
|
||||
activeDownloadIds: isScheduled ? activeDownloads?.scheduledIds : activeDownloads?.ids,
|
||||
};
|
||||
},
|
||||
|
||||
@ -37,7 +37,6 @@ import {
|
||||
getMessageContent,
|
||||
getMessageCustomShape,
|
||||
getMessageHtmlId,
|
||||
getMessageKey,
|
||||
getMessageSingleCustomEmoji,
|
||||
getMessageSingleRegularEmoji,
|
||||
getSenderTitle,
|
||||
@ -99,6 +98,7 @@ import {
|
||||
import { isAnimatingScroll } from '../../../util/animateScroll';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useRef, useState } from '../../../lib/teact/teact';
|
||||
import React, { useEffect, useRef, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
@ -102,7 +102,15 @@ const Photo: FC<OwnProps> = ({
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(message);
|
||||
|
||||
const [isSpoilerShown, , hideSpoiler] = useFlag(photo.isSpoiler);
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(photo.isSpoiler);
|
||||
|
||||
useEffect(() => {
|
||||
if (photo.isSpoiler) {
|
||||
showSpoiler();
|
||||
} else {
|
||||
hideSpoiler();
|
||||
}
|
||||
}, [photo.isSpoiler]);
|
||||
|
||||
const {
|
||||
loadProgress: downloadProgress,
|
||||
@ -116,6 +124,7 @@ const Photo: FC<OwnProps> = ({
|
||||
message,
|
||||
uploadProgress || (isDownloading ? downloadProgress : loadProgress),
|
||||
shouldLoad && !fullMediaData,
|
||||
uploadProgress !== undefined,
|
||||
);
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
|
||||
|
||||
@ -7,8 +7,9 @@ import type {
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getMessageKey, isReactionChosen, isSameReaction } from '../../../global/helpers';
|
||||
import { isReactionChosen, isSameReaction } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useRef, useState } from '../../../lib/teact/teact';
|
||||
import React, { useEffect, useRef, useState } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
@ -80,7 +80,15 @@ const Video: FC<OwnProps> = ({
|
||||
const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!;
|
||||
const localBlobUrl = video.blobUrl;
|
||||
|
||||
const [isSpoilerShown, , hideSpoiler] = useFlag(video.isSpoiler);
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(video.isSpoiler);
|
||||
|
||||
useEffect(() => {
|
||||
if (video.isSpoiler) {
|
||||
showSpoiler();
|
||||
} else {
|
||||
hideSpoiler();
|
||||
}
|
||||
}, [video.isSpoiler]);
|
||||
|
||||
const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
const isIntersectingForPlaying = (
|
||||
@ -133,6 +141,7 @@ const Video: FC<OwnProps> = ({
|
||||
message,
|
||||
uploadProgress || (isDownloading ? downloadProgress : loadProgress),
|
||||
(shouldLoad && !isPlayerReady && !isFullMediaPreloaded) || isDownloading,
|
||||
uploadProgress !== undefined,
|
||||
);
|
||||
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
|
||||
@ -34,7 +34,7 @@ export default function useInnerHandlers(
|
||||
) {
|
||||
const {
|
||||
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu,
|
||||
markMessagesRead, cancelUploadMedia, sendPollVote, openForwardMenu,
|
||||
openChatLanguageModal, openThread, openStoryViewer,
|
||||
} = getActions();
|
||||
|
||||
@ -121,7 +121,7 @@ export default function useInnerHandlers(
|
||||
});
|
||||
|
||||
const handleCancelUpload = useLastCallback(() => {
|
||||
cancelSendingMessage({ chatId, messageId });
|
||||
cancelUploadMedia({ chatId, messageId });
|
||||
});
|
||||
|
||||
const handleVoteSend = useLastCallback((options: string[]) => {
|
||||
|
||||
@ -219,6 +219,9 @@ export const SLIDE_TRANSITION_DURATION = 450;
|
||||
export const VIDEO_WEBM_TYPE = 'video/webm';
|
||||
export const GIF_MIME_TYPE = 'image/gif';
|
||||
|
||||
export const LOTTIE_STICKER_MIME_TYPE = 'application/x-tgsticker';
|
||||
export const VIDEO_STICKER_MIME_TYPE = 'video/webm';
|
||||
|
||||
export const SUPPORTED_IMAGE_CONTENT_TYPES = new Set([
|
||||
'image/png', 'image/jpeg', GIF_MIME_TYPE,
|
||||
]);
|
||||
|
||||
@ -15,6 +15,7 @@ import type {
|
||||
ApiStorySkipped,
|
||||
ApiVideo,
|
||||
} from '../../../api/types';
|
||||
import type { MessageKey } from '../../../util/messageKey';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type {
|
||||
ActionReturnType, ApiDraft, GlobalState, TabArgs,
|
||||
@ -36,17 +37,20 @@ import { isDeepLink } from '../../../util/deepLinkParser';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import {
|
||||
areSortedArraysIntersecting, buildCollectionByKey, omit, partition, split, unique,
|
||||
areSortedArraysIntersecting,
|
||||
buildCollectionByKey,
|
||||
omit,
|
||||
partition,
|
||||
split,
|
||||
unique,
|
||||
} from '../../../util/iteratees';
|
||||
import { translate } from '../../../util/langProvider';
|
||||
import {
|
||||
debounce, onTickEnd, rafPromise,
|
||||
} from '../../../util/schedulers';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
|
||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||
import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
||||
import {
|
||||
getIsSavedDialog,
|
||||
getMessageOriginalId,
|
||||
getUserFullName,
|
||||
isChatChannel,
|
||||
isDeletedUser,
|
||||
@ -81,6 +85,7 @@ import {
|
||||
updateThreadInfo,
|
||||
updateThreadUnreadFromForwardedMessage,
|
||||
updateTopic,
|
||||
updateUploadByMessageKey,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
@ -125,7 +130,7 @@ import { deleteMessages } from '../apiUpdaters/messages';
|
||||
|
||||
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
|
||||
|
||||
const uploadProgressCallbacks = new Map<number, ApiOnProgress>();
|
||||
const uploadProgressCallbacks = new Map<MessageKey, ApiOnProgress>();
|
||||
|
||||
const runDebouncedForMarkRead = debounce((cb) => cb(), 500, false);
|
||||
|
||||
@ -426,13 +431,25 @@ addActionHandler('sendInviteMessages', async (global, actions, payload): Promise
|
||||
|
||||
addActionHandler('editMessage', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
messageList, text, entities, tabId = getCurrentTabId(),
|
||||
messageList, text, entities, attachments, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (!messageList) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentMessageKey: MessageKey | undefined;
|
||||
const progressCallback = attachments ? (progress: number, messageKey: MessageKey) => {
|
||||
if (!uploadProgressCallbacks.has(messageKey)) {
|
||||
currentMessageKey = messageKey;
|
||||
uploadProgressCallbacks.set(messageKey, progressCallback!);
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateUploadByMessageKey(global, messageKey, progress);
|
||||
setGlobal(global);
|
||||
} : undefined;
|
||||
|
||||
const { chatId, threadId, type: messageListType } = messageList;
|
||||
const chat = selectChat(global, chatId);
|
||||
const message = selectEditingMessage(global, chatId, threadId, messageListType);
|
||||
@ -440,26 +457,46 @@ addActionHandler('editMessage', (global, actions, payload): ActionReturnType =>
|
||||
return;
|
||||
}
|
||||
|
||||
void callApi('editMessage', {
|
||||
chat, message, text, entities, noWebPage: selectNoWebPage(global, chatId, threadId),
|
||||
});
|
||||
|
||||
actions.setEditingId({ messageId: undefined, tabId });
|
||||
|
||||
(async () => {
|
||||
await callApi('editMessage', {
|
||||
chat,
|
||||
message,
|
||||
attachment: attachments ? attachments[0] : undefined,
|
||||
text,
|
||||
entities,
|
||||
noWebPage: selectNoWebPage(global, chatId, threadId),
|
||||
}, progressCallback);
|
||||
|
||||
if (progressCallback && currentMessageKey) {
|
||||
global = getGlobal();
|
||||
global = updateUploadByMessageKey(global, currentMessageKey, undefined);
|
||||
setGlobal(global);
|
||||
|
||||
uploadProgressCallbacks.delete(currentMessageKey);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
addActionHandler('cancelSendingMessage', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('cancelUploadMedia', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, messageId } = payload!;
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
const progressCallback = message && uploadProgressCallbacks.get(getMessageOriginalId(message));
|
||||
if (!message) return;
|
||||
|
||||
const progressCallback = message && uploadProgressCallbacks.get(getMessageKey(message));
|
||||
if (progressCallback) {
|
||||
cancelApiProgress(progressCallback);
|
||||
}
|
||||
|
||||
actions.apiUpdate({
|
||||
'@type': 'deleteMessages',
|
||||
ids: [messageId],
|
||||
chatId,
|
||||
});
|
||||
if (isMessageLocal(message)) {
|
||||
actions.apiUpdate({
|
||||
'@type': 'deleteMessages',
|
||||
ids: [messageId],
|
||||
chatId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
|
||||
@ -1349,24 +1386,15 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
|
||||
wasDrafted?: boolean;
|
||||
lastMessageId?: number;
|
||||
}) {
|
||||
let localId: number | undefined;
|
||||
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
|
||||
if (!uploadProgressCallbacks.has(messageLocalId)) {
|
||||
localId = messageLocalId;
|
||||
uploadProgressCallbacks.set(messageLocalId, progressCallback!);
|
||||
let currentMessageKey: MessageKey | undefined;
|
||||
const progressCallback = params.attachment ? (progress: number, messageKey: MessageKey) => {
|
||||
if (!uploadProgressCallbacks.has(messageKey)) {
|
||||
currentMessageKey = messageKey;
|
||||
uploadProgressCallbacks.set(messageKey, progressCallback!);
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
global = {
|
||||
...global,
|
||||
fileUploads: {
|
||||
byMessageLocalId: {
|
||||
...global.fileUploads.byMessageLocalId,
|
||||
[messageLocalId]: { progress },
|
||||
},
|
||||
},
|
||||
};
|
||||
global = updateUploadByMessageKey(global, messageKey, progress);
|
||||
setGlobal(global);
|
||||
} : undefined;
|
||||
|
||||
@ -1377,8 +1405,12 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
|
||||
|
||||
await callApi('sendMessage', params, progressCallback);
|
||||
|
||||
if (progressCallback && localId) {
|
||||
uploadProgressCallbacks.delete(localId);
|
||||
if (progressCallback && currentMessageKey) {
|
||||
global = getGlobal();
|
||||
global = updateUploadByMessageKey(global, currentMessageKey, undefined);
|
||||
setGlobal(global);
|
||||
|
||||
uploadProgressCallbacks.delete(currentMessageKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,11 @@ import { GENERAL_REFETCH_INTERVAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getMessageKey,
|
||||
getUserReactions,
|
||||
isMessageLocal,
|
||||
isSameReaction,
|
||||
|
||||
@ -12,10 +12,11 @@ import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { omit, pickTruthy, unique } from '../../../util/iteratees';
|
||||
import { getMessageKey } from '../../../util/messageKey';
|
||||
import { notifyAboutMessage } from '../../../util/notifications';
|
||||
import { onTickEnd } from '../../../util/schedulers';
|
||||
import {
|
||||
checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
|
||||
checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage, isLocalMessageId,
|
||||
isMessageLocal, isUserId,
|
||||
} from '../../helpers';
|
||||
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
|
||||
@ -278,7 +279,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
global = {
|
||||
...global,
|
||||
fileUploads: {
|
||||
byMessageLocalId: omit(global.fileUploads.byMessageLocalId, [localId.toString()]),
|
||||
byMessageKey: omit(global.fileUploads.byMessageKey, [getMessageKey(message)]),
|
||||
},
|
||||
};
|
||||
|
||||
@ -763,19 +764,20 @@ function updateWithLocalMedia(
|
||||
: selectChatMessage(global, chatId, id);
|
||||
|
||||
// Preserve locally uploaded media.
|
||||
if (currentMessage && messageUpdate.content) {
|
||||
if (currentMessage && messageUpdate.content && !isLocalMessageId(id)) {
|
||||
const {
|
||||
photo, video, sticker, document,
|
||||
} = getMessageContent(currentMessage);
|
||||
|
||||
if (photo && messageUpdate.content.photo) {
|
||||
messageUpdate.content.photo.blobUrl = photo.blobUrl;
|
||||
messageUpdate.content.photo.thumbnail = photo.thumbnail;
|
||||
messageUpdate.content.photo.blobUrl ??= photo.blobUrl;
|
||||
messageUpdate.content.photo.thumbnail ??= photo.thumbnail;
|
||||
} else if (video && messageUpdate.content.video) {
|
||||
messageUpdate.content.video.blobUrl = video.blobUrl;
|
||||
messageUpdate.content.video.blobUrl ??= video.blobUrl;
|
||||
} else if (sticker && messageUpdate.content.sticker) {
|
||||
messageUpdate.content.sticker.isPreloadedGlobally = sticker.isPreloadedGlobally;
|
||||
messageUpdate.content.sticker.isPreloadedGlobally ??= sticker.isPreloadedGlobally;
|
||||
} else if (document && messageUpdate.content.document) {
|
||||
messageUpdate.content.document.previewBlobUrl = document.previewBlobUrl;
|
||||
messageUpdate.content.document.previewBlobUrl ??= document.previewBlobUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable eslint-multitab-tt/no-immediate-global */
|
||||
import { addCallback, removeCallback } from '../lib/teact/teactn';
|
||||
|
||||
import type { ApiMessage } from '../api/types';
|
||||
import type { ActionReturnType, GlobalState, MessageList } from './types';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
|
||||
@ -219,6 +220,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
untypedCached.appConfig.peerColors = undefined;
|
||||
untypedCached.appConfig.darkPeerColors = undefined;
|
||||
}
|
||||
|
||||
if (!cached.fileUploads.byMessageKey) {
|
||||
cached.fileUploads.byMessageKey = {};
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache() {
|
||||
@ -458,8 +463,16 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
return acc;
|
||||
}, {} as GlobalState['messages']['byChatId'][string]['threadsById']);
|
||||
|
||||
const cleanedById = Object.values(byId).reduce((acc, message) => {
|
||||
if (!message) return acc;
|
||||
|
||||
const cleanedMessage = omitLocalMedia(message);
|
||||
acc[message.id] = cleanedMessage;
|
||||
return acc;
|
||||
}, {} as Record<number, ApiMessage>);
|
||||
|
||||
byChatId[chatId] = {
|
||||
byId,
|
||||
byId: cleanedById,
|
||||
threadsById,
|
||||
};
|
||||
});
|
||||
@ -470,6 +483,33 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
};
|
||||
}
|
||||
|
||||
function omitLocalMedia(message: ApiMessage): ApiMessage {
|
||||
const {
|
||||
photo, video, document, sticker,
|
||||
} = message.content;
|
||||
|
||||
if (photo) {
|
||||
photo.blobUrl = undefined;
|
||||
}
|
||||
|
||||
if (video) {
|
||||
video.blobUrl = undefined;
|
||||
video.previewBlobUrl = undefined;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
document.previewBlobUrl = undefined;
|
||||
}
|
||||
|
||||
if (sticker) {
|
||||
sticker.isPreloadedGlobally = undefined;
|
||||
}
|
||||
|
||||
message.previousLocalId = undefined;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] {
|
||||
const { byKey, themes, performance } = global.settings;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ApiAttachment,
|
||||
ApiAudio,
|
||||
ApiDimensions,
|
||||
ApiDocument,
|
||||
@ -13,6 +14,7 @@ import type {
|
||||
} from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { getMessageKey } from '../../util/messageKey';
|
||||
import {
|
||||
IS_OPFS_SUPPORTED,
|
||||
IS_OPUS_SUPPORTED,
|
||||
@ -21,7 +23,7 @@ import {
|
||||
MAX_BUFFER_SIZE,
|
||||
} from '../../util/windowEnvironment';
|
||||
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
|
||||
import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages';
|
||||
import { getAttachmentType, matchLinkInMessageText } from './messages';
|
||||
|
||||
type MediaContainer = {
|
||||
content: MediaContent;
|
||||
@ -53,6 +55,17 @@ export function hasMessageMedia(message: MediaContainer) {
|
||||
));
|
||||
}
|
||||
|
||||
export function hasReplaceableMedia(message: MediaContainer) {
|
||||
const video = getMessageVideo(message);
|
||||
return Boolean((
|
||||
getMessagePhoto(message)
|
||||
|| (video && !video?.isRound)
|
||||
|| getMessageDocument(message)
|
||||
|| getMessageSticker(message)
|
||||
|| getMessageAudio(message)
|
||||
));
|
||||
}
|
||||
|
||||
export function getMessagePhoto(message: MediaContainer) {
|
||||
return message.content.photo;
|
||||
}
|
||||
@ -101,6 +114,11 @@ export function isMessageDocumentVideo(message: MediaContainer) {
|
||||
return document ? document.mediaType === 'video' : undefined;
|
||||
}
|
||||
|
||||
export function isMessageDocumentSticker(message: MediaContainer) {
|
||||
const document = getMessageDocument(message);
|
||||
return document ? document.mimeType === 'image/webp' : undefined;
|
||||
}
|
||||
|
||||
export function getMessageContact(message: MediaContainer) {
|
||||
return message.content.contact;
|
||||
}
|
||||
@ -423,8 +441,9 @@ export function getVideoDimensions(video: ApiVideo): ApiDimensions | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getMediaTransferState(message: ApiMessage, progress?: number, isLoadNeeded = false) {
|
||||
const isUploading = isMessageLocal(message);
|
||||
export function getMediaTransferState(
|
||||
message: ApiMessage, progress?: number, isLoadNeeded = false, isUploading = false,
|
||||
) {
|
||||
const isTransferring = isUploading || isLoadNeeded;
|
||||
const transferProgress = Number(progress);
|
||||
|
||||
@ -499,3 +518,18 @@ export function getMediaDuration(message: ApiMessage) {
|
||||
|
||||
return media.duration;
|
||||
}
|
||||
|
||||
export function canReplaceMessageMedia(message: ApiMessage, attachment: ApiAttachment) {
|
||||
const isPhotoOrVideo = Boolean(getMessagePhoto(message)
|
||||
|| getMessageWebPagePhoto(message) || Boolean(getMessageVideo(message)
|
||||
|| getMessageWebPageVideo(message)));
|
||||
const isFile = Boolean(getMessageAudio(message)
|
||||
|| getMessageVoice(message) || getMessageDocument(message));
|
||||
|
||||
const fileType = getAttachmentType(attachment);
|
||||
|
||||
return (
|
||||
(isPhotoOrVideo && (fileType === 'image' || fileType === 'video'))
|
||||
|| (isFile && (fileType === 'audio' || fileType === 'file'))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiPeer, ApiStory, ApiUser,
|
||||
ApiAttachment, ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiPeer, ApiStory, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useLang';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import {
|
||||
CONTENT_NOT_SUPPORTED,
|
||||
CONTENT_NOT_SUPPORTED, LOTTIE_STICKER_MIME_TYPE,
|
||||
RE_LINK_TEMPLATE,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
SERVICE_NOTIFICATIONS_USER_ID, SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_STICKER_MIME_TYPE,
|
||||
} from '../../config';
|
||||
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
||||
import { getMessageKey } from '../../util/messageKey';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { IS_OPUS_SUPPORTED } from '../../util/windowEnvironment';
|
||||
import { getGlobal } from '../index';
|
||||
@ -18,28 +20,10 @@ import { getUserFullName } from './users';
|
||||
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
|
||||
export type MessageKey = `msg${string}-${number}`;
|
||||
|
||||
export function getMessageHtmlId(messageId: number) {
|
||||
return `message${messageId.toString().replace('.', '-')}`;
|
||||
}
|
||||
|
||||
export function getMessageKey(message: ApiMessage): MessageKey {
|
||||
const { chatId, id, previousLocalId } = message;
|
||||
|
||||
return buildMessageKey(chatId, isServiceNotificationMessage(message) ? previousLocalId || id : id);
|
||||
}
|
||||
|
||||
export function buildMessageKey(chatId: string, msgId: number): MessageKey {
|
||||
return `msg${chatId}-${msgId}`;
|
||||
}
|
||||
|
||||
export function parseMessageKey(key: MessageKey) {
|
||||
const match = key.match(/^msg(-?\d+)-(\d+)/)!;
|
||||
|
||||
return { chatId: match[1], messageId: Number(match[2]) };
|
||||
}
|
||||
|
||||
export function getMessageOriginalId(message: ApiMessage) {
|
||||
return message.previousLocalId || message.id;
|
||||
}
|
||||
@ -358,3 +342,26 @@ export function hasMessageTtl(message: ApiMessage) {
|
||||
export function isJoinedChannelMessage(message: ApiMessage) {
|
||||
return message.content.action && message.content.action.type === 'joinedChannel';
|
||||
}
|
||||
|
||||
export function getAttachmentType(attachment: ApiAttachment) {
|
||||
if (attachment.shouldSendAsFile) return 'file';
|
||||
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (SUPPORTED_VIDEO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'audio';
|
||||
}
|
||||
|
||||
return 'file';
|
||||
}
|
||||
|
||||
export function isUploadingFileSticker(attachment: ApiAttachment) {
|
||||
return attachment ? (attachment.mimeType === 'image/webp' || attachment.mimeType === LOTTIE_STICKER_MIME_TYPE
|
||||
|| attachment.mimeType === VIDEO_STICKER_MIME_TYPE) : undefined;
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
},
|
||||
|
||||
fileUploads: {
|
||||
byMessageLocalId: {},
|
||||
byMessageKey: {},
|
||||
},
|
||||
|
||||
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy', 'duck', 'cherries'],
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import type {
|
||||
ApiMessage, ApiSponsoredMessage, ApiThreadInfo,
|
||||
} from '../../api/types';
|
||||
import type { ApiMessage, ApiSponsoredMessage, ApiThreadInfo } from '../../api/types';
|
||||
import type { FocusDirection, ThreadId } from '../../types';
|
||||
import type { MessageKey } from '../../util/messageKey';
|
||||
import type {
|
||||
GlobalState, MessageList, MessageListType, TabArgs, TabThread,
|
||||
Thread,
|
||||
GlobalState, MessageList, MessageListType, TabArgs, TabThread, Thread,
|
||||
} from '../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import {
|
||||
IS_MOCKED_CLIENT,
|
||||
IS_TEST, MESSAGE_LIST_SLICE, MESSAGE_LIST_VIEWPORT_LIMIT, TMP_CHAT_ID,
|
||||
IS_MOCKED_CLIENT, IS_TEST, MESSAGE_LIST_SLICE, MESSAGE_LIST_VIEWPORT_LIMIT, TMP_CHAT_ID,
|
||||
} from '../../config';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import {
|
||||
@ -32,7 +29,9 @@ import {
|
||||
selectOutlyingLists,
|
||||
selectPinnedIds,
|
||||
selectScheduledIds,
|
||||
selectTabState, selectThreadIdFromMessage, selectThreadInfo,
|
||||
selectTabState,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectViewportIds,
|
||||
} from '../selectors';
|
||||
import { updateTabState } from './tabs';
|
||||
@ -788,3 +787,21 @@ export function cancelMessageMediaDownload<T extends GlobalState>(
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateUploadByMessageKey<T extends GlobalState>(
|
||||
global: T,
|
||||
messageKey: MessageKey,
|
||||
progress: number | undefined,
|
||||
) {
|
||||
return {
|
||||
...global,
|
||||
fileUploads: {
|
||||
byMessageKey: progress !== undefined
|
||||
? {
|
||||
...global.fileUploads.byMessageKey,
|
||||
[messageKey]: { progress },
|
||||
}
|
||||
: omit(global.fileUploads.byMessageKey, [messageKey]),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { findLast } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { getMessageKey } from '../../util/messageKey';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
|
||||
import {
|
||||
@ -32,9 +33,7 @@ import {
|
||||
getIsSavedDialog,
|
||||
getMainUsername,
|
||||
getMessageAudio,
|
||||
getMessageDocument,
|
||||
getMessageOriginalId,
|
||||
getMessagePhoto,
|
||||
getMessageDocument, getMessagePhoto,
|
||||
getMessageVideo,
|
||||
getMessageVoice,
|
||||
getMessageWebPagePhoto,
|
||||
@ -48,7 +47,10 @@ import {
|
||||
isChatSuperGroup,
|
||||
isCommonBoxChat,
|
||||
isForwardedMessage,
|
||||
isLocalMessageId, isMessageFailed, isMessageLocal,
|
||||
isLocalMessageId,
|
||||
isMessageDocumentSticker,
|
||||
isMessageFailed,
|
||||
isMessageLocal,
|
||||
isMessageTranslatable,
|
||||
isOwnMessage,
|
||||
isServiceNotificationMessage,
|
||||
@ -588,6 +590,7 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const { content } = message;
|
||||
const messageTopic = selectTopicFromMessage(global, message);
|
||||
const isDocumentSticker = isMessageDocumentSticker(message);
|
||||
|
||||
const canEditMessagesIndefinitely = isChatWithSelf
|
||||
|| (isSuperGroup && getHasAdminRight(chat, 'pinMessages'))
|
||||
@ -597,8 +600,9 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
|
||||
canEditMessagesIndefinitely
|
||||
|| getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME
|
||||
) && !(
|
||||
content.sticker || content.contact || content.poll || content.action || content.audio
|
||||
content.sticker || content.contact || content.poll || content.action
|
||||
|| (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults
|
||||
|| isDocumentSticker
|
||||
)
|
||||
&& !isForwarded
|
||||
&& !message.viaBotId
|
||||
@ -801,7 +805,7 @@ export function selectActiveDownloads<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function selectUploadProgress<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
return global.fileUploads.byMessageLocalId[getMessageOriginalId(message)]?.progress;
|
||||
return global.fileUploads.byMessageKey[getMessageKey(message)]?.progress;
|
||||
}
|
||||
|
||||
export function selectRealLastReadId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
|
||||
@ -865,7 +865,7 @@ export type GlobalState = {
|
||||
phoneCall?: ApiPhoneCall;
|
||||
|
||||
fileUploads: {
|
||||
byMessageLocalId: Record<string, {
|
||||
byMessageKey: Record<string, {
|
||||
progress: number;
|
||||
}>;
|
||||
};
|
||||
@ -1352,7 +1352,7 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
userIds: string[];
|
||||
} & WithTabId;
|
||||
cancelSendingMessage: {
|
||||
cancelUploadMedia: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
};
|
||||
@ -1382,8 +1382,9 @@ export interface ActionPayloads {
|
||||
};
|
||||
};
|
||||
editMessage: {
|
||||
messageList: MessageList;
|
||||
messageList?: MessageList;
|
||||
text: string;
|
||||
attachments?: ApiAttachment[];
|
||||
entities?: ApiMessageEntity[];
|
||||
} & WithTabId;
|
||||
deleteHistory: {
|
||||
|
||||
@ -11,7 +11,8 @@ export async function parseAudioMetadata(url: string): Promise<AudioMetadata> {
|
||||
const { common: { title, artist, picture }, format: { duration } } = metadata;
|
||||
|
||||
const cover = selectCover(picture);
|
||||
const coverUrl = cover ? `data:${cover.format};base64,${cover.data.toString('base64')}` : undefined;
|
||||
const coverBlob = cover ? new Blob([cover.data], { type: cover.format }) : undefined;
|
||||
const coverUrl = coverBlob ? URL.createObjectURL(coverBlob) : undefined;
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { getActions, getGlobal } from '../global';
|
||||
|
||||
import type { ApiMessage } from '../api/types';
|
||||
import type { MessageKey } from '../global/helpers';
|
||||
import type { MessageKey } from './messageKey';
|
||||
import { AudioOrigin, GlobalSearchContent } from '../types';
|
||||
|
||||
import { requestNextMutation } from '../lib/fasterdom/fasterdom';
|
||||
import { getMessageKey, parseMessageKey } from '../global/helpers';
|
||||
import { selectCurrentMessageList, selectTabState } from '../global/selectors';
|
||||
import { getMessageKey, parseMessageKey } from './messageKey';
|
||||
import { isSafariPatchInProgress, patchSafariProgressiveAudio } from './patchSafariProgressiveAudio';
|
||||
import safePlay from './safePlay';
|
||||
import { IS_SAFARI } from './windowEnvironment';
|
||||
|
||||
@ -16,6 +16,9 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
|
||||
YOU_BLOCKED_USER: 'You blocked this user',
|
||||
IMAGE_PROCESS_FAILED: 'Failure while processing image',
|
||||
MEDIA_EMPTY: 'The provided media object is invalid',
|
||||
MEDIA_GROUPED_INVALID: 'Failed to replace album media',
|
||||
MEDIA_NEW_INVALID: 'Failed to replace new media',
|
||||
MESSAGE_NOT_MODIFIED: 'Message not modified. The new content is identical to the current one.',
|
||||
MEDIA_INVALID: 'Media invalid',
|
||||
PHOTO_EXT_INVALID: 'The extension of the photo is invalid',
|
||||
PHOTO_INVALID_DIMENSIONS: 'The photo dimensions are invalid',
|
||||
|
||||
19
src/util/messageKey.ts
Normal file
19
src/util/messageKey.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { ApiMessage } from '../api/types';
|
||||
|
||||
export type MessageKey = `msg${string}-${number}`;
|
||||
|
||||
export function getMessageKey(message: ApiMessage): MessageKey {
|
||||
const { chatId, id, previousLocalId } = message;
|
||||
|
||||
return buildMessageKey(chatId, previousLocalId || id);
|
||||
}
|
||||
|
||||
function buildMessageKey(chatId: string, msgId: number): MessageKey {
|
||||
return `msg${chatId}-${msgId}`;
|
||||
}
|
||||
|
||||
export function parseMessageKey(key: MessageKey) {
|
||||
const match = key.match(/^msg(-?\d+)-(\d+)/)!;
|
||||
|
||||
return { chatId: match[1], messageId: Number(match[2]) };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user