UI: Add super ripple effect (#5081)
This commit is contained in:
parent
63ce255f4c
commit
36eab14839
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="46" height="39"><defs><filter id="a" x="0" y="0" width="46" height="39" filterUnits="userSpaceOnUse"><feOffset dy="1"/><feGaussianBlur stdDeviation="1" result="blur"/><feFlood flood-color="#10232f" flood-opacity=".149"/><feComposite operator="in" in2="blur"/><feComposite in="SourceGraphic"/></filter></defs><g filter="url(#a)"><path data-name="Bubble" d="M36 35H15A12 12 0 013 23v-9A12 12 0 0115 2h15a6 6 0 016 6v10a29.759 29.759 0 002.049 8.782 17.4 17.4 0 004.626 6.48A1 1 0 0142 35z" fill="#eeffde"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 561 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="46.677" height="39"><defs><filter id="a" x="0" y="0" width="46.677" height="39" filterUnits="userSpaceOnUse"><feOffset dy="1"/><feGaussianBlur stdDeviation="1" result="blur"/><feFlood flood-color="#10232f" flood-opacity=".149"/><feComposite operator="in" in2="blur"/><feComposite in="SourceGraphic"/></filter></defs><g filter="url(#a)"><path data-name="Bubble" d="M10.68 35V8a6 6 0 016-6h15a12 12 0 0112 12v9a12 12 0 01-12 12zm-6.676 0a1 1 0 01-.773-1.634l5.4-6.583A33.387 33.387 0 0010.68 18v17z" fill="#fff"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 568 B |
@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
<path fill="#fff" transform="translate(-2, -8)" d="M27.32 10.628H8.44c-3.516 0-5.745 3.792-3.976 6.858l11.801 20.455c.77 1.335 2.7 1.335 3.47 0l11.804-20.455c1.767-3.06-.462-6.858-3.975-6.858zM16.255 31.807l-2.57-4.974-6.202-11.092c-.409-.71.096-1.62.953-1.62h7.816V31.81zM28.51 15.739l-6.2 11.096-2.57 4.972V14.119h7.817c.857 0 1.362.91.953 1.62"/>
|
<path transform="translate(-2, -8)" d="M27.32 10.628H8.44c-3.516 0-5.745 3.792-3.976 6.858l11.801 20.455c.77 1.335 2.7 1.335 3.47 0l11.804-20.455c1.767-3.06-.462-6.858-3.975-6.858zM16.255 31.807l-2.57-4.974-6.202-11.092c-.409-.71.096-1.62.953-1.62h7.816V31.81zM28.51 15.739l-6.2 11.096-2.57 4.972V14.119h7.817c.857 0 1.362.91.953 1.62"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 431 B |
27
src/assets/wave_ripple.svg
Normal file
27
src/assets/wave_ripple.svg
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!-- Displacement map for "shockwave" -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1">
|
||||||
|
<style>
|
||||||
|
#mix {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="red">
|
||||||
|
<stop offset="0%" stop-color="red" />
|
||||||
|
<stop offset="100%" stop-color="red" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="blue" gradientTransform="rotate(90)">
|
||||||
|
<stop offset="0%" stop-color="blue" />
|
||||||
|
<stop offset="100%" stop-color="blue" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="grey">
|
||||||
|
<stop offset="60%" stop-color="grey" stop-opacity="1" />
|
||||||
|
<stop offset="80%" stop-color="grey" stop-opacity="0" />
|
||||||
|
<stop offset="100%" stop-color="grey" stop-opacity="1" />
|
||||||
|
</radialGradient>
|
||||||
|
<g>
|
||||||
|
<rect width="1" height="1" fill="black" />
|
||||||
|
<rect width="1" height="1" fill="url(#red)" />
|
||||||
|
<rect width="1" height="1" fill="url(#blue)" id="mix" />
|
||||||
|
<rect width="1" height="1" fill="url(#grey)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 969 B |
@ -125,6 +125,7 @@ const PaidReactionEmoji = ({
|
|||||||
tgsUrl={LOCAL_TGS_URLS.StarReactionEffect}
|
tgsUrl={LOCAL_TGS_URLS.StarReactionEffect}
|
||||||
play={isIntersecting}
|
play={isIntersecting}
|
||||||
noLoop
|
noLoop
|
||||||
|
forceAlways
|
||||||
nonInteractive
|
nonInteractive
|
||||||
quality={QUALITY}
|
quality={QUALITY}
|
||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
import { DEBUG_LOG_FILENAME } from '../../../config';
|
import { DEBUG_LOG_FILENAME } from '../../../config';
|
||||||
import { getDebugLogs } from '../../../util/debugConsole';
|
import { getDebugLogs } from '../../../util/debugConsole';
|
||||||
import download from '../../../util/download';
|
import download from '../../../util/download';
|
||||||
import { IS_ELECTRON } from '../../../util/windowEnvironment';
|
import { IS_ELECTRON, IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
|||||||
shouldCollectDebugLogs,
|
shouldCollectDebugLogs,
|
||||||
shouldDebugExportedSenders,
|
shouldDebugExportedSenders,
|
||||||
}) => {
|
}) => {
|
||||||
const { requestConfetti, setSettingOption } = getActions();
|
const { requestConfetti, setSettingOption, requestWave } = getActions();
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
const [isAutoUpdateEnabled, setIsAutoUpdateEnabled] = useState(false);
|
const [isAutoUpdateEnabled, setIsAutoUpdateEnabled] = useState(false);
|
||||||
@ -61,10 +61,14 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
|||||||
window.electron?.setIsAutoUpdateEnabled(isChecked);
|
window.electron?.setIsAutoUpdateEnabled(isChecked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleRequestWave = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
requestWave({ startX: e.clientX, startY: e.clientY });
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-content custom-scroll">
|
<div className="settings-content custom-scroll">
|
||||||
<div className="settings-content-header no-border">
|
<div className="settings-content-header no-border">
|
||||||
<AnimatedIcon
|
<AnimatedIconWithPreview
|
||||||
tgsUrl={LOCAL_TGS_URLS.Experimental}
|
tgsUrl={LOCAL_TGS_URLS.Experimental}
|
||||||
size={200}
|
size={200}
|
||||||
className="experimental-duck"
|
className="experimental-duck"
|
||||||
@ -81,6 +85,13 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="title">Launch some confetti!</div>
|
<div className="title">Launch some confetti!</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
onClick={handleRequestWave}
|
||||||
|
icon="story-expired"
|
||||||
|
disabled={!IS_WAVE_TRANSFORM_SUPPORTED}
|
||||||
|
>
|
||||||
|
<div className="title">Start wave</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Allow HTTP Transport"
|
label="Allow HTTP Transport"
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import { processDeepLink } from '../../util/deeplink';
|
|||||||
import { Bundles, loadBundle } from '../../util/moduleLoader';
|
import { Bundles, loadBundle } from '../../util/moduleLoader';
|
||||||
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
|
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
|
||||||
import updateIcon from '../../util/updateIcon';
|
import updateIcon from '../../util/updateIcon';
|
||||||
import { IS_ANDROID, IS_ELECTRON } from '../../util/windowEnvironment';
|
import { IS_ANDROID, IS_ELECTRON, IS_WAVE_TRANSFORM_SUPPORTED } from '../../util/windowEnvironment';
|
||||||
|
|
||||||
import useInterval from '../../hooks/schedulers/useInterval';
|
import useInterval from '../../hooks/schedulers/useInterval';
|
||||||
import useTimeout from '../../hooks/schedulers/useTimeout';
|
import useTimeout from '../../hooks/schedulers/useTimeout';
|
||||||
@ -72,7 +72,6 @@ import RightColumn from '../right/RightColumn';
|
|||||||
import StoryViewer from '../story/StoryViewer.async';
|
import StoryViewer from '../story/StoryViewer.async';
|
||||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||||
import BotTrustModal from './BotTrustModal.async';
|
import BotTrustModal from './BotTrustModal.async';
|
||||||
import ConfettiContainer from './ConfettiContainer';
|
|
||||||
import DeleteFolderDialog from './DeleteFolderDialog.async';
|
import DeleteFolderDialog from './DeleteFolderDialog.async';
|
||||||
import Dialogs from './Dialogs.async';
|
import Dialogs from './Dialogs.async';
|
||||||
import DownloadManager from './DownloadManager';
|
import DownloadManager from './DownloadManager';
|
||||||
@ -88,6 +87,8 @@ import PremiumGiftingPickerModal from './premium/PremiumGiftingPickerModal.async
|
|||||||
import PremiumMainModal from './premium/PremiumMainModal.async';
|
import PremiumMainModal from './premium/PremiumMainModal.async';
|
||||||
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
|
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
|
||||||
import SafeLinkModal from './SafeLinkModal.async';
|
import SafeLinkModal from './SafeLinkModal.async';
|
||||||
|
import ConfettiContainer from './visualEffects/ConfettiContainer';
|
||||||
|
import WaveContainer from './visualEffects/WaveContainer';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -565,6 +566,7 @@ const Main = ({
|
|||||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
<ConfettiContainer />
|
<ConfettiContainer />
|
||||||
|
{IS_WAVE_TRANSFORM_SUPPORTED && <WaveContainer />}
|
||||||
<PhoneCall isActive={isPhoneCallActive} />
|
<PhoneCall isActive={isPhoneCallActive} />
|
||||||
<UnreadCount isForAppBadge />
|
<UnreadCount isForAppBadge />
|
||||||
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ConfettiStyle, TabState } from '../../global/types';
|
import type { ConfettiStyle, TabState } from '../../../global/types';
|
||||||
|
|
||||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||||
import { selectTabState } from '../../global/selectors';
|
import { selectTabState } from '../../../global/selectors';
|
||||||
import buildStyle from '../../util/buildStyle';
|
import buildStyle from '../../../util/buildStyle';
|
||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
|
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
import useWindowSize from '../../../hooks/window/useWindowSize';
|
||||||
|
|
||||||
import styles from './ConfettiContainer.module.scss';
|
import styles from './ConfettiContainer.module.scss';
|
||||||
|
|
||||||
43
src/components/main/visualEffects/WaveContainer.module.scss
Normal file
43
src/components/main/visualEffects/WaveContainer.module.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
z-index: var(--z-overlay-effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
--wave-width: 100vw;
|
||||||
|
--wave-pos-top: 0%;
|
||||||
|
--wave-pos-left: 0%;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: var(--wave-pos-top);
|
||||||
|
left: var(--wave-pos-left);
|
||||||
|
width: var(--wave-width);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background-image:
|
||||||
|
radial-gradient(
|
||||||
|
circle,
|
||||||
|
transparent 52%,
|
||||||
|
#FFFFFF06 60%,
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
|
||||||
|
backdrop-filter: url(#wave-filter);
|
||||||
|
animation: waveGrow 1.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes waveGrow {
|
||||||
|
from {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale(2.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/components/main/visualEffects/WaveContainer.tsx
Normal file
115
src/components/main/visualEffects/WaveContainer.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useEffect, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { TabState } from '../../../global/types';
|
||||||
|
|
||||||
|
import { selectTabState } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
|
import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../../util/svgController';
|
||||||
|
import windowSize from '../../../util/windowSize';
|
||||||
|
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import styles from './WaveContainer.module.scss';
|
||||||
|
|
||||||
|
import waveRipple from '../../../assets/wave_ripple.svg';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
waveInfo?: TabState['wave'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Wave = {
|
||||||
|
startTime: number;
|
||||||
|
waveWidth: number;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_SIZE_MULTIPLIER = 1.73;
|
||||||
|
const FILTER_ID = 'wave-filter';
|
||||||
|
const FILTER_SCALE = '20';
|
||||||
|
const WAVE_COUNT_LIMIT = 7;
|
||||||
|
|
||||||
|
const WaveContainer = ({ waveInfo }: StateProps) => {
|
||||||
|
const [waves, setWaves] = useState<Wave[]>([]);
|
||||||
|
|
||||||
|
const addWave = useLastCallback((newWave: Wave) => {
|
||||||
|
if (waves.length >= WAVE_COUNT_LIMIT) return;
|
||||||
|
|
||||||
|
setWaves((prevWaves) => [...prevWaves, newWave]);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waveInfo) return;
|
||||||
|
|
||||||
|
const { startX, startY } = waveInfo;
|
||||||
|
const { width, height } = windowSize.get();
|
||||||
|
|
||||||
|
const maxSize = Math.max(width - startX, height - startY, startX, startY);
|
||||||
|
const overlaySize = maxSize * BASE_SIZE_MULTIPLIER;
|
||||||
|
const top = startY - overlaySize / 2;
|
||||||
|
const left = startX - overlaySize / 2;
|
||||||
|
|
||||||
|
addWave({
|
||||||
|
startTime: waveInfo.lastWaveTime,
|
||||||
|
waveWidth: overlaySize,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
});
|
||||||
|
}, [waveInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
|
||||||
|
filter.setAttribute('x', '0');
|
||||||
|
filter.setAttribute('y', '0');
|
||||||
|
filter.setAttribute('width', '1');
|
||||||
|
filter.setAttribute('height', '1');
|
||||||
|
addSvgDefinition(filter, FILTER_ID);
|
||||||
|
|
||||||
|
const feImage = document.createElementNS(SVG_NAMESPACE, 'feImage');
|
||||||
|
feImage.setAttribute('href', waveRipple);
|
||||||
|
feImage.setAttribute('result', 'waveImage');
|
||||||
|
filter.appendChild(feImage);
|
||||||
|
|
||||||
|
const feDisplacementMap = document.createElementNS(SVG_NAMESPACE, 'feDisplacementMap');
|
||||||
|
feDisplacementMap.setAttribute('in', 'SourceGraphic');
|
||||||
|
feDisplacementMap.setAttribute('in2', 'waveImage');
|
||||||
|
feDisplacementMap.setAttribute('scale', FILTER_SCALE);
|
||||||
|
feDisplacementMap.setAttribute('xChannelSelector', 'R');
|
||||||
|
feDisplacementMap.setAttribute('yChannelSelector', 'B');
|
||||||
|
filter.appendChild(feDisplacementMap);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeSvgDefinition(FILTER_ID);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root)} teactFastList>
|
||||||
|
{waves.map((wave) => (
|
||||||
|
<div
|
||||||
|
className={styles.wave}
|
||||||
|
style={buildStyle(
|
||||||
|
`--wave-width: ${wave.waveWidth}px`,
|
||||||
|
`--wave-pos-top: ${wave.top}px`,
|
||||||
|
`--wave-pos-left: ${wave.left}px`,
|
||||||
|
)}
|
||||||
|
key={wave.startTime}
|
||||||
|
onAnimationEnd={() => setWaves((prevWaves) => prevWaves.filter((w) => w !== wave))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal(
|
||||||
|
(global): StateProps => {
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
return {
|
||||||
|
waveInfo: tabState.wave,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(WaveContainer));
|
||||||
@ -15,6 +15,7 @@
|
|||||||
--reaction-background: #FFBC2E33 !important;
|
--reaction-background: #FFBC2E33 !important;
|
||||||
--reaction-background-hover: #FFBC2E55 !important;
|
--reaction-background-hover: #FFBC2E55 !important;
|
||||||
--reaction-text-color: #E98111 !important;
|
--reaction-text-color: #E98111 !important;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.paid.chosen {
|
&.paid.chosen {
|
||||||
|
|||||||
@ -64,7 +64,12 @@ const ReactionButton = ({
|
|||||||
onClick,
|
onClick,
|
||||||
onPaidClick,
|
onPaidClick,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const { openStarsBalanceModal, resetLocalPaidReactions, openPaidReactionModal } = getActions();
|
const {
|
||||||
|
openStarsBalanceModal,
|
||||||
|
resetLocalPaidReactions,
|
||||||
|
openPaidReactionModal,
|
||||||
|
requestWave,
|
||||||
|
} = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -83,6 +88,7 @@ const ReactionButton = ({
|
|||||||
if (reaction.reaction.type === 'paid') {
|
if (reaction.reaction.type === 'paid') {
|
||||||
e.stopPropagation(); // Prevent default message double click behavior
|
e.stopPropagation(); // Prevent default message double click behavior
|
||||||
handlePaidClick();
|
handlePaidClick();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +135,13 @@ const ReactionButton = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reaction.localAmount) {
|
||||||
|
const { left, top } = button.getBoundingClientRect();
|
||||||
|
const startX = left + button.offsetWidth / 2;
|
||||||
|
const startY = top + button.offsetHeight / 2;
|
||||||
|
requestWave({ startX, startY });
|
||||||
|
}
|
||||||
|
|
||||||
const currentScale = Number(getComputedStyle(button).scale) || 1;
|
const currentScale = Number(getComputedStyle(button).scale) || 1;
|
||||||
animationRef.current?.cancel();
|
animationRef.current?.cancel();
|
||||||
// Animate scaling by 20%, and then returning to 1
|
// Animate scaling by 20%, and then returning to 1
|
||||||
|
|||||||
@ -167,6 +167,7 @@ export const MAX_INT_32 = 2 ** 31 - 1;
|
|||||||
export const TMP_CHAT_ID = '0';
|
export const TMP_CHAT_ID = '0';
|
||||||
|
|
||||||
export const ANIMATION_END_DELAY = 100;
|
export const ANIMATION_END_DELAY = 100;
|
||||||
|
export const ANIMATION_WAVE_MIN_INTERVAL = 200;
|
||||||
|
|
||||||
export const SCROLL_MIN_DURATION = 300;
|
export const SCROLL_MIN_DURATION = 300;
|
||||||
export const SCROLL_MAX_DURATION = 600;
|
export const SCROLL_MAX_DURATION = 600;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { ApiError, ApiNotification } from '../../../api/types';
|
|||||||
import type { ActionReturnType, GlobalState } from '../../types';
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ANIMATION_WAVE_MIN_INTERVAL,
|
||||||
DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
|
DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
|
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
|
||||||
@ -16,7 +17,7 @@ import { refreshFromCache } from '../../../util/localization';
|
|||||||
import * as langProvider from '../../../util/oldLangProvider';
|
import * as langProvider from '../../../util/oldLangProvider';
|
||||||
import updateIcon from '../../../util/updateIcon';
|
import updateIcon from '../../../util/updateIcon';
|
||||||
import { setPageTitle, setPageTitleInstant } from '../../../util/updatePageTitle';
|
import { setPageTitle, setPageTitleInstant } from '../../../util/updatePageTitle';
|
||||||
import { IS_ELECTRON } from '../../../util/windowEnvironment';
|
import { IS_ELECTRON, IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import { getAllowedAttachmentOptions, getChatTitle } from '../../helpers';
|
import { getAllowedAttachmentOptions, getChatTitle } from '../../helpers';
|
||||||
import {
|
import {
|
||||||
addActionHandler, getActions, getGlobal, setGlobal,
|
addActionHandler, getActions, getGlobal, setGlobal,
|
||||||
@ -489,6 +490,26 @@ addActionHandler('requestConfetti', (global, actions, payload): ActionReturnType
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('requestWave', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
startX, startY, tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
if (!IS_WAVE_TRANSFORM_SUPPORTED || !selectCanAnimateInterface(global)) return undefined;
|
||||||
|
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
const currentLastTime = tabState.wave?.lastWaveTime || 0;
|
||||||
|
if (Date.now() - currentLastTime < ANIMATION_WAVE_MIN_INTERVAL) return undefined;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
wave: {
|
||||||
|
lastWaveTime: Date.now(),
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('updateAttachmentSettings', (global, actions, payload): ActionReturnType => {
|
addActionHandler('updateAttachmentSettings', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
shouldCompress, shouldSendGrouped, isInvertedMedia, webPageMediaSize,
|
shouldCompress, shouldSendGrouped, isInvertedMedia, webPageMediaSize,
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export function addPaidReaction(
|
|||||||
count: 0,
|
count: 0,
|
||||||
chosenOrder: -1,
|
chosenOrder: -1,
|
||||||
localAmount: count,
|
localAmount: count,
|
||||||
|
localIsPrivate: isAnonymous,
|
||||||
},
|
},
|
||||||
...reactionCount,
|
...reactionCount,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -699,6 +699,11 @@ export type TabState = {
|
|||||||
style?: ConfettiStyle;
|
style?: ConfettiStyle;
|
||||||
withStars?: boolean;
|
withStars?: boolean;
|
||||||
};
|
};
|
||||||
|
wave?: {
|
||||||
|
lastWaveTime: number;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
};
|
||||||
|
|
||||||
urlAuth?: {
|
urlAuth?: {
|
||||||
button?: {
|
button?: {
|
||||||
@ -3189,6 +3194,10 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
closePollModal: WithTabId | undefined;
|
closePollModal: WithTabId | undefined;
|
||||||
requestConfetti: (ConfettiParams & WithTabId) | WithTabId;
|
requestConfetti: (ConfettiParams & WithTabId) | WithTabId;
|
||||||
|
requestWave: {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
} & WithTabId;
|
||||||
|
|
||||||
updateAttachmentSettings: {
|
updateAttachmentSettings: {
|
||||||
shouldCompress?: boolean;
|
shouldCompress?: boolean;
|
||||||
|
|||||||
@ -1,31 +1,21 @@
|
|||||||
import { useEffect } from '../../lib/teact/teact';
|
import { useEffect } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../util/svgController';
|
||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
|
|
||||||
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
||||||
const SVG_MAP = new Map<string, SvgColorFilter>();
|
const SVG_MAP = new Map<string, SvgColorFilter>();
|
||||||
|
|
||||||
class SvgColorFilter {
|
class SvgColorFilter {
|
||||||
public filterId: string;
|
public filterId: string;
|
||||||
|
|
||||||
public element: SVGSVGElement;
|
|
||||||
|
|
||||||
private referenceCount = 0;
|
private referenceCount = 0;
|
||||||
|
|
||||||
constructor(public color: string) {
|
constructor(public color: string) {
|
||||||
this.filterId = `color-filter-${color.slice(1)}`;
|
this.filterId = `color-filter-${color.slice(1)}`;
|
||||||
|
|
||||||
this.element = document.createElementNS(SVG_NAMESPACE, 'svg');
|
|
||||||
this.element.width.baseVal.valueAsString = '0px';
|
|
||||||
this.element.height.baseVal.valueAsString = '0px';
|
|
||||||
|
|
||||||
const defs = document.createElementNS(SVG_NAMESPACE, 'defs');
|
|
||||||
this.element.appendChild(defs);
|
|
||||||
|
|
||||||
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
|
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
|
||||||
filter.id = this.filterId;
|
|
||||||
filter.setAttribute('color-interpolation-filters', 'sRGB');
|
filter.setAttribute('color-interpolation-filters', 'sRGB');
|
||||||
defs.appendChild(filter);
|
addSvgDefinition(filter, this.filterId);
|
||||||
|
|
||||||
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
|
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
|
||||||
feColorMatrix.setAttribute('type', 'matrix');
|
feColorMatrix.setAttribute('type', 'matrix');
|
||||||
@ -37,8 +27,6 @@ class SvgColorFilter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
filter.appendChild(feColorMatrix);
|
filter.appendChild(feColorMatrix);
|
||||||
|
|
||||||
document.body.appendChild(this.element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFilterId() {
|
public getFilterId() {
|
||||||
@ -49,7 +37,7 @@ class SvgColorFilter {
|
|||||||
public removeReference() {
|
public removeReference() {
|
||||||
this.referenceCount -= 1;
|
this.referenceCount -= 1;
|
||||||
if (this.referenceCount === 0) {
|
if (this.referenceCount === 0) {
|
||||||
this.element.remove();
|
removeSvgDefinition(this.filterId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -232,6 +232,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--symbol-menu-height: 17.6875rem;
|
--symbol-menu-height: 17.6875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--z-overlay-effects: 10001;
|
||||||
--z-modal-confirm: 10000;
|
--z-modal-confirm: 10000;
|
||||||
--z-portal-menu: 10000;
|
--z-portal-menu: 10000;
|
||||||
--z-symbol-menu-modal: 5000;
|
--z-symbol-menu-modal: 5000;
|
||||||
|
|||||||
@ -126,6 +126,10 @@ body.cursor-ew-resize {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svg-definitions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.allow-selection {
|
.allow-selection {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/util/svgController.ts
Normal file
30
src/util/svgController.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import generateUniqueId from './generateUniqueId';
|
||||||
|
|
||||||
|
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
|
const CONTAINER = document.createElementNS(SVG_NAMESPACE, 'svg');
|
||||||
|
CONTAINER.setAttribute('viewBox', '0 0 1 1');
|
||||||
|
CONTAINER.classList.add('svg-definitions');
|
||||||
|
document.body.appendChild(CONTAINER);
|
||||||
|
|
||||||
|
const DEFS = document.createElementNS(SVG_NAMESPACE, 'defs');
|
||||||
|
CONTAINER.appendChild(DEFS);
|
||||||
|
|
||||||
|
const DEFINITION_MAP = new Map<string, SVGElement>();
|
||||||
|
|
||||||
|
export function addSvgDefinition(element: SVGElement, id?: string) {
|
||||||
|
id ??= generateUniqueId();
|
||||||
|
element.id = id;
|
||||||
|
|
||||||
|
DEFS.appendChild(element);
|
||||||
|
DEFINITION_MAP.set(element.id, element);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSvgDefinition(id: string) {
|
||||||
|
const element = DEFINITION_MAP.get(id);
|
||||||
|
if (element) {
|
||||||
|
element.remove();
|
||||||
|
DEFINITION_MAP.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -69,8 +69,11 @@ export const IS_CANVAS_FILTER_SUPPORTED = (
|
|||||||
!IS_TEST && 'filter' in (document.createElement('canvas').getContext('2d') || {})
|
!IS_TEST && 'filter' in (document.createElement('canvas').getContext('2d') || {})
|
||||||
);
|
);
|
||||||
export const IS_REQUEST_FULLSCREEN_SUPPORTED = 'requestFullscreen' in document.createElement('div');
|
export const IS_REQUEST_FULLSCREEN_SUPPORTED = 'requestFullscreen' in document.createElement('div');
|
||||||
export const ARE_CALLS_SUPPORTED = !IS_FIREFOX;
|
export const ARE_CALLS_SUPPORTED = !IS_FIREFOX; // https://bugzilla.mozilla.org/show_bug.cgi?id=1923416
|
||||||
export const LAYERS_ANIMATION_NAME = IS_ANDROID ? 'slideFade' : IS_IOS ? 'slideLayers' : 'pushSlide';
|
export const LAYERS_ANIMATION_NAME = IS_ANDROID ? 'slideFade' : IS_IOS ? 'slideLayers' : 'pushSlide';
|
||||||
|
export const IS_WAVE_TRANSFORM_SUPPORTED = !IS_MOBILE
|
||||||
|
&& !IS_FIREFOX // https://bugzilla.mozilla.org/show_bug.cgi?id=1808785
|
||||||
|
&& !IS_SAFARI; // https://bugs.webkit.org/show_bug.cgi?id=245510
|
||||||
|
|
||||||
const TEST_VIDEO = document.createElement('video');
|
const TEST_VIDEO = document.createElement('video');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user