Media Viewer: Fix document real size (#6177)

This commit is contained in:
zubiden 2025-09-19 14:35:13 +02:00 committed by Alexander Zinchuk
parent eb7dff433c
commit c7bf1ebd72
32 changed files with 163 additions and 74 deletions

View File

@ -136,6 +136,7 @@ export default tseslint.config(
disallowTypeAnnotations: false, disallowTypeAnnotations: false,
}, },
], ],
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-call': 'off',

View File

@ -153,7 +153,7 @@ export interface ApiDocument {
previewPhotoSizes?: ApiPhotoSize[]; previewPhotoSizes?: ApiPhotoSize[];
previewBlobUrl?: string; previewBlobUrl?: string;
innerMediaType?: 'photo' | 'video'; innerMediaType?: 'photo' | 'video';
mediaSize?: ApiDimensions & { fromDocumentAttribute?: boolean }; mediaSize?: ApiDimensions & { fromDocumentAttribute?: boolean; fromPreload?: true };
} }
export interface ApiContact { export interface ApiContact {

View File

@ -15,6 +15,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia'; import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia';
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
import { preloadDocumentMedia } from './helpers/preloadDocumentMedia';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
@ -119,10 +120,26 @@ const Document = ({
const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined; const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined;
const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting); 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; 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(() => { const handleDownload = useLastCallback(() => {
downloadMedia({ media: document, originMessage: message }); downloadMedia({ media: document, originMessage: message });
}); });

View File

@ -225,21 +225,21 @@ const PremiumProgress: FC<OwnProps> = ({
const renderProgressLayer = ( const renderProgressLayer = (
isPositive: boolean, isPositive: boolean,
layerProgress: number, currentProgress: number,
layerClassName?: string, layerClassName?: string,
disableTransition?: boolean, disableTransition?: boolean,
) => { ) => {
const className = isPositive ? styles.positiveProgress : styles.negativeProgress; const typeClass = isPositive ? styles.positiveProgress : styles.negativeProgress;
const progressVar = '--layer-progress'; const progressVar = '--layer-progress';
return ( return (
<div <div
className={buildClassName( className={buildClassName(
className, typeClass,
layerClassName, layerClassName,
disableTransition && styles.noTransition, disableTransition && styles.noTransition,
)} )}
style={`${progressVar}: ${layerProgress}`} style={`${progressVar}: ${currentProgress}`}
> >
<div className={styles.left}> <div className={styles.left}>
<span>{displayLeftText}</span> <span>{displayLeftText}</span>

View File

@ -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<string, void>(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);
}

View File

@ -64,6 +64,7 @@ type StateProps = {
enum ContentType { enum ContentType {
Main, Main,
// eslint-disable-next-line @typescript-eslint/no-shadow
Settings, Settings,
Archived, Archived,

View File

@ -257,14 +257,14 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
}, [globalSearchChatId, selectedSearchDate]); }, [globalSearchChatId, selectedSearchDate]);
const version = useMemo(() => { const version = useMemo(() => {
let version = ''; let fullVersion = '';
if (IS_TAURI && window.tauri.version) { 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]); }, [versionString]);
return ( return (

View File

@ -109,7 +109,7 @@ export const useMediaProps = ({
} }
if (isDocument) { if (isDocument) {
return media.mediaSize!; return media.mediaSize || FALLBACK_DIMENSIONS;
} }
if (isPhoto) { if (isPhoto) {

View File

@ -145,12 +145,15 @@ type StateProps = {
}; };
enum Content { enum Content {
// eslint-disable-next-line @typescript-eslint/no-shadow
Loading, Loading,
Restricted, Restricted,
StarsRequired, StarsRequired,
PremiumRequired, PremiumRequired,
AccountInfo, AccountInfo,
// eslint-disable-next-line @typescript-eslint/no-shadow
ContactGreeting, ContactGreeting,
// eslint-disable-next-line @typescript-eslint/no-shadow
NoMessages, NoMessages,
MessageList, MessageList,
} }
@ -180,6 +183,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isChannelWithAvatars, isChannelWithAvatars,
canPost, canPost,
isSynced, isSynced,
// eslint-disable-next-line @typescript-eslint/no-shadow
isChatMonoforum, isChatMonoforum,
isReady, isReady,
isChatWithSelf, isChatWithSelf,

View File

@ -478,8 +478,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
const everyPhoto = renderingAttachments.every((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType)); 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 everyVideo = renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType));
const everyAudio = renderingAttachments.every((a) => SUPPORTED_AUDIO_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)); const anyPhoto = renderingAttachments.some((a) => SUPPORTED_PHOTO_CONTENT_TYPES.has(a.mimeType));
return [everyPhoto, everyVideo, everyAudio, hasAnyPhoto]; return [everyPhoto, everyVideo, everyAudio, anyPhoto];
}, [renderingAttachments, isQuickGallery]); }, [renderingAttachments, isQuickGallery]);
const hasAnySpoilerable = useMemo(() => { const hasAnySpoilerable = useMemo(() => {

View File

@ -287,13 +287,14 @@ const ToDoListModal = ({
}); });
function renderHeader() { function renderHeader() {
const title = isAddTaskMode ? 'TitleAppendToDoList' : editingMessage ? 'TitleEditToDoList' : 'TitleNewToDoList'; const modalTitle = isAddTaskMode ? 'TitleAppendToDoList'
: editingMessage ? 'TitleEditToDoList' : 'TitleNewToDoList';
return ( return (
<div className="modal-header-condensed"> <div className="modal-header-condensed">
<Button round color="translucent" size="smaller" ariaLabel={lang('AriaToDoCancel')} onClick={onClear}> <Button round color="translucent" size="smaller" ariaLabel={lang('AriaToDoCancel')} onClick={onClear}>
<Icon name="close" /> <Icon name="close" />
</Button> </Button>
<div className="modal-title">{lang(title)}</div> <div className="modal-title">{lang(modalTitle)}</div>
<Button <Button
color="primary" color="primary"
size="smaller" size="smaller"

View File

@ -784,10 +784,6 @@ const ActionMessageText = ({
case 'suggestedPostRefund': { case 'suggestedPostRefund': {
const { payerInitiated } = action; const { payerInitiated } = action;
const replyMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(global, chatId, message.replyInfo.replyToMsgId)
: undefined;
const postSender = replyMessage ? selectSender(global, replyMessage) : sender; const postSender = replyMessage ? selectSender(global, replyMessage) : sender;
const postSenderTitle = postSender && getPeerTitle(lang, postSender); const postSenderTitle = postSender && getPeerTitle(lang, postSender);
const postSenderLink = renderPeerLink(postSender?.id, postSenderTitle || userFallbackText, asPreview); const postSenderLink = renderPeerLink(postSender?.id, postSenderTitle || userFallbackText, asPreview);
@ -822,9 +818,6 @@ const ActionMessageText = ({
const { isRejected, isBalanceTooLow, rejectComment } = action; const { isRejected, isBalanceTooLow, rejectComment } = action;
if (isRejected) { if (isRejected) {
const senderTitle = sender && getPeerTitle(lang, sender);
const senderLink = renderPeerLink(sender?.id, senderTitle || userFallbackText, asPreview);
return translateWithYou( return translateWithYou(
lang, lang,
rejectComment ? 'SuggestedPostRejectedWithReason' : 'SuggestedPostRejected', rejectComment ? 'SuggestedPostRejectedWithReason' : 'SuggestedPostRejected',
@ -835,10 +828,6 @@ const ActionMessageText = ({
} }
if (isBalanceTooLow) { if (isBalanceTooLow) {
const replyMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(global, chatId, message.replyInfo.replyToMsgId)
: undefined;
const replyMessageSender = replyMessage ? selectSender(global, replyMessage) : sender; const replyMessageSender = replyMessage ? selectSender(global, replyMessage) : sender;
const replyPeerTitle = replyMessageSender && getPeerTitle(lang, replyMessageSender); const replyPeerTitle = replyMessageSender && getPeerTitle(lang, replyMessageSender);
const userLink = renderPeerLink(replyMessageSender?.id, replyPeerTitle || userFallbackText, asPreview); const userLink = renderPeerLink(replyMessageSender?.id, replyPeerTitle || userFallbackText, asPreview);

View File

@ -42,11 +42,11 @@ function SpeedingDiamond({ onMouseMove }: OwnProps) {
if (!isAnimating) return false; if (!isAnimating) return false;
const t = Math.min((Date.now() - startAt) / SLOWDOWN_DURATION, 1); const t = Math.min((Date.now() - startAt) / SLOWDOWN_DURATION, 1);
const speed = (MAX_SPEED - MIN_SPEED) * (1 - transition(t)); const newSpeed = (MAX_SPEED - MIN_SPEED) * (1 - transition(t));
setSpeed(speed); setSpeed(newSpeed);
isAnimating = t < 1 && speed > 1; isAnimating = t < 1 && newSpeed > 1;
return isAnimating; return isAnimating;
}, requestMutation); }, requestMutation);

View File

@ -136,10 +136,10 @@ const GiftModalResaleScreen: FC<OwnProps & StateProps> = ({
preloadBackwards={RESALE_GIFTS_LIMIT} preloadBackwards={RESALE_GIFTS_LIMIT}
scrollContainerClosest={`.${styles.resaleScreenRoot}`} scrollContainerClosest={`.${styles.resaleScreenRoot}`}
> >
{resellGifts?.map((gift) => ( {resellGifts?.map((g) => (
<GiftItemStar <GiftItemStar
key={gift.id} key={g.id}
gift={gift} gift={g}
observeIntersection={observe} observeIntersection={observe}
isResale isResale
onClick={onGiftClick} onClick={onGiftClick}

View File

@ -276,7 +276,7 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const handleModelMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeModel) => { const handleModelMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeModel) => {
if (!counters) return; if (!counters) return;
const attributes = filter.modelAttributes || []; const modelAttributes = filter.modelAttributes || [];
const modelAttribute const modelAttribute
= counters.find((counter): counter is ApiStarGiftAttributeCounter<StarGiftAttributeIdModel> => = counters.find((counter): counter is ApiStarGiftAttributeCounter<StarGiftAttributeIdModel> =>
counter.attribute.type === 'model' && counter.attribute.documentId === attribute.sticker.id, counter.attribute.type === 'model' && counter.attribute.documentId === attribute.sticker.id,
@ -284,10 +284,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
if (!modelAttribute) return; if (!modelAttribute) return;
const isActive = attributes.some((item) => item.documentId === modelAttribute.documentId); const isActive = modelAttributes.some((item) => item.documentId === modelAttribute.documentId);
const updatedAttributes = isActive const updatedAttributes = isActive
? attributes.filter((item) => item.documentId !== modelAttribute.documentId) ? modelAttributes.filter((item) => item.documentId !== modelAttribute.documentId)
: [...attributes, modelAttribute]; : [...modelAttributes, modelAttribute];
updateResaleGiftsFilter({ filter: { updateResaleGiftsFilter({ filter: {
...filter, ...filter,
modelAttributes: updatedAttributes, modelAttributes: updatedAttributes,
@ -296,7 +296,7 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const handlePatternMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributePattern) => { const handlePatternMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributePattern) => {
if (!counters) return; if (!counters) return;
const attributes = filter.patternAttributes || []; const patternAttributes = filter.patternAttributes || [];
const patternAttribute = counters.find( const patternAttribute = counters.find(
(counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdPattern> => (counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdPattern> =>
counter.attribute.type === 'pattern' && counter.attribute.documentId === attribute.sticker.id, counter.attribute.type === 'pattern' && counter.attribute.documentId === attribute.sticker.id,
@ -304,10 +304,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
if (!patternAttribute) return; if (!patternAttribute) return;
const isActive = attributes.some((item) => item.documentId === patternAttribute.documentId); const isActive = patternAttributes.some((item) => item.documentId === patternAttribute.documentId);
const updatedAttributes = isActive const updatedAttributes = isActive
? attributes.filter((item) => item.documentId !== patternAttribute.documentId) ? patternAttributes.filter((item) => item.documentId !== patternAttribute.documentId)
: [...attributes, patternAttribute]; : [...patternAttributes, patternAttribute];
updateResaleGiftsFilter({ filter: { updateResaleGiftsFilter({ filter: {
...filter, ...filter,
patternAttributes: updatedAttributes, patternAttributes: updatedAttributes,
@ -316,7 +316,7 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const handleBackdropMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeBackdrop) => { const handleBackdropMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeBackdrop) => {
if (!counters) return; if (!counters) return;
const attributes = filter.backdropAttributes || []; const backdropAttributes = filter.backdropAttributes || [];
const backdropAttribute = counters.find( const backdropAttribute = counters.find(
(counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdBackdrop> => (counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdBackdrop> =>
counter.attribute.type === 'backdrop' && counter.attribute.backdropId === attribute.backdropId, counter.attribute.type === 'backdrop' && counter.attribute.backdropId === attribute.backdropId,
@ -324,10 +324,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
if (!backdropAttribute) return; if (!backdropAttribute) return;
const isActive = attributes.some((item) => item.backdropId === backdropAttribute.backdropId); const isActive = backdropAttributes.some((item) => item.backdropId === backdropAttribute.backdropId);
const updatedAttributes = isActive const updatedAttributes = isActive
? attributes.filter((item) => item.backdropId !== backdropAttribute.backdropId) ? backdropAttributes.filter((item) => item.backdropId !== backdropAttribute.backdropId)
: [...attributes, backdropAttribute]; : [...backdropAttributes, backdropAttribute];
updateResaleGiftsFilter({ filter: { updateResaleGiftsFilter({ filter: {
...filter, ...filter,
backdropAttributes: updatedAttributes, backdropAttributes: updatedAttributes,

View File

@ -218,12 +218,12 @@ const GiftInfoModal = ({
: gift?.ownerId === currentUserId || isSelfUnique : gift?.ownerId === currentUserId || isSelfUnique
); );
function getResalePrice(shouldPayInTon?: boolean) { function getResalePrice(isInTon?: boolean) {
if (!isGiftUnique) return undefined; if (!isGiftUnique) return undefined;
const amounts = gift.resellPrice; const amounts = gift.resellPrice;
if (!amounts) return undefined; if (!amounts) return undefined;
if (gift?.resaleTonOnly || shouldPayInTon) { if (gift?.resaleTonOnly || isInTon) {
return amounts.find((amount) => amount.currency === TON_CURRENCY_CODE); return amounts.find((amount) => amount.currency === TON_CURRENCY_CODE);
} }

View File

@ -180,9 +180,9 @@ function MessageStatistics({
<div ref={containerRef}> <div ref={containerRef}>
{GRAPHS.map((graph) => { {GRAPHS.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); const isGraphReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return ( return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} /> <div className={buildClassName(styles.graph, !isGraphReady && styles.hidden)} />
); );
})} })}
</div> </div>

View File

@ -211,9 +211,9 @@ const Statistics = ({
<div ref={containerRef}> <div ref={containerRef}>
{graphs.map((graph) => { {graphs.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); const isGraphReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return ( return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} /> <div className={buildClassName(styles.graph, !isGraphReady && styles.hidden)} />
); );
})} })}
</div> </div>

View File

@ -185,9 +185,9 @@ function StoryStatistics({
<div ref={containerRef}> <div ref={containerRef}>
{GRAPHS.map((graph) => { {GRAPHS.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); const isGraphReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return ( return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} /> <div className={buildClassName(styles.graph, !isGraphReady && styles.hidden)} />
); );
})} })}
</div> </div>

View File

@ -9,7 +9,7 @@ import type {
} from '../../types'; } from '../../types';
import { PaymentStep } from '../../../types'; import { PaymentStep } from '../../../types';
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config'; import { DEBUG_PAYMENT_SMART_GLOCAL, STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
import { getStripeError } from '../../../util/payments/stripe'; import { getStripeError } from '../../../util/payments/stripe';
@ -1085,13 +1085,14 @@ async function payInputStarInvoice<T extends GlobalState>(
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const actions = getActions(); const actions = getActions();
const isTon = inputInvoice.type === 'stargiftResale' && inputInvoice.currency === 'TON'; const isTon = inputInvoice.type === 'stargiftResale' && inputInvoice.currency === TON_CURRENCY_CODE;
const balance = isTon ? global.ton?.balance : global.stars?.balance; const balance = isTon ? global.ton?.balance : global.stars?.balance;
const currency = isTon ? TON_CURRENCY_CODE : STARS_CURRENCY_CODE;
if (balance === undefined) return; if (balance === undefined) return;
if (balance.amount < price) { if (balance.amount < price) {
actions.openStarsBalanceModal({ currency: isTon ? 'TON' : 'XTR', tabId }); actions.openStarsBalanceModal({ currency, tabId });
return; return;
} }
@ -1126,12 +1127,10 @@ async function payInputStarInvoice<T extends GlobalState>(
const formPrice = form.invoice.totalAmount; const formPrice = form.invoice.totalAmount;
if (formPrice !== price) { if (formPrice !== price) {
const isTon = inputInvoice.type === 'stargiftResale' && inputInvoice.currency === 'TON';
actions.openPriceConfirmModal({ actions.openPriceConfirmModal({
originalAmount: price, originalAmount: price,
newAmount: formPrice, newAmount: formPrice,
currency: isTon ? 'TON' : 'XTR', currency,
directInfo: { directInfo: {
inputInvoice, inputInvoice,
formId: form.formId, formId: form.formId,

View File

@ -3,6 +3,7 @@ import { useEffect } from '../lib/teact/teact';
import { ApiMediaFormat } from '../api/types'; import { ApiMediaFormat } from '../api/types';
import { selectIsSynced } from '../global/selectors'; import { selectIsSynced } from '../global/selectors';
import { IS_PROGRESSIVE_SUPPORTED } from '../util/browser/windowEnvironment';
import * as mediaLoader from '../util/mediaLoader'; import * as mediaLoader from '../util/mediaLoader';
import useSelector from './data/useSelector'; import useSelector from './data/useSelector';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
@ -13,7 +14,11 @@ const useMedia = (
mediaFormat = ApiMediaFormat.BlobUrl, mediaFormat = ApiMediaFormat.BlobUrl,
delay?: number | false, delay?: number | false,
) => { ) => {
const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; const isStreaming = IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive;
const mediaData = mediaHash
? (isStreaming ? mediaLoader.getProgressiveUrl(mediaHash)
: mediaLoader.getFromMemory(mediaHash)) : undefined;
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const isSynced = useSelector(selectIsSynced); const isSynced = useSelector(selectIsSynced);

View File

@ -23,8 +23,11 @@ export default function useMediaWithLoadProgress(
delay?: number | false, delay?: number | false,
isHtmlAllowed = false, isHtmlAllowed = false,
) { ) {
const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined;
const isStreaming = IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive; const isStreaming = IS_PROGRESSIVE_SUPPORTED && mediaFormat === ApiMediaFormat.Progressive;
const mediaData = mediaHash
? (isStreaming ? mediaLoader.getProgressiveUrl(mediaHash)
: mediaLoader.getFromMemory(mediaHash)) : undefined;
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const isSynced = useSelector(selectIsSynced); const isSynced = useSelector(selectIsSynced);
const id = useUniqueId(); const id = useUniqueId();

View File

@ -9,7 +9,7 @@ import createMockedChatBannedRights from './createMockedChatBannedRights';
import { MOCK_STARTING_DATE } from './MockTypes'; import { MOCK_STARTING_DATE } from './MockTypes';
export default function createMockedChannel(id: string, mockData: MockTypes): Api.Channel { export default function createMockedChannel(id: string, mockData: MockTypes): Api.Channel {
const channel = mockData.channels.find((channel) => channel.id === id); const channel = mockData.channels.find((c) => c.id === id);
if (!channel) throw Error('No such channel ' + id); if (!channel) throw Error('No such channel ' + id);

View File

@ -7,7 +7,7 @@ import Api from '../../tl/api';
import { MOCK_STARTING_DATE } from './MockTypes'; import { MOCK_STARTING_DATE } from './MockTypes';
export default function createMockedChat(id: string, mockData: MockTypes): Api.Chat { export default function createMockedChat(id: string, mockData: MockTypes): Api.Chat {
const chat = mockData.chats.find((chat) => chat.id === id); const chat = mockData.chats.find((c) => c.id === id);
if (!chat) throw Error('No such chat ' + id); if (!chat) throw Error('No such chat ' + id);

View File

@ -3,7 +3,7 @@ import type { MockTypes } from './MockTypes';
import Api from '../../tl/api'; import Api from '../../tl/api';
export default function createMockedChatAdminRights(chatId: string, mockData: MockTypes) { export default function createMockedChatAdminRights(chatId: string, mockData: MockTypes) {
const channel = mockData.channels.find((channel) => channel.id === chatId); const channel = mockData.channels.find((c) => c.id === chatId);
if (!channel) throw Error('No such channel ' + chatId); if (!channel) throw Error('No such channel ' + chatId);

View File

@ -3,7 +3,7 @@ import type { MockTypes } from './MockTypes';
import Api from '../../tl/api'; import Api from '../../tl/api';
export default function createMockedChatBannedRights(chatId: string, mockData: MockTypes) { export default function createMockedChatBannedRights(chatId: string, mockData: MockTypes) {
const channel = mockData.channels.find((channel) => channel.id === chatId); const channel = mockData.channels.find((c) => c.id === chatId);
if (!channel) throw Error('No such channel ' + chatId); if (!channel) throw Error('No such channel ' + chatId);

View File

@ -4,7 +4,7 @@ import Api from '../../tl/api';
import createMockedTypeInputPeer from './createMockedTypeInputPeer'; import createMockedTypeInputPeer from './createMockedTypeInputPeer';
export default function createMockedDialogFilter(id: number, mockData: MockTypes) { export default function createMockedDialogFilter(id: number, mockData: MockTypes) {
const dialogFilter = mockData.dialogFilters.find((dialogFilter) => dialogFilter.id === id); const dialogFilter = mockData.dialogFilters.find((f) => f.id === id);
if (!dialogFilter) throw Error('No such dialog filter ' + id); if (!dialogFilter) throw Error('No such dialog filter ' + id);

View File

@ -5,7 +5,7 @@ import type { MockTypes } from './MockTypes';
import Api from '../../tl/api'; import Api from '../../tl/api';
export default function createMockedTypeInputPeer(id: string, mockData: MockTypes): Api.TypeInputPeer { export default function createMockedTypeInputPeer(id: string, mockData: MockTypes): Api.TypeInputPeer {
const user = mockData.users.find((user) => user.id === id); const user = mockData.users.find((u) => u.id === id);
if (user) { if (user) {
return new Api.InputPeerUser({ return new Api.InputPeerUser({
userId: BigInt(id), userId: BigInt(id),
@ -13,14 +13,14 @@ export default function createMockedTypeInputPeer(id: string, mockData: MockType
}); });
} }
const chat = mockData.chats.find((chat) => chat.id === id); const chat = mockData.chats.find((c) => c.id === id);
if (chat) { if (chat) {
return new Api.InputPeerChat({ return new Api.InputPeerChat({
chatId: BigInt(id), chatId: BigInt(id),
}); });
} }
const channel = mockData.channels.find((channel) => channel.id === id); const channel = mockData.channels.find((c) => c.id === id);
if (channel) { if (channel) {
return new Api.InputPeerChannel({ return new Api.InputPeerChannel({
channelId: BigInt(Number(id) + 1000000000), channelId: BigInt(Number(id) + 1000000000),

View File

@ -5,21 +5,21 @@ import type { MockTypes } from './MockTypes';
import Api from '../../tl/api'; import Api from '../../tl/api';
export default function createMockedTypePeer(id: string, mockData: MockTypes): Api.TypePeer { export default function createMockedTypePeer(id: string, mockData: MockTypes): Api.TypePeer {
const user = mockData.users.find((user) => user.id === id); const user = mockData.users.find((u) => u.id === id);
if (user) { if (user) {
return new Api.PeerUser({ return new Api.PeerUser({
userId: BigInt(id), userId: BigInt(id),
}); });
} }
const chat = mockData.chats.find((chat) => chat.id === id); const chat = mockData.chats.find((c) => c.id === id);
if (chat) { if (chat) {
return new Api.PeerChat({ return new Api.PeerChat({
chatId: BigInt(id), chatId: BigInt(id),
}); });
} }
const channel = mockData.channels.find((channel) => channel.id === id); const channel = mockData.channels.find((c) => c.id === id);
if (channel) { if (channel) {
return new Api.PeerChannel({ return new Api.PeerChannel({
channelId: BigInt(Number(id) + 1000000000), channelId: BigInt(Number(id) + 1000000000),

View File

@ -5,7 +5,7 @@ import type { MockTypes } from './MockTypes';
import Api from '../../tl/api'; import Api from '../../tl/api';
export default function createMockedUser(id: string, mockData: MockTypes): Api.User { export default function createMockedUser(id: string, mockData: MockTypes): Api.User {
const user = mockData.users.find((user) => user.id === id); const user = mockData.users.find((u) => u.id === id);
if (!user) throw Error('No such user ' + id); if (!user) throw Error('No such user ' + id);

View File

@ -118,7 +118,6 @@ export function removeCallback(url: string, callbackUniqueId: string) {
export function getProgressiveUrl(url: string) { export function getProgressiveUrl(url: string) {
const base = new URL(`${PROGRESSIVE_URL_PREFIX}${url}`, window.location.href); const base = new URL(`${PROGRESSIVE_URL_PREFIX}${url}`, window.location.href);
if (ACCOUNT_SLOT) base.searchParams.set('account', ACCOUNT_SLOT.toString()); if (ACCOUNT_SLOT) base.searchParams.set('account', ACCOUNT_SLOT.toString());
memoryCache.set(url, base.href); // Needed for hooks to detect document as already loaded and apply URL
return base.href; return base.href;
} }

View File

@ -133,7 +133,7 @@ export function fastRaf(callback: NoneToVoidFunction, withTimeoutFallback = fals
if (fastRafCallbacks) { if (fastRafCallbacks) {
const currentCallbacks = fastRafCallbacks; const currentCallbacks = fastRafCallbacks;
currentTimeoutCallbacks.forEach((callback) => currentCallbacks.delete(callback)); currentTimeoutCallbacks.forEach((c) => currentCallbacks.delete(c));
} }
fastRafFallbackCallbacks = undefined; fastRafFallbackCallbacks = undefined;