SVG: Use Teact rendering (#3585)

This commit is contained in:
Alexander Zinchuk 2023-07-20 15:58:46 +02:00
parent 46d9278900
commit 7a5088e11c
12 changed files with 233 additions and 235 deletions

View File

@ -30,7 +30,8 @@
"error",
{
"code": 120,
"ignoreComments": true
"ignoreComments": true,
"ignorePattern": "\\sd=\".+\"" // Ignore lines with "d" attribute
}
],
"array-bracket-newline": [

View File

@ -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>

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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)}>

View File

@ -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}

View File

@ -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);

View File

@ -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 {

View File

@ -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

View File

@ -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}

View File

@ -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} />

View File

@ -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>
);
};