SVG: Use Teact rendering (#3585)
This commit is contained in:
parent
46d9278900
commit
7a5088e11c
@ -30,7 +30,8 @@
|
||||
"error",
|
||||
{
|
||||
"code": 120,
|
||||
"ignoreComments": true
|
||||
"ignoreComments": true,
|
||||
"ignorePattern": "\\sd=\".+\"" // Ignore lines with "d" attribute
|
||||
}
|
||||
],
|
||||
"array-bracket-newline": [
|
||||
|
||||
@ -73,8 +73,6 @@ export const WITH_AVATAR_TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width:
|
||||
const AVG_VOICE_DURATION = 10;
|
||||
// This is needed for browsers requiring user interaction before playing.
|
||||
const PRELOAD = true;
|
||||
// eslint-disable-next-line max-len
|
||||
const TRANSCRIBE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" class="loading-svg"><rect class="loading-rect" fill="transparent" width="32" height="24" stroke-width="3" stroke-linejoin="round" rx="6" ry="6" stroke="var(--accent-color)" stroke-dashoffset="1" stroke-dasharray="32,68"></rect></svg>';
|
||||
|
||||
const Audio: FC<OwnProps> = ({
|
||||
theme,
|
||||
@ -252,10 +250,6 @@ const Audio: FC<OwnProps> = ({
|
||||
});
|
||||
}, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]);
|
||||
|
||||
const transcribeSvgMemo = useMemo(() => (
|
||||
<div dangerouslySetInnerHTML={{ __html: TRANSCRIBE_SVG }} />
|
||||
), []);
|
||||
|
||||
function renderFirstLine() {
|
||||
if (isVoice) {
|
||||
return senderTitle || 'Voice';
|
||||
@ -406,7 +400,6 @@ const Audio: FC<OwnProps> = ({
|
||||
isTranscriptionHidden,
|
||||
isTranscribed,
|
||||
isTranscriptionError,
|
||||
transcribeSvgMemo,
|
||||
canTranscribe ? handleTranscribe : undefined,
|
||||
onHideTranscription,
|
||||
)
|
||||
@ -494,7 +487,6 @@ function renderVoice(
|
||||
isTranscriptionHidden?: boolean,
|
||||
isTranscribed?: boolean,
|
||||
isTranscriptionError?: boolean,
|
||||
svgMemo?: React.ReactNode,
|
||||
onClickTranscribe?: VoidFunction,
|
||||
onHideTranscription?: (isHidden: boolean) => void,
|
||||
) {
|
||||
@ -525,7 +517,23 @@ function renderVoice(
|
||||
(isTranscribed || isTranscriptionError) && !isTranscriptionHidden && 'transcribe-shown',
|
||||
)}
|
||||
/>
|
||||
{isTranscribing && svgMemo}
|
||||
{isTranscribing && (
|
||||
<svg viewBox="0 0 32 24" className="loading-svg">
|
||||
<rect
|
||||
className="loading-rect"
|
||||
fill="transparent"
|
||||
width="32"
|
||||
height="24"
|
||||
stroke-width="3"
|
||||
stroke-linejoin="round"
|
||||
rx="6"
|
||||
ry="6"
|
||||
stroke="var(--accent-color)"
|
||||
stroke-dashoffset="1"
|
||||
stroke-dasharray="32,68"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import generateUniqueId from '../../util/generateUniqueId';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import './PremiumIcon.scss';
|
||||
import useUniqueId from '../../hooks/useUniqueId';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const PREMIUM_ICON = { __html: '<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z" fill="var(--color-fill)"/></svg>' };
|
||||
import './PremiumIcon.scss';
|
||||
|
||||
type OwnProps = {
|
||||
withGradient?: boolean;
|
||||
@ -17,15 +14,16 @@ type OwnProps = {
|
||||
onClick?: VoidFunction;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z';
|
||||
|
||||
const PremiumIcon: FC<OwnProps> = ({
|
||||
withGradient,
|
||||
big,
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const html = useMemo(() => {
|
||||
return withGradient ? getPremiumIconGradient() : PREMIUM_ICON;
|
||||
}, [withGradient]);
|
||||
const randomId = useUniqueId();
|
||||
|
||||
return (
|
||||
<i
|
||||
@ -33,18 +31,26 @@ const PremiumIcon: FC<OwnProps> = ({
|
||||
className={buildClassName(
|
||||
'PremiumIcon', className, withGradient && 'gradient', onClick && 'clickable', big && 'big',
|
||||
)}
|
||||
dangerouslySetInnerHTML={html}
|
||||
title="Premium"
|
||||
/>
|
||||
>
|
||||
{withGradient ? (
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="none">
|
||||
<defs>
|
||||
<linearGradient id={randomId} x1="3" y1="63.5001" x2="84.1475" y2="-1.32262" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6B93FF" />
|
||||
<stop offset="0.439058" stop-color="#976FFF" />
|
||||
<stop offset="1" stop-color="#E46ACE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill={`url(#${randomId})`} />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill="var(--color-fill)" />
|
||||
</svg>
|
||||
)}
|
||||
</i>
|
||||
);
|
||||
};
|
||||
|
||||
function getPremiumIconGradient() {
|
||||
const id = generateUniqueId();
|
||||
return {
|
||||
// eslint-disable-next-line max-len
|
||||
__html: `<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="${id}" x1="3" y1="63.5001" x2="84.1475" y2="-1.32262" gradientUnits="userSpaceOnUse"><stop stop-color="#6B93FF"/><stop offset="0.439058" stop-color="#976FFF"/><stop offset="1" stop-color="#E46ACE"/></linearGradient></defs><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z" fill="url(#${id})"/></svg>`,
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(PremiumIcon);
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import './VerifiedIcon.scss';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const VERIFIED_ICON = { __html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.3 2.9c.1.1.2.1.3.2.7.6 1.3 1.1 2 1.7.3.2.6.4.9.4.9.1 1.7.2 2.6.2.5 0 .6.1.7.7.1.9.1 1.8.2 2.6 0 .4.2.7.4 1 .6.7 1.1 1.3 1.7 2 .3.4.3.5 0 .8-.5.6-1.1 1.3-1.6 1.9-.3.3-.5.7-.5 1.2-.1.8-.2 1.7-.2 2.5 0 .4-.2.5-.6.6-.8 0-1.6.1-2.5.2-.5 0-1 .2-1.4.5-.6.5-1.3 1.1-1.9 1.6-.3.3-.5.3-.8 0-.7-.6-1.4-1.2-2-1.8-.3-.2-.6-.4-.9-.4-.9-.1-1.8-.2-2.7-.2-.4 0-.5-.2-.6-.5 0-.9-.1-1.7-.2-2.6 0-.4-.2-.8-.4-1.1-.6-.6-1.1-1.3-1.6-2-.4-.4-.3-.5 0-1 .6-.6 1.1-1.3 1.7-1.9.3-.3.4-.6.4-1 0-.8.1-1.6.2-2.5 0-.5.1-.6.6-.6.9-.1 1.7-.1 2.6-.2.4 0 .7-.2 1-.4.7-.6 1.4-1.2 2.1-1.7.1-.2.3-.3.5-.2z" style="fill: var(--color-fill)"/><path class="lol" d="M16.4 10.1l-.2.2-5.4 5.4c-.1.1-.2.2-.4 0l-2.6-2.6c-.2-.2-.1-.3 0-.4.2-.2.5-.6.7-.6.3 0 .5.4.7.6l1.1 1.1c.2.2.3.2.5 0l4.3-4.3c.2-.2.4-.3.6 0 .1.2.3.3.4.5.2 0 .3.1.3.1z" style="fill: var(--color-checkmark)"/></svg>' };
|
||||
|
||||
const VerifiedIcon: FC = () => {
|
||||
return (
|
||||
// eslint-disable-next-line react/no-danger
|
||||
<span className="VerifiedIcon" dangerouslySetInnerHTML={VERIFIED_ICON} />
|
||||
<svg className="VerifiedIcon" viewBox="0 0 24 24">
|
||||
<path d="M12.3 2.9c.1.1.2.1.3.2.7.6 1.3 1.1 2 1.7.3.2.6.4.9.4.9.1 1.7.2 2.6.2.5 0 .6.1.7.7.1.9.1 1.8.2 2.6 0 .4.2.7.4 1 .6.7 1.1 1.3 1.7 2 .3.4.3.5 0 .8-.5.6-1.1 1.3-1.6 1.9-.3.3-.5.7-.5 1.2-.1.8-.2 1.7-.2 2.5 0 .4-.2.5-.6.6-.8 0-1.6.1-2.5.2-.5 0-1 .2-1.4.5-.6.5-1.3 1.1-1.9 1.6-.3.3-.5.3-.8 0-.7-.6-1.4-1.2-2-1.8-.3-.2-.6-.4-.9-.4-.9-.1-1.8-.2-2.7-.2-.4 0-.5-.2-.6-.5 0-.9-.1-1.7-.2-2.6 0-.4-.2-.8-.4-1.1-.6-.6-1.1-1.3-1.6-2-.4-.4-.3-.5 0-1 .6-.6 1.1-1.3 1.7-1.9.3-.3.4-.6.4-1 0-.8.1-1.6.2-2.5 0-.5.1-.6.6-.6.9-.1 1.7-.1 2.6-.2.4 0 .7-.2 1-.4.7-.6 1.4-1.2 2.1-1.7.1-.2.3-.3.5-.2z" style="fill: var(--color-fill)" />
|
||||
<path d="M16.4 10.1l-.2.2-5.4 5.4c-.1.1-.2.2-.4 0l-2.6-2.6c-.2-.2-.1-.3 0-.4.2-.2.5-.6.7-.6.3 0 .5.4.7.6l1.1 1.1c.2.2.3.2.5 0l4.3-4.3c.2-.2.4-.3.6 0 .1.2.3.3.4.5.2 0 .3.1.3.1z" style="fill: var(--color-checkmark)" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
import styles from './PremiumLimitsCompare.module.scss';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const TRIANGLE_SVG = '<svg width="26" height="9" viewBox="0 0 26 9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H26H24.4853C22.894 0 21.3679 0.632141 20.2426 1.75736L14.4142 7.58579C13.6332 8.36684 12.3668 8.36683 11.5858 7.58579L5.75736 1.75736C4.63214 0.632139 3.10602 0 1.51472 0H0Z" fill="#7E85FF"/></svg>';
|
||||
|
||||
type OwnProps = {
|
||||
floatingBadgeIcon?: string;
|
||||
leftValue?: string;
|
||||
@ -32,7 +29,11 @@ const PremiumLimitsCompare: FC<OwnProps> = ({
|
||||
<div className={styles.floatingBadge}>
|
||||
<i className={buildClassName(styles.floatingBadgeIcon, floatingBadgeIcon, 'icon')} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>{leftValue}</div>
|
||||
<div className={styles.floatingBadgeTriangle} dangerouslySetInnerHTML={{ __html: TRIANGLE_SVG }} />
|
||||
<div className={styles.floatingBadgeTriangle}>
|
||||
<svg width="26" height="9" viewBox="0 0 26 9" fill="none">
|
||||
<path d="M0 0H26H24.4853C22.894 0 21.3679 0.632141 20.2426 1.75736L14.4142 7.58579C13.6332 8.36684 12.3668 8.36683 11.5858 7.58579L5.75736 1.75736C4.63214 0.632139 3.10602 0 1.51472 0H0Z" fill="#7E85FF" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={buildClassName(styles.line, styles.left)}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
@ -229,8 +229,6 @@ const SELECT_MODE_TRANSITION_MS = 200;
|
||||
const MESSAGE_MAX_LENGTH = 4096;
|
||||
const SENDING_ANIMATION_DURATION = 350;
|
||||
const MOUNT_ANIMATION_DURATION = 430;
|
||||
// eslint-disable-next-line max-len
|
||||
const APPENDIX = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#FFF" class="corner"/></g></svg>';
|
||||
|
||||
const Composer: FC<OwnProps & StateProps> = ({
|
||||
isOnActiveTab,
|
||||
@ -314,8 +312,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const appendixRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -364,12 +360,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
shouldAnimateSendAsButtonRef.current = Boolean(chatId === prevChatId && sendAsPeerIds && !prevSendAsPeerIds);
|
||||
}, [chatId, sendAsPeerIds]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!appendixRef.current) return;
|
||||
|
||||
appendixRef.current.innerHTML = APPENDIX;
|
||||
}, []);
|
||||
|
||||
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
|
||||
const hasAttachments = Boolean(attachments.length);
|
||||
const [nextText, setNextText] = useState<ApiFormattedText | undefined>(undefined);
|
||||
@ -1339,7 +1329,29 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onClose={closeBotCommandTooltip}
|
||||
/>
|
||||
<div id="message-compose">
|
||||
<div className="svg-appendix" ref={appendixRef} />
|
||||
<svg className="svg-appendix" width="9" height="20">
|
||||
<defs>
|
||||
<filter
|
||||
x="-50%"
|
||||
y="-14.7%"
|
||||
width="200%"
|
||||
height="141.2%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="composerAppendix"
|
||||
>
|
||||
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0"
|
||||
in="shadowBlurOuter1"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#composerAppendix)" />
|
||||
<path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#FFF" className="corner" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<InlineBotTooltip
|
||||
isOpen={isInlineBotTooltipOpen}
|
||||
|
||||
@ -35,6 +35,8 @@ import './Location.scss';
|
||||
|
||||
import mapPin from '../../../assets/map-pin.svg';
|
||||
|
||||
const TIMER_RADIUS = 12;
|
||||
const TIMER_CIRCUMFERENCE = TIMER_RADIUS * 2 * Math.PI;
|
||||
const MOVE_THRESHOLD = 0.0001; // ~11m
|
||||
const DEFAULT_MAP_CONFIG = {
|
||||
width: 400,
|
||||
@ -43,9 +45,6 @@ const DEFAULT_MAP_CONFIG = {
|
||||
scale: 2,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const SVG_PIN = { __html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" class="round-pin" style="enable-background:new 0 0 64 64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="24.5"/><path d="M32 8c13.23 0 24 10.77 24 24S45.23 56 32 56 8 45.23 8 32 18.77 8 32 8m0-1C18.19 7 7 18.19 7 32s11.19 25 25 25 25-11.19 25-25S45.81 7 32 7z"/><path d="m29.38 57.67-1.98-1.59 3.02-1.66L32 51.54l1.58 2.88 3.02 1.66-1.91 1.53L32 60.73z"/><path d="m32 52.58 1.07 1.95.14.26.26.14 2.24 1.22-1.33 1.06-.07.06-.06.07L32 59.96l-2.24-2.61-.06-.07-.07-.06-1.33-1.06 2.24-1.22.26-.14.14-.26L32 52.58m0-2.08-1.94 3.56L26.5 56l2.5 2 3 3.5 3-3.5 2.5-2-3.56-1.94L32 50.5z"/></svg>' };
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
peer?: ApiUser | ApiChat;
|
||||
@ -102,28 +101,14 @@ const Location: FC<OwnProps> = ({
|
||||
|
||||
const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => {
|
||||
if (type !== 'geoLive') return;
|
||||
const radius = 12;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
const svgEl = countdownEl.lastElementChild;
|
||||
const timerEl = countdownEl.firstElementChild as SVGElement;
|
||||
const svgEl = countdownEl.lastElementChild!;
|
||||
const timerEl = countdownEl.firstElementChild!;
|
||||
|
||||
const timeLeft = message.date + location.period - getServerTime();
|
||||
const strokeDashOffset = (1 - timeLeft / location.period) * circumference;
|
||||
const strokeDashOffset = (1 - timeLeft / location.period) * TIMER_CIRCUMFERENCE;
|
||||
const text = formatCountdownShort(lang, timeLeft * 1000);
|
||||
|
||||
if (!svgEl || !timerEl) {
|
||||
countdownEl.innerHTML = `
|
||||
<span class="geo-countdown-text">${text}</span>
|
||||
<svg width="32px" height="32px">
|
||||
<circle cx="16" cy="16" r="${radius}" class="geo-countdown-progress" transform="rotate(-90, 16, 16)"
|
||||
stroke-dasharray="${circumference} ${circumference}"
|
||||
stroke-dashoffset="-${strokeDashOffset}"
|
||||
/>
|
||||
</svg>`;
|
||||
} else {
|
||||
timerEl.textContent = text;
|
||||
svgEl.firstElementChild!.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||
}
|
||||
timerEl.textContent = text;
|
||||
svgEl.firstElementChild!.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -180,7 +165,22 @@ const Location: FC<OwnProps> = ({
|
||||
<div className="location-info-subtitle">
|
||||
{formatLastUpdated(lang, serverTime, message.editDate)}
|
||||
</div>
|
||||
{!isExpired && <div className="geo-countdown" ref={countdownRef} />}
|
||||
{!isExpired && (
|
||||
<div className="geo-countdown" ref={countdownRef}>
|
||||
<span className="geo-countdown-text" />
|
||||
<svg width="32px" height="32px">
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r={TIMER_RADIUS}
|
||||
className="geo-countdown-progress"
|
||||
transform="rotate(-90, 16, 16)"
|
||||
stroke-dasharray={TIMER_CIRCUMFERENCE}
|
||||
stroke-dashoffset="0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -207,7 +207,8 @@ const Location: FC<OwnProps> = ({
|
||||
);
|
||||
if (type === 'geoLive') {
|
||||
return (
|
||||
<div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN}>
|
||||
<div className={pinClassName}>
|
||||
<PinSvg />
|
||||
<Avatar peer={peer} className="location-avatar" />
|
||||
{location.heading !== undefined && (
|
||||
<div className="direction" style={`--direction: ${location.heading}deg`} />
|
||||
@ -221,7 +222,8 @@ const Location: FC<OwnProps> = ({
|
||||
const iconSrc = getVenueIconUrl(location.venueType);
|
||||
if (iconSrc) {
|
||||
return (
|
||||
<div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN} style={`--pin-color: ${color}`}>
|
||||
<div className={pinClassName} style={`--pin-color: ${color}`}>
|
||||
<PinSvg />
|
||||
<img src={iconSrc} className="venue-icon" alt="" />
|
||||
</div>
|
||||
);
|
||||
@ -264,4 +266,15 @@ const Location: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function PinSvg() {
|
||||
return (
|
||||
<svg className="round-pin" style="enable-background:new 0 0 64 64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="24.5" />
|
||||
<path d="M32 8c13.23 0 24 10.77 24 24S45.23 56 32 56 8 45.23 8 32 18.77 8 32 8m0-1C18.19 7 7 18.19 7 32s11.19 25 25 25 25-11.19 25-25S45.81 7 32 7z" />
|
||||
<path d="m29.38 57.67-1.98-1.59 3.02-1.66L32 51.54l1.58 2.88 3.02 1.66-1.91 1.53L32 60.73z" />
|
||||
<path d="m32 52.58 1.07 1.95.14.26.26.14 2.24 1.22-1.33 1.06-.07.06-.06.07L32 59.96l-2.24-2.61-.06-.07-.07-.06-1.33-1.06 2.24-1.22.26-.14.14-.26L32 52.58m0-2.08-1.94 3.56L26.5 56l2.5 2 3 3.5 3-3.5 2.5-2-3.56-1.94L32 50.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Location);
|
||||
|
||||
@ -3,7 +3,6 @@ import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@ -275,10 +274,6 @@ type QuickReactionPosition =
|
||||
| 'in-meta';
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
// eslint-disable-next-line max-len
|
||||
const APPENDIX_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>' };
|
||||
// eslint-disable-next-line max-len
|
||||
const APPENDIX_NOT_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>' };
|
||||
const APPEARANCE_DELAY = 10;
|
||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||
const QUICK_REACTION_SIZE = 1.75 * REM;
|
||||
@ -287,9 +282,6 @@ const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5;
|
||||
const THROTTLE_MS = 300;
|
||||
const RESIZE_ANIMATION_DURATION = 400;
|
||||
|
||||
let appendixOwnCloned: SVGElement;
|
||||
let appendixNotOwnCloned: SVGElement;
|
||||
|
||||
const Message: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
chatUsernames,
|
||||
@ -389,8 +381,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const bottomMarkerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const quickReactionRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const appendixRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const messageHeightRef = useRef(0);
|
||||
|
||||
@ -726,30 +716,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [hasUnreadReaction, messageId, animateUnreadReaction]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!withAppendix) return;
|
||||
|
||||
const appendixEl = appendixRef.current!;
|
||||
const cloned = isOwn ? appendixOwnCloned : appendixNotOwnCloned;
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const html = isOwn ? APPENDIX_OWN.__html : APPENDIX_NOT_OWN.__html;
|
||||
let nextCloned: SVGElement;
|
||||
|
||||
if (cloned) {
|
||||
nextCloned = cloned.cloneNode(true) as SVGElement;
|
||||
appendixEl.appendChild(cloned);
|
||||
} else {
|
||||
appendixEl.innerHTML = html;
|
||||
nextCloned = appendixEl.firstChild!.cloneNode(true) as SVGElement;
|
||||
}
|
||||
|
||||
if (isOwn) {
|
||||
appendixOwnCloned = nextCloned;
|
||||
} else {
|
||||
appendixNotOwnCloned = nextCloned;
|
||||
}
|
||||
}, [isOwn, withAppendix]);
|
||||
|
||||
let style = '';
|
||||
let calculatedWidth;
|
||||
let reactionsMaxWidth;
|
||||
@ -1325,9 +1291,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
) : undefined}
|
||||
{withCommentButton && <CommentButton threadInfo={repliesThreadInfo!} disabled={noComments} />}
|
||||
{withAppendix && (
|
||||
<div ref={appendixRef} className="svg-appendix" />
|
||||
)}
|
||||
{withAppendix && <MessageAppendix isOwn={isOwn} />}
|
||||
{withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()}
|
||||
</div>
|
||||
{message.inlineButtons && (
|
||||
@ -1367,6 +1331,30 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function MessageAppendix({ isOwn } : { isOwn: boolean }) {
|
||||
const path = isOwn
|
||||
? 'M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z'
|
||||
: 'M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z';
|
||||
return (
|
||||
<svg width="9" height="20" className="svg-appendix">
|
||||
<defs>
|
||||
<filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="messageAppendix">
|
||||
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0"
|
||||
in="shadowBlurOuter1"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d={path} fill="#000" filter="url(#messageAppendix)" />
|
||||
<path d={path} fill={isOwn ? '#EEFFDE' : 'FFF'} className="corner" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps): StateProps => {
|
||||
const {
|
||||
|
||||
@ -18,7 +18,7 @@ import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntit
|
||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||
import type { LangFn } from '../../../hooks/useLang';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
@ -44,6 +44,9 @@ type StateProps = {
|
||||
|
||||
const SOLUTION_CONTAINER_ID = '#middle-column-portals';
|
||||
const SOLUTION_DURATION = 5000;
|
||||
const TIMER_RADIUS = 6;
|
||||
const TIMER_CIRCUMFERENCE = TIMER_RADIUS * 2 * Math.PI;
|
||||
const TIMER_UPDATE_INTERVAL = 1000;
|
||||
const NBSP = '\u00A0';
|
||||
|
||||
const Poll: FC<OwnProps & StateProps> = ({
|
||||
@ -63,29 +66,27 @@ const Poll: FC<OwnProps & StateProps> = ({
|
||||
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
|
||||
const [closePeriod, setClosePeriod] = useState<number>(
|
||||
!summary.closed && summary.closeDate && summary.closeDate > 0
|
||||
? Math.min(summary.closeDate - Math.floor(Date.now() / 1000) + getServerTimeOffset(), summary.closePeriod!)
|
||||
? Math.min(summary.closeDate - getServerTime(), summary.closePeriod!)
|
||||
: 0,
|
||||
);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const countdownRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const timerCircleRef = useRef<SVGCircleElement>(null);
|
||||
const { results: voteResults, totalVoters } = results;
|
||||
const hasVoted = voteResults && voteResults.some((r) => r.isChosen);
|
||||
const canVote = !summary.closed && !hasVoted;
|
||||
const canViewResult = !canVote && summary.isPublic && Number(results.totalVoters) > 0;
|
||||
const isMultiple = canVote && summary.multipleChoice;
|
||||
const maxVotersCount = voteResults ? Math.max(...voteResults.map((r) => r.votersCount)) : totalVoters;
|
||||
const correctResults = voteResults ? voteResults.reduce((answers: string[], r) => {
|
||||
if (r.isCorrect) {
|
||||
answers.push(r.option);
|
||||
}
|
||||
|
||||
return answers;
|
||||
}, []) : [];
|
||||
const answers = summary.answers.map((a) => ({
|
||||
const correctResults = useMemo(() => {
|
||||
return voteResults?.filter((r) => r.isCorrect).map((r) => r.option) || [];
|
||||
}, [voteResults]);
|
||||
const answers = useMemo(() => summary.answers.map((a) => ({
|
||||
label: a.text,
|
||||
value: a.option,
|
||||
hidden: Boolean(summary.quiz && summary.closePeriod && closePeriod <= 0),
|
||||
}));
|
||||
})), [closePeriod, summary]);
|
||||
|
||||
useEffect(() => {
|
||||
const chosen = poll.results.results?.find((result) => result.isChosen);
|
||||
@ -99,34 +100,16 @@ const Poll: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (closePeriod > 0) {
|
||||
setTimeout(() => setClosePeriod(closePeriod - 1), 1000);
|
||||
setTimeout(() => setClosePeriod(closePeriod - 1), TIMER_UPDATE_INTERVAL);
|
||||
}
|
||||
if (!timerCircleRef.current) return;
|
||||
|
||||
if (closePeriod <= 5) {
|
||||
countdownRef.current!.classList.add('hurry-up');
|
||||
}
|
||||
|
||||
const countdownEl = countdownRef.current;
|
||||
|
||||
if (countdownEl) {
|
||||
const circumference = 6 * 2 * Math.PI;
|
||||
const svgEl = countdownEl.lastElementChild;
|
||||
const timerEl = countdownEl.firstElementChild;
|
||||
if (closePeriod <= 5) {
|
||||
countdownEl.classList.add('hurry-up');
|
||||
}
|
||||
|
||||
if (!svgEl || !timerEl) {
|
||||
countdownEl.innerHTML = `
|
||||
<span>${formatMediaDuration(closePeriod)}</span>
|
||||
<svg width="16px" height="16px">
|
||||
<circle cx="8" cy="8" r="6" class="poll-countdown-progress" transform="rotate(-90, 8, 8)"
|
||||
stroke-dasharray="${circumference} ${circumference}"
|
||||
stroke-dashoffset="0"
|
||||
/>
|
||||
</svg>`;
|
||||
} else {
|
||||
const strokeDashOffset = ((summary.closePeriod! - closePeriod) / summary.closePeriod!) * circumference;
|
||||
timerEl.textContent = formatMediaDuration(closePeriod);
|
||||
(svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||
}
|
||||
}
|
||||
const strokeDashOffset = ((summary.closePeriod! - closePeriod) / summary.closePeriod!) * TIMER_CIRCUMFERENCE;
|
||||
timerCircleRef.current.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||
}, [closePeriod, summary.closePeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -255,7 +238,23 @@ const Poll: FC<OwnProps & StateProps> = ({
|
||||
<div className="poll-type">
|
||||
{lang(getPollTypeString(summary))}
|
||||
{renderRecentVoters()}
|
||||
{closePeriod > 0 && canVote && <div ref={countdownRef} className="poll-countdown" />}
|
||||
{closePeriod > 0 && canVote && (
|
||||
<div ref={countdownRef} className="poll-countdown">
|
||||
<span>{formatMediaDuration(closePeriod)}</span>
|
||||
<svg width="16px" height="16px">
|
||||
<circle
|
||||
ref={timerCircleRef}
|
||||
cx="8"
|
||||
cy="8"
|
||||
r={TIMER_RADIUS}
|
||||
className="poll-countdown-progress"
|
||||
transform="rotate(-90, 8, 8)"
|
||||
stroke-dasharray={TIMER_CIRCUMFERENCE}
|
||||
stroke-dashoffset="0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{summary.quiz && poll.results.solution && !canVote && (
|
||||
<Button
|
||||
round
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
useState, useEffect, useRef, useLayoutEffect,
|
||||
useState, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiPollAnswer, ApiPollResult } from '../../../api/types';
|
||||
@ -32,8 +32,6 @@ const PollOption: FC<OwnProps> = ({
|
||||
const showIcon = (correctResults.length > 0 && correctAnswer) || (result?.isChosen);
|
||||
const answerPercent = result ? getPercentage(result.votersCount, totalVoters || 0) : 0;
|
||||
const [finalPercent, setFinalPercent] = useState(shouldAnimate ? 0 : answerPercent);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const lineRef = useRef<HTMLDivElement>(null);
|
||||
const lineWidth = result ? getPercentage(result.votersCount, maxVotersCount || 0) : 0;
|
||||
const isAnimationDoesNotStart = finalPercent !== answerPercent;
|
||||
|
||||
@ -43,24 +41,6 @@ const PollOption: FC<OwnProps> = ({
|
||||
}
|
||||
}, [shouldAnimate, answerPercent]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const lineEl = lineRef.current;
|
||||
|
||||
if (lineEl && shouldAnimate) {
|
||||
const svgEl = lineEl.firstElementChild;
|
||||
|
||||
const style = isAnimationDoesNotStart ? '' : 'stroke-dasharray: 100% 200%; stroke-dashoffset: -44';
|
||||
if (!svgEl) {
|
||||
lineEl.innerHTML = `
|
||||
<svg class="poll-line" xmlns="http://www.w3.org/2000/svg" style="${style}">
|
||||
<path d="M4.47 5.33v13.6a9 9 0 009 9h13"/>
|
||||
</svg>`;
|
||||
} else {
|
||||
svgEl.setAttribute('style', style);
|
||||
}
|
||||
}
|
||||
}, [isAnimationDoesNotStart, shouldAnimate]);
|
||||
|
||||
if (!voteResults || !result) {
|
||||
return undefined;
|
||||
}
|
||||
@ -87,7 +67,14 @@ const PollOption: FC<OwnProps> = ({
|
||||
{renderText(answer.text)}
|
||||
</div>
|
||||
<div className={buildClassName('poll-option-answer', showIcon && !correctAnswer && 'wrong')}>
|
||||
<div className="poll-option-corner" ref={lineRef} />
|
||||
{shouldAnimate && (
|
||||
<svg
|
||||
className="poll-line"
|
||||
style={!isAnimationDoesNotStart ? 'stroke-dasharray: 100% 200%; stroke-dashoffset: -44' : ''}
|
||||
>
|
||||
<path d="M4.47 5.33v13.6a9 9 0 009 9h13" />
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
className="poll-option-line"
|
||||
style={lineStyle}
|
||||
|
||||
@ -5,7 +5,6 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
@ -42,6 +41,9 @@ type OwnProps = {
|
||||
isDownloading?: boolean;
|
||||
};
|
||||
|
||||
const PROGRESS_CENTER = ROUND_VIDEO_DIMENSIONS_PX / 2;
|
||||
const PROGRESS_MARGIN = 6;
|
||||
const PROGRESS_CIRCUMFERENCE = (PROGRESS_CENTER - PROGRESS_MARGIN) * 2 * Math.PI;
|
||||
const PROGRESS_THROTTLE = 16; // Min period needed for `playerEl.currentTime` to update
|
||||
|
||||
let stopPrevious: NoneToVoidFunction;
|
||||
@ -55,9 +57,9 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const playingProgressRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const playerRef = useRef<HTMLVideoElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const circleRef = useRef<SVGCircleElement>(null);
|
||||
|
||||
const video = message.content.video!;
|
||||
|
||||
@ -106,29 +108,12 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
}, [setProgress, isActivated, getThrottledProgress]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isActivated) {
|
||||
if (!isActivated || !circleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svgCenter = ROUND_VIDEO_DIMENSIONS_PX / 2;
|
||||
const svgMargin = 6;
|
||||
const circumference = (svgCenter - svgMargin) * 2 * Math.PI;
|
||||
const strokeDashOffset = circumference - getThrottledProgress() * circumference;
|
||||
const playingProgressEl = playingProgressRef.current!;
|
||||
const svgEl = playingProgressEl.firstElementChild;
|
||||
|
||||
if (!svgEl) {
|
||||
playingProgressEl.innerHTML = `
|
||||
<svg width="${ROUND_VIDEO_DIMENSIONS_PX}px" height="${ROUND_VIDEO_DIMENSIONS_PX}px">
|
||||
<circle cx="${svgCenter}" cy="${svgCenter}" r="${svgCenter - svgMargin}" class="progress-circle"
|
||||
transform="rotate(-90, ${svgCenter}, ${svgCenter})"
|
||||
stroke-dasharray="${circumference} ${circumference}"
|
||||
stroke-dashoffset="${circumference}"
|
||||
/>
|
||||
</svg>`;
|
||||
} else {
|
||||
(svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString());
|
||||
}
|
||||
const strokeDashOffset = PROGRESS_CIRCUMFERENCE - getThrottledProgress() * PROGRESS_CIRCUMFERENCE;
|
||||
circleRef.current.setAttribute('stroke-dashoffset', strokeDashOffset.toString());
|
||||
}, [isActivated, getThrottledProgress]);
|
||||
|
||||
const shouldPlay = Boolean(mediaData && isIntersecting);
|
||||
@ -141,10 +126,6 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
setIsActivated(false);
|
||||
setProgress(0);
|
||||
safePlay(playerRef.current);
|
||||
|
||||
requestMutation(() => {
|
||||
playingProgressRef.current!.innerHTML = '';
|
||||
});
|
||||
});
|
||||
|
||||
const capturePlaying = useLastCallback(() => {
|
||||
@ -221,7 +202,22 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
<div className="progress" ref={playingProgressRef} />
|
||||
<div className="progress">
|
||||
{isActivated && (
|
||||
<svg width={ROUND_VIDEO_DIMENSIONS_PX} height={ROUND_VIDEO_DIMENSIONS_PX}>
|
||||
<circle
|
||||
ref={circleRef}
|
||||
cx={PROGRESS_CENTER}
|
||||
cy={PROGRESS_CENTER}
|
||||
r={PROGRESS_CENTER - PROGRESS_MARGIN}
|
||||
className="progress-circle"
|
||||
transform={`rotate(-90, ${PROGRESS_CENTER}, ${PROGRESS_CENTER})`}
|
||||
stroke-dasharray={PROGRESS_CIRCUMFERENCE}
|
||||
stroke-dashoffset={PROGRESS_CIRCUMFERENCE}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{shouldSpinnerRender && (
|
||||
<div className={`media-loading ${spinnerClassNames}`}>
|
||||
<ProgressSpinner progress={isDownloading ? downloadProgress : loadProgress} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef, memo, useLayoutEffect } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
@ -31,36 +31,7 @@ const ProgressSpinner: FC<{
|
||||
const circleRadius = radius - STROKE_WIDTH * 2;
|
||||
const borderRadius = radius - 1;
|
||||
const circumference = circleRadius * 2 * Math.PI;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const svg = container.firstElementChild;
|
||||
const strokeDashOffset = circumference - Math.min(Math.max(MIN_PROGRESS, progress), MAX_PROGRESS) * circumference;
|
||||
|
||||
if (!svg) {
|
||||
container.innerHTML = `<svg
|
||||
viewBox="0 0 ${borderRadius * 2} ${borderRadius * 2}"
|
||||
height="${borderRadius * 2}"
|
||||
width="${borderRadius * 2}"
|
||||
>
|
||||
<circle
|
||||
stroke="white"
|
||||
fill="transparent"
|
||||
stroke-width=${STROKE_WIDTH}
|
||||
stroke-dasharray="${circumference} ${circumference}"}
|
||||
stroke-dashoffset="${strokeDashOffset}"
|
||||
stroke-linecap="round"
|
||||
r=${circleRadius}
|
||||
cx=${borderRadius}
|
||||
cy=${borderRadius}
|
||||
/>
|
||||
</svg>`;
|
||||
} else {
|
||||
(svg.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString());
|
||||
}
|
||||
}, [containerRef, circumference, borderRadius, circleRadius, progress]);
|
||||
const strokeDashOffset = circumference - Math.min(Math.max(MIN_PROGRESS, progress), MAX_PROGRESS) * circumference;
|
||||
|
||||
const className = buildClassName(
|
||||
`ProgressSpinner size-${size}`,
|
||||
@ -71,10 +42,27 @@ const ProgressSpinner: FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${borderRadius * 2} ${borderRadius * 2}`}
|
||||
height={borderRadius * 2}
|
||||
width={borderRadius * 2}
|
||||
>
|
||||
<circle
|
||||
stroke="white"
|
||||
fill="transparent"
|
||||
stroke-width={STROKE_WIDTH}
|
||||
stroke-dasharray={`${circumference} ${circumference}`}
|
||||
stroke-dashoffset={strokeDashOffset}
|
||||
stroke-linecap="round"
|
||||
r={circleRadius}
|
||||
cx={borderRadius}
|
||||
cy={borderRadius}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user