diff --git a/.eslintrc b/.eslintrc index dde2eaf5e..0090c982f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,7 +30,8 @@ "error", { "code": 120, - "ignoreComments": true + "ignoreComments": true, + "ignorePattern": "\\sd=\".+\"" // Ignore lines with "d" attribute } ], "array-bracket-newline": [ diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index 5045e4314..f5975eb86 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -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 = ''; const Audio: FC = ({ theme, @@ -252,10 +250,6 @@ const Audio: FC = ({ }); }, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]); - const transcribeSvgMemo = useMemo(() => ( -
- ), []); - function renderFirstLine() { if (isVoice) { return senderTitle || 'Voice'; @@ -406,7 +400,6 @@ const Audio: FC = ({ 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 && ( + + + + )} )}
diff --git a/src/components/common/PremiumIcon.tsx b/src/components/common/PremiumIcon.tsx index 39f541357..ec9512887 100644 --- a/src/components/common/PremiumIcon.tsx +++ b/src/components/common/PremiumIcon.tsx @@ -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: '' }; +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 = ({ withGradient, big, className, onClick, }) => { - const html = useMemo(() => { - return withGradient ? getPremiumIconGradient() : PREMIUM_ICON; - }, [withGradient]); + const randomId = useUniqueId(); return ( = ({ className={buildClassName( 'PremiumIcon', className, withGradient && 'gradient', onClick && 'clickable', big && 'big', )} - dangerouslySetInnerHTML={html} title="Premium" - /> + > + {withGradient ? ( + + + + + + + + + + + ) : ( + + + + )} + ); }; -function getPremiumIconGradient() { - const id = generateUniqueId(); - return { - // eslint-disable-next-line max-len - __html: ``, - }; -} - export default memo(PremiumIcon); diff --git a/src/components/common/VerifiedIcon.tsx b/src/components/common/VerifiedIcon.tsx index 6a1e66b23..f1b9ec3eb 100644 --- a/src/components/common/VerifiedIcon.tsx +++ b/src/components/common/VerifiedIcon.tsx @@ -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: '' }; - const VerifiedIcon: FC = () => { return ( - // eslint-disable-next-line react/no-danger - + + + + ); }; diff --git a/src/components/main/premium/common/PremiumLimitsCompare.tsx b/src/components/main/premium/common/PremiumLimitsCompare.tsx index f90d9bb9a..38c1b8074 100644 --- a/src/components/main/premium/common/PremiumLimitsCompare.tsx +++ b/src/components/main/premium/common/PremiumLimitsCompare.tsx @@ -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 = ''; - type OwnProps = { floatingBadgeIcon?: string; leftValue?: string; @@ -32,7 +29,11 @@ const PremiumLimitsCompare: FC = ({
{leftValue}
-
+
+ + + +
)}
diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index b443a5be1..eccd5c060 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -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 = ''; const Composer: FC = ({ isOnActiveTab, @@ -314,8 +312,6 @@ const Composer: FC = ({ const lang = useLang(); - // eslint-disable-next-line no-null/no-null - const appendixRef = useRef(null); // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -364,12 +360,6 @@ const Composer: FC = ({ shouldAnimateSendAsButtonRef.current = Boolean(chatId === prevChatId && sendAsPeerIds && !prevSendAsPeerIds); }, [chatId, sendAsPeerIds]); - useLayoutEffect(() => { - if (!appendixRef.current) return; - - appendixRef.current.innerHTML = APPENDIX; - }, []); - const [attachments, setAttachments] = useState([]); const hasAttachments = Boolean(attachments.length); const [nextText, setNextText] = useState(undefined); @@ -1339,7 +1329,29 @@ const Composer: FC = ({ onClose={closeBotCommandTooltip} />
-
+ + + + + + + + + + + + + ' }; - type OwnProps = { message: ApiMessage; peer?: ApiUser | ApiChat; @@ -102,28 +101,14 @@ const Location: FC = ({ 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 = ` - ${text} - - - `; - } 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 = ({
{formatLastUpdated(lang, serverTime, message.editDate)}
- {!isExpired &&
} + {!isExpired && ( +
+ + + + +
+ )}
); } @@ -207,7 +207,8 @@ const Location: FC = ({ ); if (type === 'geoLive') { return ( -
+
+ {location.heading !== undefined && (
@@ -221,7 +222,8 @@ const Location: FC = ({ const iconSrc = getVenueIconUrl(location.venueType); if (iconSrc) { return ( -
+
+
); @@ -264,4 +266,15 @@ const Location: FC = ({ ); }; +function PinSvg() { + return ( + + + + + + + ); +} + export default memo(Location); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 8da2b99d0..a9969cb61 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -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: '' }; -// eslint-disable-next-line max-len -const APPENDIX_NOT_OWN = { __html: '' }; 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 = ({ message, chatUsernames, @@ -389,8 +381,6 @@ const Message: FC = ({ const bottomMarkerRef = useRef(null); // eslint-disable-next-line no-null/no-null const quickReactionRef = useRef(null); - // eslint-disable-next-line no-null/no-null - const appendixRef = useRef(null); const messageHeightRef = useRef(0); @@ -726,30 +716,6 @@ const Message: FC = ({ } }, [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 = ({ ) : undefined} {withCommentButton && } - {withAppendix && ( -
- )} + {withAppendix && } {withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()}
{message.inlineButtons && ( @@ -1367,6 +1331,30 @@ const Message: FC = ({ ); }; +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 ( + + + + + + + + + + + + + + ); +} + export default memo(withGlobal( (global, ownProps): StateProps => { const { diff --git a/src/components/middle/message/Poll.tsx b/src/components/middle/message/Poll.tsx index 94bdc238c..fccd0d9d1 100644 --- a/src/components/middle/message/Poll.tsx +++ b/src/components/middle/message/Poll.tsx @@ -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 = ({ @@ -63,29 +66,27 @@ const Poll: FC = ({ const [wasSubmitted, setWasSubmitted] = useState(false); const [closePeriod, setClosePeriod] = useState( !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(null); + // eslint-disable-next-line no-null/no-null + const timerCircleRef = useRef(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 = ({ 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 = ` - ${formatMediaDuration(closePeriod)} - - - `; - } 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 = ({
{lang(getPollTypeString(summary))} {renderRecentVoters()} - {closePeriod > 0 && canVote &&
} + {closePeriod > 0 && canVote && ( +
+ {formatMediaDuration(closePeriod)} + + + +
+ )} {summary.quiz && poll.results.solution && !canVote && (
-
+ {shouldAnimate && ( + + + + )}
= ({ // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null - const playingProgressRef = useRef(null); - // eslint-disable-next-line no-null/no-null const playerRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const circleRef = useRef(null); const video = message.content.video!; @@ -106,29 +108,12 @@ const RoundVideo: FC = ({ }, [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 = ` - - - `; - } 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 = ({ setIsActivated(false); setProgress(0); safePlay(playerRef.current); - - requestMutation(() => { - playingProgressRef.current!.innerHTML = ''; - }); }); const capturePlaying = useLastCallback(() => { @@ -221,7 +202,22 @@ const RoundVideo: FC = ({ className={buildClassName('thumbnail', thumbClassNames)} style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`} /> -
+
+ {isActivated && ( + + + + )} +
{shouldSpinnerRender && (
diff --git a/src/components/ui/ProgressSpinner.tsx b/src/components/ui/ProgressSpinner.tsx index 9c4a3a51d..cdc27e4e7 100644 --- a/src/components/ui/ProgressSpinner.tsx +++ b/src/components/ui/ProgressSpinner.tsx @@ -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(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 = ` - - `; - } 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 (
+ > + + + +
); };