diff --git a/eslint.config.mjs b/eslint.config.mjs index 32fa12fe2..157d4fc8a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -136,6 +136,7 @@ export default tseslint.config( disallowTypeAnnotations: false, }, ], + '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 70c900066..c494705c6 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -153,7 +153,7 @@ export interface ApiDocument { previewPhotoSizes?: ApiPhotoSize[]; previewBlobUrl?: string; innerMediaType?: 'photo' | 'video'; - mediaSize?: ApiDimensions & { fromDocumentAttribute?: boolean }; + mediaSize?: ApiDimensions & { fromDocumentAttribute?: boolean; fromPreload?: true }; } export interface ApiContact { diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index b9f0d2c2b..e76f8a4e9 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -15,6 +15,7 @@ import { } from '../../global/helpers'; import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; +import { preloadDocumentMedia } from './helpers/preloadDocumentMedia'; import useFlag from '../../hooks/useFlag'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; @@ -119,10 +120,26 @@ const Document = ({ const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined; const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting); - const shouldForceDownload = document.innerMediaType === 'photo' && !document.mediaSize?.fromDocumentAttribute; + const shouldForceDownload = document.innerMediaType === 'photo' && document.mediaSize + && !document.mediaSize.fromDocumentAttribute && !document.mediaSize.fromPreload; const withMediaViewer = onMediaClick && document.innerMediaType && !shouldForceDownload; + useEffect(() => { + const fileEl = ref.current; + if (!withMediaViewer || !fileEl || !message) return; + + const onHover = () => { + preloadDocumentMedia(message); + }; + + fileEl.addEventListener('mouseenter', onHover); + + return () => { + fileEl.removeEventListener('mouseenter', onHover); + }; + }, [withMediaViewer, message]); + const handleDownload = useLastCallback(() => { downloadMedia({ media: document, originMessage: message }); }); diff --git a/src/components/common/PremiumProgress.tsx b/src/components/common/PremiumProgress.tsx index b44812d8e..f240f538f 100644 --- a/src/components/common/PremiumProgress.tsx +++ b/src/components/common/PremiumProgress.tsx @@ -225,21 +225,21 @@ const PremiumProgress: FC = ({ const renderProgressLayer = ( isPositive: boolean, - layerProgress: number, + currentProgress: number, layerClassName?: string, disableTransition?: boolean, ) => { - const className = isPositive ? styles.positiveProgress : styles.negativeProgress; + const typeClass = isPositive ? styles.positiveProgress : styles.negativeProgress; const progressVar = '--layer-progress'; return (
{displayLeftText} diff --git a/src/components/common/helpers/preloadDocumentMedia.ts b/src/components/common/helpers/preloadDocumentMedia.ts new file mode 100644 index 000000000..ffebbf200 --- /dev/null +++ b/src/components/common/helpers/preloadDocumentMedia.ts @@ -0,0 +1,70 @@ +import { getGlobal, setGlobal } from '../../../global'; + +import type { ApiDocument, ApiMessage } from '../../../api/types'; + +import { + getDocumentMediaHash, getMediaFormat, getMessageDocumentPhoto, getMessageDocumentVideo, +} from '../../../global/helpers'; +import { updateChatMessage } from '../../../global/reducers'; +import { selectChatMessage } from '../../../global/selectors'; +import { IS_PROGRESSIVE_SUPPORTED } from '../../../util/browser/windowEnvironment'; +import { preloadImage, preloadVideo } from '../../../util/files'; +import { fetch } from '../../../util/mediaLoader'; +import LimitedMap from '../../../util/primitives/LimitedMap'; + +const preloadedHashes = new LimitedMap(100); + +export async function preloadDocumentMedia(mediaContainer: ApiMessage) { + const video = getMessageDocumentVideo(mediaContainer); + const photo = getMessageDocumentPhoto(mediaContainer); + + const media = video || photo; + + // Skip large photos that were not processed by the server + const shouldSkipPhoto = photo && photo.mediaSize && !photo.mediaSize.fromDocumentAttribute; + if (!media || media.previewBlobUrl || shouldSkipPhoto) { + return; + } + + const hash = getDocumentMediaHash(media, 'full'); + if (!hash || preloadedHashes.has(hash)) { + return; + } + + preloadedHashes.set(hash, undefined); + + const url = await fetch(hash, getMediaFormat(media, 'full')); + if (!url) { + return; + } + + let dimensions: ApiDocument['mediaSize'] | undefined; + + if (video && IS_PROGRESSIVE_SUPPORTED) { + const videoEl = await preloadVideo(url); + dimensions = { width: videoEl.videoWidth, height: videoEl.videoHeight, fromPreload: true }; + } + + if (photo) { + const img = await preloadImage(url); + dimensions = { width: img.naturalWidth, height: img.naturalHeight, fromPreload: true }; + } + + if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) { + return; + } + + let global = getGlobal(); + const message = selectChatMessage(global, mediaContainer.chatId, mediaContainer.id); + if (!message || !message.content.document) return; + global = updateChatMessage(global, mediaContainer.chatId, mediaContainer.id, { + content: { + ...message.content, + document: { + ...message.content.document, + mediaSize: dimensions, + }, + }, + }); + setGlobal(global); +} diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index e019bbfa2..decf055c4 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -64,6 +64,7 @@ type StateProps = { enum ContentType { Main, + // eslint-disable-next-line @typescript-eslint/no-shadow Settings, Archived, diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 305a16078..e00cd9a55 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -257,14 +257,14 @@ const LeftMainHeader: FC = ({ }, [globalSearchChatId, selectedSearchDate]); const version = useMemo(() => { - let version = ''; + let fullVersion = ''; if (IS_TAURI && window.tauri.version) { - version = `Tauri ${window.tauri.version} | `; + fullVersion = `Tauri ${window.tauri.version} | `; } - version += `${APP_NAME} ${versionString}`; + fullVersion += `${APP_NAME} ${versionString}`; - return version; + return fullVersion; }, [versionString]); return ( diff --git a/src/components/mediaViewer/hooks/useMediaProps.ts b/src/components/mediaViewer/hooks/useMediaProps.ts index 32e269b42..9aac2b976 100644 --- a/src/components/mediaViewer/hooks/useMediaProps.ts +++ b/src/components/mediaViewer/hooks/useMediaProps.ts @@ -109,7 +109,7 @@ export const useMediaProps = ({ } if (isDocument) { - return media.mediaSize!; + return media.mediaSize || FALLBACK_DIMENSIONS; } if (isPhoto) { diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 0851a2778..ad6d8d2a6 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -145,12 +145,15 @@ type StateProps = { }; enum Content { + // eslint-disable-next-line @typescript-eslint/no-shadow Loading, Restricted, StarsRequired, PremiumRequired, AccountInfo, + // eslint-disable-next-line @typescript-eslint/no-shadow ContactGreeting, + // eslint-disable-next-line @typescript-eslint/no-shadow NoMessages, MessageList, } @@ -180,6 +183,7 @@ const MessageList: FC = ({ isChannelWithAvatars, canPost, isSynced, + // eslint-disable-next-line @typescript-eslint/no-shadow isChatMonoforum, isReady, isChatWithSelf, diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 02957dda8..9c432cde8 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -478,8 +478,8 @@ const AttachmentModal: FC = ({ 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 hasAnyPhoto = renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)); - return [everyPhoto, everyVideo, everyAudio, hasAnyPhoto]; + const anyPhoto = renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)); + return [everyPhoto, everyVideo, everyAudio, anyPhoto]; }, [renderingAttachments, isQuickGallery]); const hasAnySpoilerable = useMemo(() => { diff --git a/src/components/middle/composer/ToDoListModal.tsx b/src/components/middle/composer/ToDoListModal.tsx index f6c37463d..6006cc7a5 100644 --- a/src/components/middle/composer/ToDoListModal.tsx +++ b/src/components/middle/composer/ToDoListModal.tsx @@ -287,13 +287,14 @@ const ToDoListModal = ({ }); function renderHeader() { - const title = isAddTaskMode ? 'TitleAppendToDoList' : editingMessage ? 'TitleEditToDoList' : 'TitleNewToDoList'; + const modalTitle = isAddTaskMode ? 'TitleAppendToDoList' + : editingMessage ? 'TitleEditToDoList' : 'TitleNewToDoList'; return (
-
{lang(title)}
+
{lang(modalTitle)}