+ {waves.map((wave) => (
+
setWaves((prevWaves) => prevWaves.filter((w) => w !== wave))}
+ />
+ ))}
+
+ );
+};
+
+export default memo(withGlobal(
+ (global): StateProps => {
+ const tabState = selectTabState(global);
+ return {
+ waveInfo: tabState.wave,
+ };
+ },
+)(WaveContainer));
diff --git a/src/components/middle/message/reactions/ReactionButton.module.scss b/src/components/middle/message/reactions/ReactionButton.module.scss
index 816e43139..3214dd290 100644
--- a/src/components/middle/message/reactions/ReactionButton.module.scss
+++ b/src/components/middle/message/reactions/ReactionButton.module.scss
@@ -15,6 +15,7 @@
--reaction-background: #FFBC2E33 !important;
--reaction-background-hover: #FFBC2E55 !important;
--reaction-text-color: #E98111 !important;
+ z-index: 2;
}
&.paid.chosen {
diff --git a/src/components/middle/message/reactions/ReactionButton.tsx b/src/components/middle/message/reactions/ReactionButton.tsx
index e66699847..75217d519 100644
--- a/src/components/middle/message/reactions/ReactionButton.tsx
+++ b/src/components/middle/message/reactions/ReactionButton.tsx
@@ -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
(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
diff --git a/src/config.ts b/src/config.ts
index 7033ed19a..64b4f9ceb 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -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;
diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts
index 8901e9b72..cc1e35f79 100644
--- a/src/global/actions/ui/misc.ts
+++ b/src/global/actions/ui/misc.ts
@@ -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,
diff --git a/src/global/helpers/reactions.ts b/src/global/helpers/reactions.ts
index 34e511310..273f6636a 100644
--- a/src/global/helpers/reactions.ts
+++ b/src/global/helpers/reactions.ts
@@ -152,6 +152,7 @@ export function addPaidReaction(
count: 0,
chosenOrder: -1,
localAmount: count,
+ localIsPrivate: isAnonymous,
},
...reactionCount,
];
diff --git a/src/global/types.ts b/src/global/types.ts
index 364bc4e82..44c3ab6b6 100644
--- a/src/global/types.ts
+++ b/src/global/types.ts
@@ -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;
diff --git a/src/hooks/stickers/useColorFilter.ts b/src/hooks/stickers/useColorFilter.ts
index 0eb400afc..34e0901ff 100644
--- a/src/hooks/stickers/useColorFilter.ts
+++ b/src/hooks/stickers/useColorFilter.ts
@@ -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();
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);
}
}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index 5b091f33c..fa6d98658 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -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;
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 38eae501a..7171008bb 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -126,6 +126,10 @@ body.cursor-ew-resize {
visibility: hidden;
}
+.svg-definitions {
+ display: none;
+}
+
.allow-selection {
user-select: text;
}
diff --git a/src/util/svgController.ts b/src/util/svgController.ts
new file mode 100644
index 000000000..0ec75040f
--- /dev/null
+++ b/src/util/svgController.ts
@@ -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();
+
+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);
+ }
+}
diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts
index a3c7c6d95..54474f5c7 100644
--- a/src/util/windowEnvironment.ts
+++ b/src/util/windowEnvironment.ts
@@ -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');