From 24957c958ea26e878957690d793d2e586cea79f2 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 22 Jan 2023 19:16:30 +0100 Subject: [PATCH] Message: Support "Hidden Media" (#2308) --- src/api/gramjs/apiBuilders/common.ts | 3 +- src/api/gramjs/apiBuilders/messages.ts | 12 +- src/api/gramjs/methods/messages.ts | 13 +- src/api/gramjs/methods/users.ts | 2 +- src/api/types/messages.ts | 3 + src/api/types/misc.ts | 3 +- src/assets/fonts/icomoon.woff | Bin 53644 -> 53568 bytes src/assets/fonts/icomoon.woff2 | Bin 24892 -> 24896 bytes src/assets/spoilers/mask.svg | 6 + src/assets/{ => spoilers}/turbulence_1x.png | Bin src/assets/{ => spoilers}/turbulence_2x.png | Bin src/assets/{ => spoilers}/turbulence_3x.png | Bin src/components/common/EmbeddedMessage.scss | 16 ++- src/components/common/EmbeddedMessage.tsx | 31 +++-- src/components/common/Media.scss | 2 +- src/components/common/Media.tsx | 25 +++- .../common/MediaSpoiler.module.scss | 105 ++++++++++++++++ src/components/common/MediaSpoiler.tsx | 65 ++++++++++ src/components/common/UiLoader.tsx | 2 + src/components/left/main/Chat.scss | 5 + .../left/main/hooks/useChatListEntry.tsx | 11 +- src/components/left/search/ChatMessage.scss | 7 +- src/components/left/search/ChatMessage.tsx | 11 +- src/components/middle/HeaderPinnedMessage.tsx | 18 ++- src/components/middle/MiddleHeader.scss | 13 +- .../middle/composer/AttachmentModal.scss | 35 ++++-- .../middle/composer/AttachmentModal.tsx | 117 ++++++++++++++---- .../composer/AttachmentModalItem.module.scss | 6 +- .../middle/composer/AttachmentModalItem.tsx | 30 ++++- src/components/middle/composer/Composer.scss | 2 +- src/components/middle/composer/Composer.tsx | 3 +- .../middle/composer/MessageInput.tsx | 4 +- .../composer/helpers/buildAttachment.ts | 6 +- .../composer/hooks/useAttachmentModal.ts | 17 +-- .../message/InvoiceMediaPreview.module.scss | 70 +---------- .../middle/message/InvoiceMediaPreview.tsx | 15 ++- src/components/middle/message/Photo.tsx | 37 ++++-- src/components/middle/message/Video.tsx | 55 +++++--- src/global/helpers/messageMedia.ts | 11 +- src/hooks/useCanvasBlur.ts | 7 ++ src/hooks/useScrolledState.ts | 17 +++ src/hooks/useShowTransition.ts | 3 +- src/styles/Telegram T.json | 14 +-- 43 files changed, 609 insertions(+), 193 deletions(-) create mode 100644 src/assets/spoilers/mask.svg rename src/assets/{ => spoilers}/turbulence_1x.png (100%) rename src/assets/{ => spoilers}/turbulence_2x.png (100%) rename src/assets/{ => spoilers}/turbulence_3x.png (100%) create mode 100644 src/components/common/MediaSpoiler.module.scss create mode 100644 src/components/common/MediaSpoiler.tsx create mode 100644 src/hooks/useScrolledState.ts diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index a6598a697..a4d9e0272 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -62,7 +62,7 @@ export function buildApiThumbnailFromPath( }; } -export function buildApiPhoto(photo: GramJs.Photo): ApiPhoto { +export function buildApiPhoto(photo: GramJs.Photo, isSpoiler?: boolean): ApiPhoto { const sizes = photo.sizes .filter((s: any): s is GramJs.PhotoSize => { return s instanceof GramJs.PhotoSize || s instanceof GramJs.PhotoSizeProgressive; @@ -73,6 +73,7 @@ export function buildApiPhoto(photo: GramJs.Photo): ApiPhoto { id: String(photo.id), thumbnail: buildApiThumbnailFromStripped(photo.sizes), sizes, + isSpoiler, ...(photo.videoSizes && { videoSizes: photo.videoSizes.map(buildApiVideoSize), isVideo: true }), }; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index f90a21cc1..f81a1a725 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -447,10 +447,10 @@ function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined { return undefined; } - return buildApiPhoto(media.photo); + return buildApiPhoto(media.photo, media.spoiler); } -export function buildVideoFromDocument(document: GramJs.Document): ApiVideo | undefined { +export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: boolean): ApiVideo | undefined { if (document instanceof GramJs.DocumentEmpty) { return undefined; } @@ -499,6 +499,7 @@ export function buildVideoFromDocument(document: GramJs.Document): ApiVideo | un isGif: Boolean(gifAttr), thumbnail: buildApiThumbnailFromStripped(thumbs), size: size.toJSNumber(), + isSpoiler, }; } @@ -511,7 +512,7 @@ function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined { return undefined; } - return buildVideoFromDocument(media.document); + return buildVideoFromDocument(media.document, media.spoiler); } function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined { @@ -1393,6 +1394,7 @@ function buildUploadingMedia( size, audio, shouldSendAsFile, + shouldSendAsSpoiler, } = attachment; if (!shouldSendAsFile) { @@ -1403,8 +1405,9 @@ function buildUploadingMedia( photo: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, sizes: [], - thumbnail: { width, height, dataUri: '' }, // Used only for dimensions + thumbnail: { width, height, dataUri: blobUrl }, blobUrl, + isSpoiler: shouldSendAsSpoiler, }, }; } @@ -1421,6 +1424,7 @@ function buildUploadingMedia( blobUrl, ...(previewBlobUrl && { thumbnail: { width, height, dataUri: previewBlobUrl } }), size, + isSpoiler: shouldSendAsSpoiler, }, }; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 9d58af8ce..0497ce705 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -462,6 +462,7 @@ async function fetchInputMedia( peer, media: uploadedMedia, })); + const isSpoiler = uploadedMedia.spoiler; if (( messageMedia instanceof GramJs.MessageMediaPhoto @@ -472,6 +473,7 @@ async function fetchInputMedia( return new GramJs.InputMediaPhoto({ id: new GramJs.InputPhoto({ id, accessHash, fileReference }), + spoiler: isSpoiler, }); } @@ -484,6 +486,7 @@ async function fetchInputMedia( return new GramJs.InputMediaDocument({ id: new GramJs.InputDocument({ id, accessHash, fileReference }), + spoiler: isSpoiler, }); } @@ -562,7 +565,7 @@ export async function rescheduleMessage({ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) { const { - filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, shouldSendAsFile, + filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, shouldSendAsFile, shouldSendAsSpoiler, } = attachment; const patchedOnProgress: ApiOnProgress = (progress) => { @@ -583,7 +586,10 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, if (!shouldSendAsFile) { if (quick) { if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { - return new GramJs.InputMediaUploadedPhoto({ file: inputFile }); + return new GramJs.InputMediaUploadedPhoto({ + file: inputFile, + spoiler: shouldSendAsSpoiler, + }); } if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { @@ -624,7 +630,8 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, mimeType, attributes, thumb, - forceFile: shouldSendAsFile || undefined, + forceFile: shouldSendAsFile, + spoiler: shouldSendAsSpoiler, }); } diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index cb588cb37..d819a9ee4 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -251,7 +251,7 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { return { photos: result.photos .filter((photo): photo is GramJs.Photo => photo instanceof GramJs.Photo) - .map(buildApiPhoto), + .map((photo) => buildApiPhoto(photo)), users: result.users.map(buildApiUser).filter(Boolean), }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 2f833e301..71ff93a28 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -28,6 +28,7 @@ export interface ApiPhoto { sizes: ApiPhotoSize[]; videoSizes?: ApiVideoSize[]; blobUrl?: string; + isSpoiler?: boolean; } export interface ApiSticker { @@ -88,8 +89,10 @@ export interface ApiVideo { supportsStreaming?: boolean; isRound?: boolean; isGif?: boolean; + isSpoiler?: boolean; thumbnail?: ApiThumbnail; blobUrl?: string; + previewBlobUrl?: string; size: number; } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 177407e52..ae3fa0a97 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -46,7 +46,8 @@ export interface ApiAttachment { }; previewBlobUrl?: string; - shouldSendAsFile?: boolean; + shouldSendAsFile?: true; + shouldSendAsSpoiler?: true; uniqueId?: string; } diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 7d6f7cbec11ad6158bc2b523eb025ceb6dba3ead..a4f7b575c7d21cc83b08329aba9834dadf4f6503 100644 GIT binary patch delta 872 zcmYjPT}V_x82x7N?48->?#^9i?!9~0ZN06w8(3DZKSU)#ut*4j1rZ3MDF|j7843j( z89fD&5fnl614R%*?2AEB4?aj@W-mqsK}6VFkUr^FGp4YY?=asv-+aIZZw-aIti z(W8X|AW+}JAz*razYOT+*#-B;&O&i!31DU^c6lP#-`Cv>@b;N9G;7qL_I>?IRN8mN zYco#IX!er`R3LD&_#{8^M5A>yT^tB=9L$Mb-Y7g&U6p< zDeboMT-CV4jzm_6ht7?taKZDnl{ZBTIw52%8pojk$Dj)aU=+sTI^2aPP=eR+4Pbjs zCY!5Axt3O>YHz;5ZAqhQl-6uFQUg`l42dCC9PMdrisxd;iA&c@BUSrRLvvGOwgzQP zN4oj{L~M|v9vKyrTt!bR`btmbNtJ%xn;SP&>L`Zv6HvCXrMaORwW7RwiF$@3&0?K8 zj38H{50kuy;RCeq=J`;6b=$z zDiF5sNbdWnmU61942)S>OX3I>b@CNYRtcRoG|5hy7*pexNKhKWnD`>BHjoSt^E9Il zp%hDtrTGBDsMKZ$;?F@ZC)4#diK?rUp&wCF)VWEtT+$(h#Ra;rq9-J``G&;Aq;K*>FBX~KJ|{2rsrqSKV|r#yZ`_I delta 936 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 + + + + + diff --git a/src/assets/turbulence_1x.png b/src/assets/spoilers/turbulence_1x.png similarity index 100% rename from src/assets/turbulence_1x.png rename to src/assets/spoilers/turbulence_1x.png diff --git a/src/assets/turbulence_2x.png b/src/assets/spoilers/turbulence_2x.png similarity index 100% rename from src/assets/turbulence_2x.png rename to src/assets/spoilers/turbulence_2x.png diff --git a/src/assets/turbulence_3x.png b/src/assets/spoilers/turbulence_3x.png similarity index 100% rename from src/assets/turbulence_3x.png rename to src/assets/spoilers/turbulence_3x.png diff --git a/src/components/common/EmbeddedMessage.scss b/src/components/common/EmbeddedMessage.scss index ea5b5f1b8..490bd2798 100644 --- a/src/components/common/EmbeddedMessage.scss +++ b/src/components/common/EmbeddedMessage.scss @@ -36,7 +36,7 @@ bottom: 0.625rem; } - .pictogram { + .embedded-thumb { margin-inline-start: 0.5rem; } @@ -120,19 +120,27 @@ opacity: 0.75; } - .pictogram { + .embedded-thumb { + position: relative; width: 2rem; height: 2rem; - object-fit: cover; border-radius: 0.25rem; margin-left: 0.25rem; flex-shrink: 0; + overflow: hidden; + &.round { border-radius: 1rem; } } + .pictogram { + width: 100%; + height: 100%; + object-fit: cover; + } + &.inside-input { padding-inline-start: 0.5625rem; width: 100%; @@ -144,7 +152,7 @@ bottom: 0.3125rem; } - .pictogram { + .embedded-thumb { margin-left: 0.125rem; } diff --git a/src/components/common/EmbeddedMessage.tsx b/src/components/common/EmbeddedMessage.tsx index 893d07a62..a9fbc5717 100644 --- a/src/components/common/EmbeddedMessage.tsx +++ b/src/components/common/EmbeddedMessage.tsx @@ -11,6 +11,7 @@ import { getSenderTitle, getMessageRoundVideo, getUserColorKey, + getMessageIsSpoiler, } from '../../global/helpers'; import renderText from './helpers/renderText'; import { getPictogramDimensions } from './helpers/mediaDimensions'; @@ -24,6 +25,7 @@ import useLang from '../../hooks/useLang'; import ActionMessage from '../middle/ActionMessage'; import MessageSummary from './MessageSummary'; +import MediaSpoiler from './MediaSpoiler'; import './EmbeddedMessage.scss'; @@ -63,6 +65,7 @@ const EmbeddedMessage: FC = ({ const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting); const mediaThumbnail = useThumbnail(message); const isRoundVideo = Boolean(message && getMessageRoundVideo(message)); + const isSpoiler = Boolean(message && getMessageIsSpoiler(message)); const lang = useLang(); @@ -78,7 +81,7 @@ const EmbeddedMessage: FC = ({ )} onClick={message ? onClick : undefined} > - {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected)} + {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}

{!message ? ( @@ -112,21 +115,27 @@ function renderPictogram( blobUrl?: string, isRoundVideo?: boolean, isProtected?: boolean, + isSpoiler?: boolean, ) { const { width, height } = getPictogramDimensions(); + const srcUrl = blobUrl || thumbDataUri; + return ( - <> - +

+ {!isSpoiler && ( + + )} + {isProtected && } - +
); } diff --git a/src/components/common/Media.scss b/src/components/common/Media.scss index f4c138f67..00aa0e01d 100644 --- a/src/components/common/Media.scss +++ b/src/components/common/Media.scss @@ -17,7 +17,7 @@ line-height: 1.125rem; } - img { + .media-miniature { position: absolute; left: 0; top: 0; diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index 33c997474..4692737db 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -1,22 +1,27 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useCallback, useRef } from '../../lib/teact/teact'; +import type { FC } from '../../lib/teact/teact'; import type { ApiMessage } from '../../api/types'; +import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { formatMediaDuration } from '../../util/dateFormat'; import stopEvent from '../../util/stopEvent'; import { getMessageHtmlId, + getMessageIsSpoiler, getMessageMediaHash, getMessageMediaThumbDataUri, getMessageVideo, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; + import useMedia from '../../hooks/useMedia'; import useMediaTransition from '../../hooks/useMediaTransition'; -import type { ObserveFn } from '../../hooks/useIntersectionObserver'; +import useFlag from '../../hooks/useFlag'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; +import MediaSpoiler from './MediaSpoiler'; + import './Media.scss'; type OwnProps = { @@ -44,9 +49,13 @@ const Media: FC = ({ const video = getMessageVideo(message); + const hasSpoiler = getMessageIsSpoiler(message); + const [isSpoilerShown, , hideSpoiler] = useFlag(hasSpoiler); + const handleClick = useCallback(() => { + hideSpoiler(); onClick!(message.id, message.chatId); - }, [message.id, message.chatId, onClick]); + }, [hideSpoiler, message, onClick]); return (
= ({ > = ({ /> + {hasSpoiler && ( + + )} {video && {video.isGif ? 'GIF' : formatMediaDuration(video.duration)}} {isProtected && }
diff --git a/src/components/common/MediaSpoiler.module.scss b/src/components/common/MediaSpoiler.module.scss new file mode 100644 index 000000000..acbf100c4 --- /dev/null +++ b/src/components/common/MediaSpoiler.module.scss @@ -0,0 +1,105 @@ +.root { + position: absolute; + width: 100%; + height: 100%; + background-color: var(--color-text-secondary); // Fallback before canvas is prepared + + --click-shift-x: 0px; + --click-shift-y: 0px; +} + +.mask-animation:global(.closing) { + mask-image: url("../../assets/spoilers/mask.svg"), linear-gradient(#ffffff, #ffffff); + -webkit-mask-composite: destination-out; + mask-composite: exclude; + mask-position: calc(50% + var(--click-shift-x)) calc(50% + var(--click-shift-y)), center center; + mask-repeat: no-repeat; + animation: 500ms ease-in circle-cut forwards; + + .dots { + transform: scale(1.2); + } +} + +.canvas { + display: block; + width: 100%; + height: 100%; +} + +.dots { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + --background-url: url("../../assets/spoilers/turbulence_1x.png"); + --background-size: 256px; + background: rgba(0, 0, 0, 0.25) var(--background-url); + background-size: var(--background-size) var(--background-size); + + transition: transform 500ms ease-in; + transform-origin: calc(50% + var(--click-shift-x)) calc(50% + var(--click-shift-y)); + + @media (-webkit-min-device-pixel-ratio: 2) { + --background-url: url("../../assets/spoilers/turbulence_2x.png"); + } + + @media (-webkit-min-device-pixel-ratio: 3) { + --background-url: url("../../assets/spoilers/turbulence_3x.png"); + } + + --x-direction: var(--background-size); + --y-direction: 0; + animation: 20s linear infinite dots; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: var(--background-url); + background-size: var(--background-size) var(--background-size); + + --x-direction: 0; + --y-direction: var(--background-size); + animation: 20s linear -7s infinite dots; + } + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: var(--background-url); + background-size: var(--background-size) var(--background-size); + + --x-direction: calc(-1 * var(--background-size)); + --y-direction: calc(-1 * var(--background-size)); + animation: 20s linear -14s infinite dots; + } +} + +@keyframes dots { + 0% { + background-position: 0 0; + } + 100% { + background-position: var(--x-direction) var(--y-direction); + } +} + +@keyframes circle-cut { + 0% { + mask-size: 0%, 100%; + } + + 100% { + mask-size: 350%, 100%; + } +} diff --git a/src/components/common/MediaSpoiler.tsx b/src/components/common/MediaSpoiler.tsx new file mode 100644 index 000000000..902ba9646 --- /dev/null +++ b/src/components/common/MediaSpoiler.tsx @@ -0,0 +1,65 @@ +import React, { memo, useCallback, useRef } from '../../lib/teact/teact'; + +import type { FC } from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; +import useCanvasBlur from '../../hooks/useCanvasBlur'; +import useShowTransition from '../../hooks/useShowTransition'; + +import styles from './MediaSpoiler.module.scss'; + +type OwnProps = { + isVisible: boolean; + withAnimation?: boolean; + thumbDataUri?: string; + width?: number; + height?: number; + className?: string; +}; + +const BLUR_RADIUS = 25; +const ANIMATION_DURATION = 500; + +const MediaSpoiler: FC = ({ + isVisible, + withAnimation, + thumbDataUri, + className, + width, + height, +}) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + const { shouldRender, transitionClassNames } = useShowTransition( + isVisible, undefined, true, withAnimation ? false : undefined, undefined, ANIMATION_DURATION, + ); + const canvasRef = useCanvasBlur(thumbDataUri, !shouldRender, undefined, BLUR_RADIUS, width, height); + + const handleClick = useCallback((e: React.MouseEvent) => { + if (!ref.current) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const shiftX = x - (rect.width / 2); + const shiftY = y - (rect.height / 2); + ref.current.setAttribute('style', `--click-shift-x: ${shiftX}px; --click-shift-y: ${shiftY}px`); + }, []); + + if (!shouldRender) { + return undefined; + } + + return ( +
+ +
+
+ ); +}; + +export default memo(MediaSpoiler); diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index 55d63dc9f..59e4017c0 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -24,6 +24,7 @@ import telegramLogoPath from '../../assets/telegram-logo.svg'; import reactionThumbsPath from '../../assets/reaction-thumbs.png'; import lockPreviewPath from '../../assets/lock.png'; import monkeyPath from '../../assets/monkey.svg'; +import spoilerMaskPath from '../../assets/spoilers/mask.svg'; export type UiLoaderPage = 'main' @@ -76,6 +77,7 @@ const preloadTasks = { .then(preloadFonts), preloadAvatars(), preloadImage(reactionThumbsPath), + preloadImage(spoilerMaskPath), ]), authPhoneNumber: () => Promise.all([ preloadFonts(), diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 3937ee0e4..2a0be3913 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -250,6 +250,10 @@ margin-inline-end: 0.25rem; } + .media-preview-spoiler { + filter: blur(1px); + } + .media-preview--image { width: 1.25rem; height: 1.25rem; @@ -257,6 +261,7 @@ border-radius: 0.125rem; vertical-align: -0.25rem; margin-inline-end: 0.25rem; + margin-inline-start: 0.125rem; body.is-ios & { width: 1.125rem; diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index e6d22b1ea..7bdb3744c 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -12,6 +12,7 @@ import type { Thread } from '../../../../global/types'; import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { + getMessageIsSpoiler, getMessageMediaHash, getMessageMediaThumbDataUri, getMessageRoundVideo, getMessageSenderName, getMessageSticker, getMessageVideo, isActionMessage, isChatChannel, @@ -214,9 +215,17 @@ function renderSummary( return messageSummary; } + const isSpoiler = getMessageIsSpoiler(message); + return ( - + {getMessageVideo(message) && } {messageSummary} diff --git a/src/components/left/search/ChatMessage.scss b/src/components/left/search/ChatMessage.scss index 28df1c3a1..d6a7819a9 100644 --- a/src/components/left/search/ChatMessage.scss +++ b/src/components/left/search/ChatMessage.scss @@ -53,13 +53,18 @@ } } + .media-preview-spoiler { + filter: blur(1px); + } + .media-preview--image { width: 1.25rem; height: 1.25rem; object-fit: cover; border-radius: 0.125rem; vertical-align: -0.25rem; - margin-right: 0.25rem; + margin-inline-start: 0.125rem; + margin-inline-end: 0.25rem; } .icon-play { diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index f0e6f2ec3..847c5a455 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -15,6 +15,7 @@ import { getMessageVideo, getMessageRoundVideo, getMessageSticker, + getMessageIsSpoiler, } from '../../../global/helpers'; import { selectChat, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; @@ -120,9 +121,17 @@ function renderSummary( return renderMessageSummary(lang, message, undefined, searchQuery); } + const isSpoiler = getMessageIsSpoiler(message); + return ( - + {getMessageVideo(message) && } {renderMessageSummary(lang, message, true, searchQuery)} diff --git a/src/components/middle/HeaderPinnedMessage.tsx b/src/components/middle/HeaderPinnedMessage.tsx index d8bf1f1f6..648ea6ffd 100644 --- a/src/components/middle/HeaderPinnedMessage.tsx +++ b/src/components/middle/HeaderPinnedMessage.tsx @@ -5,7 +5,10 @@ import { getActions } from '../../global'; import type { ApiMessage } from '../../api/types'; import { getPictogramDimensions } from '../common/helpers/mediaDimensions'; -import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers'; +import { + getMessageIsSpoiler, + getMessageMediaHash, getMessageSingleInlineButton, +} from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV } from '../../util/environment'; import renderText from '../common/helpers/renderText'; @@ -20,6 +23,7 @@ import ConfirmDialog from '../ui/ConfirmDialog'; import Button from '../ui/Button'; import PinnedMessageNavigation from './PinnedMessageNavigation'; import MessageSummary from '../common/MessageSummary'; +import MediaSpoiler from '../common/MediaSpoiler'; type OwnProps = { message: ApiMessage; @@ -40,6 +44,8 @@ const HeaderPinnedMessage: FC = ({ const mediaThumbnail = useThumbnail(message); const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram')); + const isSpoiler = getMessageIsSpoiler(message); + const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag(); const handleUnpinMessage = useCallback(() => { @@ -102,7 +108,7 @@ const HeaderPinnedMessage: FC = ({ count={count} index={index} /> - {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl)} + {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isSpoiler)}
{customTitle ? renderText(customTitle) : `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`} @@ -129,11 +135,15 @@ const HeaderPinnedMessage: FC = ({ ); }; -function renderPictogram(thumbDataUri: string, blobUrl?: string) { +function renderPictogram(thumbDataUri: string, blobUrl?: string, isSpoiler?: boolean) { const { width, height } = getPictogramDimensions(); + const srcUrl = blobUrl || thumbDataUri; return ( - +
+ {!isSpoiler && } + +
); } diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index e321b709d..b2564a9f8 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -566,19 +566,26 @@ height: 1rem; } - & > img { + .pinned-thumb { + position: relative; width: 2.25rem; height: 2.25rem; - object-fit: cover; - border-radius: 0.25rem; + margin-inline-start: 0.375rem; margin-top: 0.125rem; flex-shrink: 0; + border-radius: 0.25rem; + overflow: hidden; + & + .message-text { max-width: 12rem; } } + + .pinned-thumb-image { + object-fit: cover; + } } .HeaderActions { diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index 599b777f7..98b768726 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -3,6 +3,7 @@ .modal-dialog { max-width: 26.25rem; + @media (max-width: 600px) { max-height: 100%; } @@ -10,20 +11,30 @@ .modal-header-condensed { padding: 0.3125rem 0.5rem !important; - border-bottom: 1px solid var(--color-borders); + border-bottom: 1px solid transparent; + + transition: border-color 250ms ease-in-out; + } + + &.modal-header-border .modal-header-condensed { + border-bottom-color: var(--color-borders); } .modal-content { - padding: 0; - max-height: calc(100vh - 3.25rem); + display: flex; + flex-direction: column; + padding: 0 0 0.5rem; + // Full height - header - margins + max-height: calc(100vh - 3.25rem - 5rem); overflow-x: auto; } .attachments-wrapper { max-height: 26rem; + min-height: 13rem; overflow: auto; - flex-shrink: 0; + flex-shrink: 1; display: flex; flex-wrap: wrap; @@ -46,7 +57,9 @@ left: -0.5rem; top: 0; right: -0.5rem; - border-top: 1px solid var(--color-borders); + border-top: 1px solid transparent; + + transition: border-color 250ms ease-in-out; } .form-control { @@ -59,8 +72,13 @@ } } + .caption-top-border::before { + border-top-color: var(--color-borders); + } + .attachment-caption { display: flex; + align-items: center; gap: 0.5rem; } @@ -69,6 +87,9 @@ } .drop-target { + display: flex; + flex-direction: column; + position: relative; overflow: hidden; @@ -140,12 +161,12 @@ .AttachmentModal--send-wrapper { align-self: flex-end; - margin-right: 0.25rem; + padding-bottom: 0.25rem; + margin-right: 0.375rem; } .AttachmentModal--send { height: 2.5rem; padding: 0 1rem; - margin-bottom: 0.25rem; } } diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 72665db44..baa47d376 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -32,6 +32,8 @@ import useFlag from '../../../hooks/useFlag'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import { useStateRef } from '../../../hooks/useStateRef'; import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip'; +import useAppLayout from '../../../hooks/useAppLayout'; +import useScrolledState from '../../../hooks/useScrolledState'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; @@ -57,9 +59,9 @@ export type OwnProps = { shouldSuggestCompression?: boolean; onCaptionUpdate: (html: string) => void; onSend: (sendCompressed: boolean, sendGrouped: boolean) => void; - onFileAppend: (files: File[]) => void; - onDelete: (attachmentIndex: number) => void; - onClear: () => void; + onFileAppend: (files: File[], isSpoiler?: boolean) => void; + onAttachmentsUpdate: (attachments: ApiAttachment[]) => void; + onClear: NoneToVoidFunction; onSendSilent: (sendCompressed: boolean, sendGrouped: boolean) => void; onSendScheduled: (sendCompressed: boolean, sendGrouped: boolean) => void; }; @@ -99,10 +101,10 @@ const AttachmentModal: FC = ({ customEmojiForEmoji, attachmentSettings, shouldSuggestCompression, + onAttachmentsUpdate, onCaptionUpdate, onSend, onFileAppend, - onDelete, onClear, onSendSilent, onSendScheduled, @@ -121,6 +123,14 @@ const AttachmentModal: FC = ({ ); const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped); + const { + handleScroll: handleAttachmentsScroll, + isAtBeginning: areAttachmentsNotScrolled, + isAtEnd: areAttachmentsScrolledToBottom, + } = useScrolledState(); + + const { handleScroll: handleCaptionScroll, isAtBeginning: isCaptionNotScrolled } = useScrolledState(); + const isOpen = Boolean(attachments.length); const [isHovered, markHovered, unmarkHovered] = useFlag(); @@ -131,6 +141,13 @@ const AttachmentModal: FC = ({ return [oneMedia, false]; }, [renderingAttachments]); + const [hasSpoiler, isEverySpoiler] = useMemo(() => { + const areAllSpoilers = Boolean(renderingAttachments?.every((a) => a.shouldSendAsSpoiler)); + if (areAllSpoilers) return [true, true]; + const hasOneSpoiler = Boolean(renderingAttachments?.some((a) => a.shouldSendAsSpoiler)); + return [hasOneSpoiler, false]; + }, [renderingAttachments]); + const { isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, } = useMentionTooltip( @@ -240,9 +257,9 @@ const AttachmentModal: FC = ({ const files = await getFilesFromDataTransferItems(dataTransfer.items); if (files?.length) { - onFileAppend(files); + onFileAppend(files, isEverySpoiler); } - }, [onFileAppend, unmarkHovered]); + }, [isEverySpoiler, onFileAppend, unmarkHovered]); function handleDragOver(e: React.MouseEvent) { e.preventDefault(); @@ -258,14 +275,39 @@ const AttachmentModal: FC = ({ const validatedFiles = validateFiles(files); if (validatedFiles?.length) { - onFileAppend(validatedFiles); + onFileAppend(validatedFiles, isEverySpoiler); } - }, [onFileAppend]); + }, [isEverySpoiler, onFileAppend]); const handleDocumentSelect = useCallback(() => { openSystemFilesDialog('*', (e) => handleFileSelect(e)); }, [handleFileSelect]); + const handleDelete = useCallback((index: number) => { + onAttachmentsUpdate(attachments.filter((a, i) => i !== index)); + }, [attachments, onAttachmentsUpdate]); + + const handleEnableSpoilers = useCallback(() => { + onAttachmentsUpdate(attachments.map((a) => ({ ...a, shouldSendAsSpoiler: true }))); + }, [attachments, onAttachmentsUpdate]); + + const handleDisableSpoilers = useCallback(() => { + onAttachmentsUpdate(attachments.map((a) => ({ ...a, shouldSendAsSpoiler: undefined }))); + }, [attachments, onAttachmentsUpdate]); + + const handleToggleSpoiler = useCallback((index: number) => { + onAttachmentsUpdate(attachments.map((attachment, i) => { + if (i === index) { + return { + ...attachment, + shouldSendAsSpoiler: !attachment.shouldSendAsSpoiler || undefined, + }; + } + + return attachment; + })); + }, [attachments, onAttachmentsUpdate]); + const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { return ({ onTrigger, isOpen: isMenuOpen }) => (