GIF: Support sending with caption (#6755)

This commit is contained in:
Alexander Zinchuk 2026-03-31 11:28:36 +02:00
parent 07d24e2d43
commit b5fed8276a
23 changed files with 249 additions and 120 deletions

View File

@ -662,6 +662,10 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
export function buildUploadingMedia(
attachment: ApiAttachment,
): MediaContent {
if (attachment.gif) {
return { video: attachment.gif };
}
const {
filename: fileName,
blobUrl,

View File

@ -197,14 +197,14 @@ export function buildInputDocument(media: ApiSticker | ApiVideo) {
]));
}
export function buildInputMediaDocument(media: ApiSticker | ApiVideo) {
export function buildInputMediaDocument(media: ApiSticker | ApiVideo, spoiler?: true) {
const inputDocument = buildInputDocument(media);
if (!inputDocument) {
return undefined;
}
return new GramJs.InputMediaDocument({ id: inputDocument });
return new GramJs.InputMediaDocument({ id: inputDocument, spoiler });
}
export function buildInputPoll(pollParams: ApiNewPoll, randomId: bigint) {

View File

@ -438,6 +438,9 @@ export function sendApiMessage(
}
}
if (!media && attachment?.gif) {
media = buildInputMediaDocument(attachment.gif, attachment.shouldSendAsSpoiler);
}
if (!media && attachment) {
try {
media = await uploadMedia(localMessage, attachment, onProgress!);
@ -607,27 +610,33 @@ function sendGroupedMedia(
const prevMediaQueue = mediaQueue;
mediaQueue = (async () => {
let media;
try {
media = await uploadMedia(localMessage, attachment, onProgress!);
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn(err);
let inputMedia: GramJs.TypeInputMedia | undefined;
if (attachment.gif) {
inputMedia = buildInputMediaDocument(attachment.gif, attachment.shouldSendAsSpoiler);
} else {
let media;
try {
media = await uploadMedia(localMessage, attachment, onProgress!);
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn(err);
}
groupedUploads[groupedId].counter--;
await prevMediaQueue;
return;
}
groupedUploads[groupedId].counter--;
await prevMediaQueue;
return;
inputMedia = await fetchInputMedia(
buildInputPeer(chat.id, chat.accessHash),
media,
);
}
const inputMedia = await fetchInputMedia(
buildInputPeer(chat.id, chat.accessHash),
media,
);
await prevMediaQueue;
if (!inputMedia) {

View File

@ -1,7 +1,7 @@
import type { CallbackAction } from '../../global/types';
import type { IconName } from '../../types/icons';
import type { LangFnParameters, RegularLangFnParameters } from '../../util/localization';
import type { ApiDocument, ApiFormattedText, ApiMessageEntity, ApiPhoto, ApiReaction } from './messages';
import type { ApiDocument, ApiFormattedText, ApiMessageEntity, ApiPhoto, ApiReaction, ApiVideo } from './messages';
import type { ApiPremiumSection } from './payments';
import type { ApiBotVerification } from './peers';
import type { ApiStarsSubscriptionPricing } from './stars';
@ -44,7 +44,7 @@ export interface ApiOnProgress {
}
export interface ApiAttachment {
blob: Blob;
blob?: Blob;
blobUrl: string;
compressedBlobUrl?: string;
filename: string;
@ -72,6 +72,8 @@ export interface ApiAttachment {
uniqueId?: string;
ttlSeconds?: number;
shouldSendInHighQuality?: boolean;
gif?: ApiVideo;
}
export interface ApiWallpaper {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 32 32"><path d="M24.241 8.014H3.18c-.741 0-1.342-.604-1.342-1.35 0-.745.601-1.349 1.342-1.349H24.24c.74 0 1.342.604 1.342 1.35 0 .745-.601 1.35-1.342 1.35" /><rect width="13.437" height="2.699" x="1.837" y="22.019" rx="1.35" ry="1.35"/><rect width="19.521" height="2.699" x="1.837" y="13.667" rx="1.35" ry="1.35"/><rect width="2.699" height="11.86" x="22.883" y="17.439" rx="1.35" ry="1.35"/><rect width="11.86" height="2.699" x="18.302" y="22.019" rx="1.35" ry="1.35"/></svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@ -2666,6 +2666,8 @@
"AttachmentSendAudio_other" = "Send {count} Audios";
"AttachmentSendFile_one" = "Send File";
"AttachmentSendFile_other" = "Send {count} Files";
"AttachmentSendGif" = "Send GIF";
"AttachmentReplaceGif" = "Replace GIF";
"AttachmentDragAddItems" = "Add Items";
"AttachmentCaptionPlaceholder" = "Add a caption...";
"MessageSummaryTitle" = "AI Summary";
@ -2729,3 +2731,4 @@
"GiftPreviewToggleRegularModels" = "View Primary Models >";
"AriaGiftPreviewPlay" = "Play random previews";
"AriaGiftPreviewStop" = "Pause random previews";
"MenuAddCaption" = "Add Caption";

View File

@ -132,7 +132,10 @@ import { getServerTime } from '../../util/serverTime';
import windowSize from '../../util/windowSize';
import { DEFAULT_MAX_MESSAGE_LENGTH } from '../../limits';
import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix';
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
import buildAttachment, {
buildGifAttachment,
prepareAttachmentsToSend,
} from '../middle/composer/helpers/buildAttachment';
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
import renderText from './helpers/renderText';
@ -1021,6 +1024,8 @@ const Composer = ({
theme,
});
const hasGifFromPicker = attachments.some((a) => a.gif);
useClipboardPaste(
isForCurrentMessageList || isInStoryViewer,
insertFormattedTextAndUpdateCursor,
@ -1030,6 +1035,7 @@ const Composer = ({
!isCurrentUserPremium && !isChatWithSelf,
showCustomEmojiPremiumNotification,
!attachments.length,
hasGifFromPicker,
);
const handleEmbeddedClear = useLastCallback(() => {
@ -1474,6 +1480,11 @@ const Composer = ({
clearDraft({ chatId, threadId, isLocalOnly: true });
});
const handleGifAddCaption = useLastCallback((gif: ApiVideo) => {
handleSetAttachments([buildGifAttachment(gif)]);
closeSymbolMenu();
});
const handleStickerSelect = useLastCallback((
sticker: ApiSticker,
isSilent?: boolean,
@ -2234,6 +2245,7 @@ const Composer = ({
canSendGifs={canSendGifs}
isMessageComposer={isInMessageList}
onGifSelect={handleGifSelect}
onGifAddCaption={handleGifAddCaption}
onStickerSelect={handleStickerSelect}
onCustomEmojiSelect={handleCustomEmojiSelect}
onRemoveSymbol={removeSymbol}

View File

@ -16,6 +16,7 @@ import useBuffering from '../../hooks/useBuffering';
import useCanvasBlur from '../../hooks/useCanvasBlur';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia';
import useOldLang from '../../hooks/useOldLang';
@ -33,9 +34,10 @@ type OwnProps = {
observeIntersection: ObserveFn;
isDisabled?: boolean;
className?: string;
isSavedMessages?: boolean;
onClick?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
onUnsaveClick?: (gif: ApiVideo) => void;
isSavedMessages?: boolean;
onAddCaption?: (gif: ApiVideo) => void;
};
const GifButton: FC<OwnProps> = ({
@ -43,13 +45,15 @@ const GifButton: FC<OwnProps> = ({
isDisabled,
className,
observeIntersection,
isSavedMessages,
onClick,
onUnsaveClick,
isSavedMessages,
onAddCaption,
}) => {
const ref = useRef<HTMLDivElement>();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const loadAndPlay = isIntersecting && !isDisabled;
@ -76,6 +80,7 @@ const GifButton: FC<OwnProps> = ({
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar'));
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.gif-context-menu .bubble'));
const getLayout = useLastCallback(() => ({ shouldAvoidNegativePosition: true }));
const handleClick = useLastCallback(() => {
if (isContextMenuOpen || !onClick) return;
@ -109,6 +114,13 @@ const GifButton: FC<OwnProps> = ({
}, undefined, true);
});
const handleAddCaption = useLastCallback(() => {
onAddCaption?.({
...gif,
blobUrl: videoData,
});
});
const handleMouseDown = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
preventMessageInputBlurWithBubbling(e);
handleBeforeContextMenu(e);
@ -182,17 +194,21 @@ const GifButton: FC<OwnProps> = ({
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className="gif-context-menu"
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
{!isSavedMessages && <MenuItem onClick={handleSendQuiet} icon="mute">{lang('SendWithoutSound')}</MenuItem>}
{!isSavedMessages && <MenuItem onClick={handleSendQuiet} icon="mute">{oldLang('SendWithoutSound')}</MenuItem>}
<MenuItem onClick={handleSendScheduled} icon="calendar">
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
{oldLang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
</MenuItem>
{onAddCaption && (
<MenuItem icon="add-caption" onClick={handleAddCaption}>{lang('MenuAddCaption')}</MenuItem>
)}
{onUnsaveClick && (
<MenuItem destructive icon="delete" onClick={handleContextDelete}>{lang('Delete')}</MenuItem>
<MenuItem destructive icon="delete" onClick={handleContextDelete}>{oldLang('Delete')}</MenuItem>
)}
</Menu>
)}

View File

@ -9,6 +9,7 @@ import type { Signal } from '../../../util/signals';
import {
BASE_EMOJI_KEYWORD_LANG,
EDITABLE_INPUT_MODAL_ID,
GIF_MIME_TYPE,
SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_PHOTO_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
@ -154,6 +155,7 @@ const AttachmentModal = ({
const svgRef = useRef<SVGSVGElement>();
const {
addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings, resetMessageMediaEditorRequest,
updateShouldSaveAttachmentsCompression,
} = getActions();
const lang = useLang();
@ -170,6 +172,7 @@ const AttachmentModal = ({
const isInAlbum = editingMessage && editingMessage?.groupedId;
const isEditingMessageFile = isEditing && attachments?.length && getAttachmentMediaType(attachments[0]);
const notEditingFile = isEditingMessageFile !== 'file';
const hasGifFromPicker = renderingAttachments?.some((a) => a.gif);
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
const [editingAttachmentIndex, setEditingAttachmentIndex] = useState<number | undefined>(undefined);
@ -185,7 +188,7 @@ const AttachmentModal = ({
const shouldSendCompressed = attachmentSettings.shouldCompress;
const isSendingCompressed = Boolean(
(shouldSendCompressed || shouldForceCompression || isInAlbum) && !shouldForceAsFile,
(shouldSendCompressed || shouldForceCompression || isInAlbum || hasGifFromPicker) && !shouldForceAsFile,
);
const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped);
const isInvertedMedia = attachmentSettings.isInvertedMedia;
@ -215,6 +218,13 @@ const AttachmentModal = ({
}
}, [closeSymbolMenu, isOpen]);
useEffect(() => {
if (hasGifFromPicker) {
updateShouldSaveAttachmentsCompression({ shouldSave: false });
setShouldSendGrouped(false);
}
}, [hasGifFromPicker, updateShouldSaveAttachmentsCompression]);
const [hasMedia, hasOnlyMedia] = useMemo(() => {
const onlyMedia = Boolean(renderingAttachments?.every((a) => a.quick || a.audio));
if (onlyMedia) return [true, true];
@ -509,13 +519,25 @@ const AttachmentModal = ({
const isQuickGallery = isSendingCompressed && hasOnlyMedia;
const [areAllPhotos, areAllVideos, areAllAudios, hasAnyPhoto] = useMemo(() => {
if (!isQuickGallery || !renderingAttachments) return [false, false, false];
const everyPhoto = renderingAttachments.every((a) => SUPPORTED_PHOTO_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));
const anyPhoto = renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType));
return [everyPhoto, everyVideo, everyAudio, anyPhoto];
const {
areAllPhotos, areAllVideos, areAllAudios, areAllGifs, hasAnyPhoto,
} = useMemo(() => {
if (!isQuickGallery || !renderingAttachments) {
return {
areAllPhotos: false,
areAllVideos: false,
areAllAudios: false,
areAllGifs: false,
hasAnyPhoto: false,
};
}
return {
areAllPhotos: renderingAttachments.every((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)),
areAllVideos: renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType)),
areAllAudios: renderingAttachments.every((a) => SUPPORTED_AUDIO_CONTENT_TYPES.has(a.mimeType)),
areAllGifs: renderingAttachments.every((a) => a.gif || a.mimeType === GIF_MIME_TYPE),
hasAnyPhoto: renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)),
};
}, [renderingAttachments, isQuickGallery]);
const hasAnySpoilerable = useMemo(() => {
@ -553,7 +575,10 @@ const AttachmentModal = ({
let title = '';
const attachmentsLength = renderingAttachments.length;
if (areAllPhotos) {
if (areAllGifs) {
title = lang(isEditing ? 'AttachmentReplaceGif' : 'AttachmentSendGif');
} else if (areAllPhotos) {
title = lang(
`Attachment${isEditing ? 'Replace' : 'Send'}Photo`,
{ count: attachmentsLength },
@ -602,7 +627,7 @@ const AttachmentModal = ({
trigger={MoreMenuButton}
positionX="right"
>
{Boolean(!editingMessage) && (
{Boolean(!editingMessage) && !hasGifFromPicker && (
<MenuItem icon="add" onClick={handleDocumentSelect}>{lang('Add')}</MenuItem>
)}
{hasMedia && (
@ -621,7 +646,7 @@ const AttachmentModal = ({
))
}
{
!shouldForceAsFile && !shouldForceCompression && (isSendingCompressed ? (
!shouldForceAsFile && !shouldForceCompression && !hasGifFromPicker && (isSendingCompressed ? (
<MenuItem icon="document" onClick={handleToggleShouldCompress}>
{lang(isMultiple ? 'AttachmentMenuSendAllAsFiles' : 'AttachmentMenuSendAsFiles')}

View File

@ -4,6 +4,7 @@
overflow-y: auto;
width: calc(100% - 0.375rem);
height: calc(100% - 0.1875rem);
margin: 0 0.1875rem;
padding-bottom: 0.1875rem;

View File

@ -24,6 +24,7 @@ type OwnProps = {
loadAndPlay: boolean;
canSendGifs?: boolean;
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
onGifAddCaption?: (gif: ApiVideo) => void;
};
type StateProps = {
@ -40,6 +41,7 @@ const GifPicker: FC<OwnProps & StateProps> = ({
savedGifs,
isSavedMessages,
onGifSelect,
onGifAddCaption,
}) => {
const { loadSavedGifs, saveGif } = getActions();
@ -80,9 +82,10 @@ const GifPicker: FC<OwnProps & StateProps> = ({
gif={gif}
observeIntersection={observeIntersection}
isDisabled={!loadAndPlay}
isSavedMessages={isSavedMessages}
onClick={canSendGifs ? onGifSelect : undefined}
onUnsaveClick={handleUnsaveClick}
isSavedMessages={isSavedMessages}
onAddCaption={canSendGifs ? onGifAddCaption : undefined}
/>
))
) : canRenderContents && savedGifs ? (

View File

@ -56,6 +56,7 @@ export type OwnProps = {
canUpdateStickerSetsOrder?: boolean,
) => void;
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
onGifAddCaption?: (gif: ApiVideo) => void;
onRemoveSymbol: () => void;
onSearchOpen: (type: 'stickers' | 'gifs') => void;
addRecentEmoji: GlobalActions['addRecentEmoji'];
@ -91,6 +92,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onCustomEmojiSelect,
onStickerSelect,
onGifSelect,
onGifAddCaption,
onRemoveSymbol,
onSearchOpen,
addRecentEmoji,
@ -239,6 +241,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
loadAndPlay={canSendGifs ? isOpen && (isActive || isFrom) : false}
canSendGifs={canSendGifs}
onGifSelect={onGifSelect}
onGifAddCaption={onGifAddCaption}
/>
);
}

View File

@ -41,6 +41,7 @@ type OwnProps = {
canUpdateStickerSetsOrder?: boolean,
) => void;
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
onGifAddCaption?: (gif: ApiVideo) => void;
onRemoveSymbol: VoidFunction;
onEmojiSelect: (emoji: string) => void;
closeBotCommandMenu?: VoidFunction;
@ -73,6 +74,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
onCustomEmojiSelect,
onStickerSelect,
onGifSelect,
onGifAddCaption,
onRemoveSymbol,
onEmojiSelect,
closeBotCommandMenu,
@ -181,6 +183,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
onStickerSelect={onStickerSelect}
onCustomEmojiSelect={onCustomEmojiSelect}
onGifSelect={onGifSelect}
onGifAddCaption={onGifAddCaption}
onRemoveSymbol={onRemoveSymbol}
onSearchOpen={handleSearchOpen}
addRecentEmoji={addRecentEmoji}

View File

@ -1,4 +1,4 @@
import type { ApiAttachment } from '../../../../api/types';
import type { ApiAttachment, ApiVideo } from '../../../../api/types';
import {
GIF_MIME_TYPE,
@ -132,3 +132,26 @@ function validateAspectRatio(width: number, height: number) {
const maxAspectRatio = Math.max(width, height) / Math.min(width, height);
return maxAspectRatio <= MAX_ASPECT_RATIO;
}
export function buildGifAttachment(gif: ApiVideo): ApiAttachment {
const {
blobUrl,
thumbnail,
fileName,
mimeType,
size,
width,
height,
duration,
} = gif;
return {
gif,
blobUrl: blobUrl || '',
previewBlobUrl: thumbnail?.dataUri,
filename: fileName,
mimeType,
size,
quick: width && height ? { width, height, duration } : undefined,
} satisfies ApiAttachment;
}

View File

@ -3,6 +3,7 @@ import { getActions } from '../../../../global';
import type { ApiAttachment, ApiMessage } from '../../../../api/types';
import { GIF_MIME_TYPE } from '../../../../config';
import { canReplaceMessageMedia, getAttachmentMediaType } from '../../../../global/helpers';
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
import buildAttachment from '../helpers/buildAttachment';
@ -86,6 +87,11 @@ export default function useAttachmentModal({
const handleAppendFiles = useLastCallback(async (files: File[], isSpoiler?: boolean) => {
if (editedMessage) {
if (editedMessage.groupedId && files[0].type === GIF_MIME_TYPE) {
showNotification({ message: lang('MediaReplaceInvalidError', undefined, { pluralValue: 1 }) });
return;
}
const newAttachment = await buildAttachment(files[0].name, files[0]);
const canReplace = editedMessage && canReplaceMessageMedia(editedMessage, newAttachment);
@ -109,6 +115,11 @@ export default function useAttachmentModal({
const handleFileSelect = useLastCallback(async (files: File[]) => {
if (editedMessage) {
if (editedMessage.groupedId && files[0].type === GIF_MIME_TYPE) {
showNotification({ message: lang('MediaReplaceInvalidError', undefined, { pluralValue: 1 }) });
return;
}
const newAttachment = await buildAttachment(files[0].name, files[0]);
const canReplace = editedMessage && canReplaceMessageMedia(editedMessage, newAttachment);
@ -129,8 +140,10 @@ export default function useAttachmentModal({
});
const handleUpdateAttachmentsQuality = useLastCallback(async () => {
const newAttachments = await Promise.all(attachments.map((attachment) =>
buildAttachment(attachment.filename, attachment.blob, { shouldSendInHighQuality })));
const newAttachments = await Promise.all(attachments.map(async (attachment) => {
if (!attachment.blob) return attachment;
return buildAttachment(attachment.filename, attachment.blob, { shouldSendInHighQuality });
}));
handleSetAttachments(newAttachments);
});

View File

@ -32,6 +32,7 @@ const useClipboardPaste = (
shouldStripCustomEmoji?: boolean,
onCustomEmojiStripped?: VoidFunction,
shouldUpdateAttachmentCompression?: boolean,
shouldSkipFilePaste?: boolean,
) => {
const {
showNotification,
@ -100,7 +101,7 @@ const useClipboardPaste = (
}
const hasText = textToPaste && textToPaste.text;
let shouldSetAttachments = files?.length && !isWordDocument;
let shouldSetAttachments = files?.length && !isWordDocument && !shouldSkipFilePaste;
const newAttachments = files ? await Promise.all(files.map((file) => buildAttachment(file.name, file))) : [];
const canReplace = (editedMessage && newAttachments?.length
@ -154,7 +155,7 @@ const useClipboardPaste = (
};
}, [
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji,
onCustomEmojiStripped, setNextText, lang, shouldUpdateAttachmentCompression,
onCustomEmojiStripped, setNextText, lang, shouldUpdateAttachmentCompression, shouldSkipFilePaste,
]);
};

View File

@ -299,14 +299,16 @@ const Video = <T,>({
style={forcedWidth ? `width: ${forcedWidth}px` : undefined}
/>
)}
<img
ref={previewRef}
src={previewBlobUrl}
className={buildClassName('thumbnail', withBlurredBackground && 'with-blurred-bg')}
alt=""
style={forcedWidth ? `width: ${forcedWidth}px;` : undefined}
draggable={!isProtected}
/>
{previewBlobUrl && (
<img
ref={previewRef}
src={previewBlobUrl}
className={buildClassName('thumbnail', withBlurredBackground && 'with-blurred-bg')}
alt=""
style={forcedWidth ? `width: ${forcedWidth}px;` : undefined}
draggable={!isProtected}
/>
)}
{hasThumb && !isPreviewPreloaded && (
<canvas ref={thumbRef} className="thumbnail" />
)}

View File

@ -3,8 +3,8 @@
font-weight: normal;
font-style: normal;
font-display: block;
src: url("./icons.woff2?d1a9cacb64e401206f928a39a587a2bd") format("woff2"),
url("./icons.woff?d1a9cacb64e401206f928a39a587a2bd") format("woff");
src: url("./icons.woff2?c37e5ad179a86df12e2f9214af6a855a") format("woff2"),
url("./icons.woff?c37e5ad179a86df12e2f9214af6a855a") format("woff");
}
.icon-char::before {
@ -900,87 +900,90 @@ url("./icons.woff?d1a9cacb64e401206f928a39a587a2bd") format("woff");
.icon-add-filled::before {
content: "\f225";
}
.icon-active-sessions::before {
.icon-add-caption::before {
content: "\f226";
}
.icon-folder-tabs-user::before {
.icon-active-sessions::before {
content: "\f227";
}
.icon-folder-tabs-star::before {
.icon-rating-icons-negative::before {
content: "\f228";
}
.icon-folder-tabs-group::before {
.icon-rating-icons-level90::before {
content: "\f229";
}
.icon-folder-tabs-folder::before {
.icon-rating-icons-level9::before {
content: "\f22a";
}
.icon-folder-tabs-chats::before {
.icon-rating-icons-level80::before {
content: "\f22b";
}
.icon-folder-tabs-chat::before {
.icon-rating-icons-level8::before {
content: "\f22c";
}
.icon-folder-tabs-channel::before {
.icon-rating-icons-level70::before {
content: "\f22d";
}
.icon-folder-tabs-bot::before {
.icon-rating-icons-level7::before {
content: "\f22e";
}
.icon-rating-icons-negative::before {
.icon-rating-icons-level60::before {
content: "\f22f";
}
.icon-rating-icons-level90::before {
.icon-rating-icons-level6::before {
content: "\f230";
}
.icon-rating-icons-level9::before {
.icon-rating-icons-level50::before {
content: "\f231";
}
.icon-rating-icons-level80::before {
.icon-rating-icons-level5::before {
content: "\f232";
}
.icon-rating-icons-level8::before {
.icon-rating-icons-level40::before {
content: "\f233";
}
.icon-rating-icons-level70::before {
.icon-rating-icons-level4::before {
content: "\f234";
}
.icon-rating-icons-level7::before {
.icon-rating-icons-level30::before {
content: "\f235";
}
.icon-rating-icons-level60::before {
.icon-rating-icons-level3::before {
content: "\f236";
}
.icon-rating-icons-level6::before {
.icon-rating-icons-level20::before {
content: "\f237";
}
.icon-rating-icons-level50::before {
.icon-rating-icons-level2::before {
content: "\f238";
}
.icon-rating-icons-level5::before {
.icon-rating-icons-level10::before {
content: "\f239";
}
.icon-rating-icons-level40::before {
.icon-rating-icons-level1::before {
content: "\f23a";
}
.icon-rating-icons-level4::before {
.icon-folder-tabs-user::before {
content: "\f23b";
}
.icon-rating-icons-level30::before {
.icon-folder-tabs-star::before {
content: "\f23c";
}
.icon-rating-icons-level3::before {
.icon-folder-tabs-group::before {
content: "\f23d";
}
.icon-rating-icons-level20::before {
.icon-folder-tabs-folder::before {
content: "\f23e";
}
.icon-rating-icons-level2::before {
.icon-folder-tabs-chats::before {
content: "\f23f";
}
.icon-rating-icons-level10::before {
.icon-folder-tabs-chat::before {
content: "\f240";
}
.icon-rating-icons-level1::before {
.icon-folder-tabs-channel::before {
content: "\f241";
}
.icon-folder-tabs-bot::before {
content: "\f242";
}

View File

@ -309,32 +309,33 @@ $icons-map: (
"add-user-filled": "\f223",
"add-one-badge": "\f224",
"add-filled": "\f225",
"active-sessions": "\f226",
"folder-tabs-user": "\f227",
"folder-tabs-star": "\f228",
"folder-tabs-group": "\f229",
"folder-tabs-folder": "\f22a",
"folder-tabs-chats": "\f22b",
"folder-tabs-chat": "\f22c",
"folder-tabs-channel": "\f22d",
"folder-tabs-bot": "\f22e",
"rating-icons-negative": "\f22f",
"rating-icons-level90": "\f230",
"rating-icons-level9": "\f231",
"rating-icons-level80": "\f232",
"rating-icons-level8": "\f233",
"rating-icons-level70": "\f234",
"rating-icons-level7": "\f235",
"rating-icons-level60": "\f236",
"rating-icons-level6": "\f237",
"rating-icons-level50": "\f238",
"rating-icons-level5": "\f239",
"rating-icons-level40": "\f23a",
"rating-icons-level4": "\f23b",
"rating-icons-level30": "\f23c",
"rating-icons-level3": "\f23d",
"rating-icons-level20": "\f23e",
"rating-icons-level2": "\f23f",
"rating-icons-level10": "\f240",
"rating-icons-level1": "\f241",
"add-caption": "\f226",
"active-sessions": "\f227",
"rating-icons-negative": "\f228",
"rating-icons-level90": "\f229",
"rating-icons-level9": "\f22a",
"rating-icons-level80": "\f22b",
"rating-icons-level8": "\f22c",
"rating-icons-level70": "\f22d",
"rating-icons-level7": "\f22e",
"rating-icons-level60": "\f22f",
"rating-icons-level6": "\f230",
"rating-icons-level50": "\f231",
"rating-icons-level5": "\f232",
"rating-icons-level40": "\f233",
"rating-icons-level4": "\f234",
"rating-icons-level30": "\f235",
"rating-icons-level3": "\f236",
"rating-icons-level20": "\f237",
"rating-icons-level2": "\f238",
"rating-icons-level10": "\f239",
"rating-icons-level1": "\f23a",
"folder-tabs-user": "\f23b",
"folder-tabs-star": "\f23c",
"folder-tabs-group": "\f23d",
"folder-tabs-folder": "\f23e",
"folder-tabs-chats": "\f23f",
"folder-tabs-chat": "\f240",
"folder-tabs-channel": "\f241",
"folder-tabs-bot": "\f242",
);

Binary file not shown.

Binary file not shown.

View File

@ -292,15 +292,8 @@ export type FontIconName =
| 'add-user-filled'
| 'add-one-badge'
| 'add-filled'
| 'add-caption'
| 'active-sessions'
| 'folder-tabs-user'
| 'folder-tabs-star'
| 'folder-tabs-group'
| 'folder-tabs-folder'
| 'folder-tabs-chats'
| 'folder-tabs-chat'
| 'folder-tabs-channel'
| 'folder-tabs-bot'
| 'rating-icons-negative'
| 'rating-icons-level90'
| 'rating-icons-level9'
@ -319,4 +312,12 @@ export type FontIconName =
| 'rating-icons-level20'
| 'rating-icons-level2'
| 'rating-icons-level10'
| 'rating-icons-level1';
| 'rating-icons-level1'
| 'folder-tabs-user'
| 'folder-tabs-star'
| 'folder-tabs-group'
| 'folder-tabs-folder'
| 'folder-tabs-chats'
| 'folder-tabs-chat'
| 'folder-tabs-channel'
| 'folder-tabs-bot';

View File

@ -1958,6 +1958,8 @@ export interface LangPair {
'AttachmentMenuUngroupAllMedia': undefined;
'AttachmentMenuEnableSpoiler': undefined;
'AttachmentMenuDisableSpoiler': undefined;
'AttachmentSendGif': undefined;
'AttachmentReplaceGif': undefined;
'AttachmentDragAddItems': undefined;
'AttachmentCaptionPlaceholder': undefined;
'MessageSummaryTitle': undefined;
@ -2002,6 +2004,7 @@ export interface LangPair {
'GiftPreviewToggleRegularModels': undefined;
'AriaGiftPreviewPlay': undefined;
'AriaGiftPreviewStop': undefined;
'MenuAddCaption': undefined;
}
export interface LangPairWithVariables<V = LangVariable> {