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">
|
||||
<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>
|
||||
|
||||
|
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}
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
forceAlways
|
||||
nonInteractive
|
||||
quality={QUALITY}
|
||||
onEnded={handleEnded}
|
||||
|
||||
@ -7,14 +7,14 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import { DEBUG_LOG_FILENAME } from '../../../config';
|
||||
import { getDebugLogs } from '../../../util/debugConsole';
|
||||
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 useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
|
||||
@ -38,7 +38,7 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
||||
shouldCollectDebugLogs,
|
||||
shouldDebugExportedSenders,
|
||||
}) => {
|
||||
const { requestConfetti, setSettingOption } = getActions();
|
||||
const { requestConfetti, setSettingOption, requestWave } = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
const [isAutoUpdateEnabled, setIsAutoUpdateEnabled] = useState(false);
|
||||
@ -61,10 +61,14 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
||||
window.electron?.setIsAutoUpdateEnabled(isChecked);
|
||||
}, []);
|
||||
|
||||
const handleRequestWave = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
requestWave({ startX: e.clientX, startY: e.clientY });
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settings-content custom-scroll">
|
||||
<div className="settings-content-header no-border">
|
||||
<AnimatedIcon
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={LOCAL_TGS_URLS.Experimental}
|
||||
size={200}
|
||||
className="experimental-duck"
|
||||
@ -81,6 +85,13 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<div className="title">Launch some confetti!</div>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
onClick={handleRequestWave}
|
||||
icon="story-expired"
|
||||
disabled={!IS_WAVE_TRANSFORM_SUPPORTED}
|
||||
>
|
||||
<div className="title">Start wave</div>
|
||||
</ListItem>
|
||||
|
||||
<Checkbox
|
||||
label="Allow HTTP Transport"
|
||||
|
||||
@ -37,7 +37,7 @@ import { processDeepLink } from '../../util/deeplink';
|
||||
import { Bundles, loadBundle } from '../../util/moduleLoader';
|
||||
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
|
||||
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 useTimeout from '../../hooks/schedulers/useTimeout';
|
||||
@ -72,7 +72,6 @@ import RightColumn from '../right/RightColumn';
|
||||
import StoryViewer from '../story/StoryViewer.async';
|
||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||
import BotTrustModal from './BotTrustModal.async';
|
||||
import ConfettiContainer from './ConfettiContainer';
|
||||
import DeleteFolderDialog from './DeleteFolderDialog.async';
|
||||
import Dialogs from './Dialogs.async';
|
||||
import DownloadManager from './DownloadManager';
|
||||
@ -88,6 +87,8 @@ import PremiumGiftingPickerModal from './premium/PremiumGiftingPickerModal.async
|
||||
import PremiumMainModal from './premium/PremiumMainModal.async';
|
||||
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
|
||||
import SafeLinkModal from './SafeLinkModal.async';
|
||||
import ConfettiContainer from './visualEffects/ConfettiContainer';
|
||||
import WaveContainer from './visualEffects/WaveContainer';
|
||||
|
||||
import './Main.scss';
|
||||
|
||||
@ -565,6 +566,7 @@ const Main = ({
|
||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||
<DownloadManager />
|
||||
<ConfettiContainer />
|
||||
{IS_WAVE_TRANSFORM_SUPPORTED && <WaveContainer />}
|
||||
<PhoneCall isActive={isPhoneCallActive} />
|
||||
<UnreadCount isForAppBadge />
|
||||
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||
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 { selectTabState } from '../../global/selectors';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import useWindowSize from '../../../hooks/window/useWindowSize';
|
||||
|
||||
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-hover: #FFBC2E55 !important;
|
||||
--reaction-text-color: #E98111 !important;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.paid.chosen {
|
||||
|
||||
@ -64,7 +64,12 @@ const ReactionButton = ({
|
||||
onClick,
|
||||
onPaidClick,
|
||||
}: OwnProps) => {
|
||||
const { openStarsBalanceModal, resetLocalPaidReactions, openPaidReactionModal } = getActions();
|
||||
const {
|
||||
openStarsBalanceModal,
|
||||
resetLocalPaidReactions,
|
||||
openPaidReactionModal,
|
||||
requestWave,
|
||||
} = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -83,6 +88,7 @@ const ReactionButton = ({
|
||||
if (reaction.reaction.type === 'paid') {
|
||||
e.stopPropagation(); // Prevent default message double click behavior
|
||||
handlePaidClick();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -129,6 +135,13 @@ const ReactionButton = ({
|
||||
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;
|
||||
animationRef.current?.cancel();
|
||||
// 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 ANIMATION_END_DELAY = 100;
|
||||
export const ANIMATION_WAVE_MIN_INTERVAL = 200;
|
||||
|
||||
export const SCROLL_MIN_DURATION = 300;
|
||||
export const SCROLL_MAX_DURATION = 600;
|
||||
|
||||
@ -4,6 +4,7 @@ import type { ApiError, ApiNotification } from '../../../api/types';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
import {
|
||||
ANIMATION_WAVE_MIN_INTERVAL,
|
||||
DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
|
||||
} from '../../../config';
|
||||
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
|
||||
@ -16,7 +17,7 @@ import { refreshFromCache } from '../../../util/localization';
|
||||
import * as langProvider from '../../../util/oldLangProvider';
|
||||
import updateIcon from '../../../util/updateIcon';
|
||||
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 {
|
||||
addActionHandler, getActions, getGlobal, setGlobal,
|
||||
@ -489,6 +490,26 @@ addActionHandler('requestConfetti', (global, actions, payload): ActionReturnType
|
||||
}, 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 => {
|
||||
const {
|
||||
shouldCompress, shouldSendGrouped, isInvertedMedia, webPageMediaSize,
|
||||
|
||||
@ -152,6 +152,7 @@ export function addPaidReaction(
|
||||
count: 0,
|
||||
chosenOrder: -1,
|
||||
localAmount: count,
|
||||
localIsPrivate: isAnonymous,
|
||||
},
|
||||
...reactionCount,
|
||||
];
|
||||
|
||||
@ -699,6 +699,11 @@ export type TabState = {
|
||||
style?: ConfettiStyle;
|
||||
withStars?: boolean;
|
||||
};
|
||||
wave?: {
|
||||
lastWaveTime: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
};
|
||||
|
||||
urlAuth?: {
|
||||
button?: {
|
||||
@ -3189,6 +3194,10 @@ export interface ActionPayloads {
|
||||
} & WithTabId) | undefined;
|
||||
closePollModal: WithTabId | undefined;
|
||||
requestConfetti: (ConfettiParams & WithTabId) | WithTabId;
|
||||
requestWave: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
} & WithTabId;
|
||||
|
||||
updateAttachmentSettings: {
|
||||
shouldCompress?: boolean;
|
||||
|
||||
@ -1,31 +1,21 @@
|
||||
import { useEffect } from '../../lib/teact/teact';
|
||||
|
||||
import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../util/svgController';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
|
||||
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||
const SVG_MAP = new Map<string, SvgColorFilter>();
|
||||
|
||||
class SvgColorFilter {
|
||||
public filterId: string;
|
||||
|
||||
public element: SVGSVGElement;
|
||||
|
||||
private referenceCount = 0;
|
||||
|
||||
constructor(public color: string) {
|
||||
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');
|
||||
filter.id = this.filterId;
|
||||
filter.setAttribute('color-interpolation-filters', 'sRGB');
|
||||
defs.appendChild(filter);
|
||||
addSvgDefinition(filter, this.filterId);
|
||||
|
||||
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
|
||||
feColorMatrix.setAttribute('type', 'matrix');
|
||||
@ -37,8 +27,6 @@ class SvgColorFilter {
|
||||
);
|
||||
|
||||
filter.appendChild(feColorMatrix);
|
||||
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
public getFilterId() {
|
||||
@ -49,7 +37,7 @@ class SvgColorFilter {
|
||||
public removeReference() {
|
||||
this.referenceCount -= 1;
|
||||
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;
|
||||
}
|
||||
|
||||
--z-overlay-effects: 10001;
|
||||
--z-modal-confirm: 10000;
|
||||
--z-portal-menu: 10000;
|
||||
--z-symbol-menu-modal: 5000;
|
||||
|
||||
@ -126,6 +126,10 @@ body.cursor-ew-resize {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.svg-definitions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.allow-selection {
|
||||
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') || {})
|
||||
);
|
||||
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 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');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user