Extensibility, TypeScript and lazy loading
This commit is contained in:
parent
41160ff08e
commit
607e33feba
11 changed files with 296 additions and 297 deletions
|
@ -77,6 +77,7 @@ export const CommandCategories = {
|
||||||
"actions": _td("Actions"),
|
"actions": _td("Actions"),
|
||||||
"admin": _td("Admin"),
|
"admin": _td("Admin"),
|
||||||
"advanced": _td("Advanced"),
|
"advanced": _td("Advanced"),
|
||||||
|
"effects": _td("Effects"),
|
||||||
"other": _td("Other"),
|
"other": _td("Other"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1045,19 +1046,16 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
return success((async () => {
|
return success((async () => {
|
||||||
const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
|
if (!args) {
|
||||||
if ((!args) || (!args && isChatEffectsDisabled)) {
|
|
||||||
args = _t("sends confetti");
|
args = _t("sends confetti");
|
||||||
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
|
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
|
||||||
} else {
|
} else {
|
||||||
MatrixClientPeg.get().sendTextMessage(roomId, args);
|
MatrixClientPeg.get().sendTextMessage(roomId, args);
|
||||||
}
|
}
|
||||||
if (!isChatEffectsDisabled) {
|
dis.dispatch({action: 'effects.confetti'});
|
||||||
dis.dispatch({action: 'confetti'});
|
|
||||||
}
|
|
||||||
})());
|
})());
|
||||||
},
|
},
|
||||||
category: CommandCategories.actions,
|
category: CommandCategories.effects,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Command definitions for autocompletion ONLY:
|
// Command definitions for autocompletion ONLY:
|
||||||
|
|
|
@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||||
import {Action} from "../../dispatcher/actions";
|
import {Action} from "../../dispatcher/actions";
|
||||||
import {SettingLevel} from "../../settings/SettingLevel";
|
import {SettingLevel} from "../../settings/SettingLevel";
|
||||||
import {animateConfetti, forceStopConfetti, isConfettiEmoji} from "../views/elements/Confetti";
|
|
||||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
|
@ -73,7 +72,7 @@ import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
||||||
import ConfettiOverlay from "../views/elements/ConfettiOverlay";
|
import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -248,8 +247,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||||
this.context.on("Event.decrypted", this.onEventDecrypted);
|
|
||||||
this.context.on("event", this.onEvent);
|
|
||||||
// Start listening for RoomViewStore updates
|
// Start listening for RoomViewStore updates
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||||
|
@ -570,8 +567,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||||
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
|
||||||
this.context.removeListener("event", this.onEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||||
|
@ -693,9 +688,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this.checkIfAlone(this.state.room);
|
this.checkIfAlone(this.state.room);
|
||||||
break;
|
break;
|
||||||
case 'confetti':
|
|
||||||
//TODO: animateConfetti(this.roomView.current.offsetWidth);
|
|
||||||
break;
|
|
||||||
case 'post_sticker_message':
|
case 'post_sticker_message':
|
||||||
this.injectSticker(
|
this.injectSticker(
|
||||||
payload.data.content.url,
|
payload.data.content.url,
|
||||||
|
@ -804,28 +796,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEventDecrypted = (ev) => {
|
|
||||||
if (!SettingsStore.getValue('dontShowChatEffects')) {
|
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
|
|
||||||
this.state.room.getUnreadNotificationCount() === 0) return;
|
|
||||||
this.handleConfetti(ev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onEvent = (ev) => {
|
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
|
||||||
this.handleConfetti(ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleConfetti = (ev) => {
|
|
||||||
if (this.state.matrixClientIsReady) {
|
|
||||||
const messageBody = _t('sends confetti');
|
|
||||||
if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
|
|
||||||
dis.dispatch({action: 'confetti'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomName = (room: Room) => {
|
private onRoomName = (room: Room) => {
|
||||||
if (this.state.room && room.roomId == this.state.room.roomId) {
|
if (this.state.room && room.roomId == this.state.room.roomId) {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
@ -2070,11 +2040,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
mx_RoomView_inCall: Boolean(activeCall),
|
mx_RoomView_inCall: Boolean(activeCall),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={this.state}>
|
<RoomContext.Provider value={this.state}>
|
||||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||||
{this.roomView.current &&
|
{showChatEffects && this.roomView.current &&
|
||||||
<ConfettiOverlay roomWidth={this.roomView.current.offsetWidth} />
|
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||||
}
|
}
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
<RoomHeader
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 MathuSum Mut
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const confetti = {
|
|
||||||
//set max confetti count
|
|
||||||
maxCount: 150,
|
|
||||||
//syarn addet the particle animation speed
|
|
||||||
speed: 3,
|
|
||||||
//the confetti animation frame interval in milliseconds
|
|
||||||
frameInterval: 15,
|
|
||||||
//the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
|
||||||
alpha: 1.0,
|
|
||||||
//call to start confetti animation (with optional timeout in milliseconds)
|
|
||||||
start: null,
|
|
||||||
//call to stop adding confetti
|
|
||||||
stop: null,
|
|
||||||
//call to stop the confetti animation and remove all confetti immediately
|
|
||||||
remove: null,
|
|
||||||
isRunning: null,
|
|
||||||
//call and returns true or false depending on whether the animation is running
|
|
||||||
animate: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
confetti.start = startConfetti;
|
|
||||||
confetti.stop = stopConfetti;
|
|
||||||
confetti.remove = removeConfetti;
|
|
||||||
confetti.isRunning = isConfettiRunning;
|
|
||||||
confetti.animate = animateConfetti;
|
|
||||||
const supportsAnimationFrame = window.requestAnimationFrame ||
|
|
||||||
window.webkitRequestAnimationFrame ||
|
|
||||||
window.mozRequestAnimationFrame ||
|
|
||||||
window.oRequestAnimationFrame ||
|
|
||||||
window.msRequestAnimationFrame;
|
|
||||||
const colors = ["rgba(30,144,255,", "rgba(107,142,35,", "rgba(255,215,0,",
|
|
||||||
"rgba(255,192,203,", "rgba(106,90,205,", "rgba(173,216,230,",
|
|
||||||
"rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
|
|
||||||
"rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
|
|
||||||
let streamingConfetti = false;
|
|
||||||
// let animationTimer = null;
|
|
||||||
let lastFrameTime = Date.now();
|
|
||||||
let particles = [];
|
|
||||||
let waveAngle = 0;
|
|
||||||
let context = null;
|
|
||||||
|
|
||||||
function resetParticle(particle, width, height) {
|
|
||||||
particle.color = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
|
|
||||||
particle.color2 = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
|
|
||||||
particle.x = Math.random() * width;
|
|
||||||
particle.y = Math.random() * height - height;
|
|
||||||
particle.diameter = Math.random() * 10 + 5;
|
|
||||||
particle.tilt = Math.random() * 10 - 10;
|
|
||||||
particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
|
|
||||||
particle.tiltAngle = Math.random() * Math.PI;
|
|
||||||
return particle;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runAnimation() {
|
|
||||||
if (particles.length === 0) {
|
|
||||||
context.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
|
||||||
//animationTimer = null;
|
|
||||||
} else {
|
|
||||||
const now = Date.now();
|
|
||||||
const delta = now - lastFrameTime;
|
|
||||||
if (!supportsAnimationFrame || delta > confetti.frameInterval) {
|
|
||||||
context.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
|
||||||
updateParticles();
|
|
||||||
drawParticles(context);
|
|
||||||
lastFrameTime = now - (delta % confetti.frameInterval);
|
|
||||||
}
|
|
||||||
requestAnimationFrame(runAnimation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startConfetti(canvas, roomWidth, timeout) {
|
|
||||||
window.requestAnimationFrame = (function() {
|
|
||||||
return window.requestAnimationFrame ||
|
|
||||||
window.webkitRequestAnimationFrame ||
|
|
||||||
window.mozRequestAnimationFrame ||
|
|
||||||
window.oRequestAnimationFrame ||
|
|
||||||
window.msRequestAnimationFrame ||
|
|
||||||
function(callback) {
|
|
||||||
return window.setTimeout(callback, confetti.frameInterval);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
if (context === null) {
|
|
||||||
context = canvas.getContext("2d");
|
|
||||||
}
|
|
||||||
const count = confetti.maxCount;
|
|
||||||
while (particles.length < count) {
|
|
||||||
particles.push(resetParticle({}, canvas.width, canvas.height));
|
|
||||||
}
|
|
||||||
streamingConfetti = true;
|
|
||||||
runAnimation();
|
|
||||||
if (timeout) {
|
|
||||||
window.setTimeout(stopConfetti, timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopConfetti() {
|
|
||||||
streamingConfetti = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeConfetti() {
|
|
||||||
stop();
|
|
||||||
particles = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isConfettiRunning() {
|
|
||||||
return streamingConfetti;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawParticles(context) {
|
|
||||||
let particle;
|
|
||||||
let x; let x2; let y2;
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
particle = particles[i];
|
|
||||||
context.beginPath();
|
|
||||||
context.lineWidth = particle.diameter;
|
|
||||||
x2 = particle.x + particle.tilt;
|
|
||||||
x = x2 + particle.diameter / 2;
|
|
||||||
y2 = particle.y + particle.tilt + particle.diameter / 2;
|
|
||||||
if (confetti.gradient) {
|
|
||||||
const gradient = context.createLinearGradient(x, particle.y, x2, y2);
|
|
||||||
gradient.addColorStop("0", particle.color);
|
|
||||||
gradient.addColorStop("1.0", particle.color2);
|
|
||||||
context.strokeStyle = gradient;
|
|
||||||
} else {
|
|
||||||
context.strokeStyle = particle.color;
|
|
||||||
}
|
|
||||||
context.moveTo(x, particle.y);
|
|
||||||
context.lineTo(x2, y2);
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateParticles() {
|
|
||||||
const width = window.innerWidth;
|
|
||||||
const height = window.innerHeight;
|
|
||||||
let particle;
|
|
||||||
waveAngle += 0.01;
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
particle = particles[i];
|
|
||||||
if (!streamingConfetti && particle.y < -15) {
|
|
||||||
particle.y = height + 100;
|
|
||||||
} else {
|
|
||||||
particle.tiltAngle += particle.tiltAngleIncrement;
|
|
||||||
particle.x += Math.sin(waveAngle) - 0.5;
|
|
||||||
particle.y += (Math.cos(waveAngle) + particle.diameter + confetti.speed) * 0.5;
|
|
||||||
particle.tilt = Math.sin(particle.tiltAngle) * 15;
|
|
||||||
}
|
|
||||||
if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
|
|
||||||
if (streamingConfetti && particles.length <= confetti.maxCount) {
|
|
||||||
resetParticle(particle, width, height);
|
|
||||||
} else {
|
|
||||||
particles.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
export function convertToHex(content) {
|
|
||||||
const contentBodyToHexArray = [];
|
|
||||||
let hex;
|
|
||||||
if (content.body) {
|
|
||||||
for (let i = 0; i < content.body.length; i++) {
|
|
||||||
hex = content.body.codePointAt(i).toString(16);
|
|
||||||
contentBodyToHexArray.push(hex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contentBodyToHexArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isConfettiEmoji(content) {
|
|
||||||
const hexArray = convertToHex(content);
|
|
||||||
return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function animateConfetti(canvas, roomWidth) {
|
|
||||||
confetti.start(canvas, roomWidth, 3000);
|
|
||||||
}
|
|
||||||
export function forceStopConfetti() {
|
|
||||||
confetti.remove();
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React, {useEffect, useRef} from 'react';
|
|
||||||
import {animateConfetti, forceStopConfetti} from './Confetti.js';
|
|
||||||
|
|
||||||
export default function ConfettiOverlay({roomWidth}) {
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
// on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const resize = () => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
};
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
canvas.width = roomWidth;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
window.addEventListener("resize", resize, true);
|
|
||||||
animateConfetti(canvas, roomWidth);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("resize", resize);
|
|
||||||
forceStopConfetti();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
// on roomWidth change
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
canvas.width = roomWidth;
|
|
||||||
}, [roomWidth]);
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
zIndex: 999999,
|
|
||||||
pointerEvents: "none",
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
77
src/components/views/elements/effects/EffectsOverlay.tsx
Normal file
77
src/components/views/elements/effects/EffectsOverlay.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, {FunctionComponent, useEffect, useRef} from 'react';
|
||||||
|
import dis from '../../../../dispatcher/dispatcher';
|
||||||
|
import ICanvasEffect from './ICanvasEffect.js';
|
||||||
|
|
||||||
|
type EffectsOverlayProps = {
|
||||||
|
roomWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const effectsRef = useRef<Map<String, ICanvasEffect>>(new Map<String, ICanvasEffect>());
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
canvasRef.current.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
|
||||||
|
if(!name) return null;
|
||||||
|
let effect = effectsRef.current[name] ?? null;
|
||||||
|
if(effect === null) {
|
||||||
|
try {
|
||||||
|
var { default: Effect } = await import(`./${name}`);
|
||||||
|
effect = new Effect();
|
||||||
|
effectsRef.current[name] = effect;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to load effect module at \'./${name}\'.', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAction = (payload: { action: string }) => {
|
||||||
|
const actionPrefix = 'effects.';
|
||||||
|
if(payload.action.indexOf(actionPrefix) === 0) {
|
||||||
|
const effect = payload.action.substr(actionPrefix.length);
|
||||||
|
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const dispatcherRef = dis.register(onAction);
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
canvas.width = roomWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
window.addEventListener('resize', resize, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dis.unregister(dispatcherRef);
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
|
for(const effect in effectsRef.current) {
|
||||||
|
effectsRef.current[effect]?.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// on roomWidth change
|
||||||
|
useEffect(() => {
|
||||||
|
canvasRef.current.width = roomWidth;
|
||||||
|
}, [roomWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
zIndex: 999999,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EffectsOverlay;
|
5
src/components/views/elements/effects/ICanvasEffect.ts
Normal file
5
src/components/views/elements/effects/ICanvasEffect.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default interface ICanvasEffect {
|
||||||
|
start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
|
||||||
|
stop: () => Promise<void>,
|
||||||
|
isRunning: boolean
|
||||||
|
}
|
197
src/components/views/elements/effects/confetti/index.ts
Normal file
197
src/components/views/elements/effects/confetti/index.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import ICanvasEffect from '../ICanvasEffect'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mozRequestAnimationFrame: any;
|
||||||
|
oRequestAnimationFrame: any;
|
||||||
|
msRequestAnimationFrame: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfettiOptions = {
|
||||||
|
maxCount: number,
|
||||||
|
speed: number,
|
||||||
|
frameInterval: number,
|
||||||
|
alpha: number,
|
||||||
|
gradient: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfettiParticle = {
|
||||||
|
color: string,
|
||||||
|
color2: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
diameter: number,
|
||||||
|
tilt: number,
|
||||||
|
tiltAngleIncrement: number,
|
||||||
|
tiltAngle: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultOptions: ConfettiOptions = {
|
||||||
|
//set max confetti count
|
||||||
|
maxCount: 150,
|
||||||
|
//syarn addet the particle animation speed
|
||||||
|
speed: 3,
|
||||||
|
//the confetti animation frame interval in milliseconds
|
||||||
|
frameInterval: 15,
|
||||||
|
//the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||||
|
alpha: 1.0,
|
||||||
|
//use gradient instead of solid particle color
|
||||||
|
gradient: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Confetti implements ICanvasEffect {
|
||||||
|
private readonly options: ConfettiOptions;
|
||||||
|
|
||||||
|
constructor(options: ConfettiOptions = DefaultOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private context: CanvasRenderingContext2D | null;
|
||||||
|
private supportsAnimationFrame = window.requestAnimationFrame ||
|
||||||
|
window.webkitRequestAnimationFrame ||
|
||||||
|
window.mozRequestAnimationFrame ||
|
||||||
|
window.oRequestAnimationFrame ||
|
||||||
|
window.msRequestAnimationFrame;
|
||||||
|
private colors = ['rgba(30,144,255,', 'rgba(107,142,35,', 'rgba(255,215,0,',
|
||||||
|
'rgba(255,192,203,', 'rgba(106,90,205,', 'rgba(173,216,230,',
|
||||||
|
'rgba(238,130,238,', 'rgba(152,251,152,', 'rgba(70,130,180,',
|
||||||
|
'rgba(244,164,96,', 'rgba(210,105,30,', 'rgba(220,20,60,'];
|
||||||
|
|
||||||
|
private lastFrameTime = Date.now();
|
||||||
|
private particles: Array<ConfettiParticle> = [];
|
||||||
|
private waveAngle = 0;
|
||||||
|
|
||||||
|
public isRunning: boolean;
|
||||||
|
|
||||||
|
public start = async (canvas: HTMLCanvasElement, timeout?: number) => {
|
||||||
|
if(!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame = (function () {
|
||||||
|
return window.requestAnimationFrame ||
|
||||||
|
window.webkitRequestAnimationFrame ||
|
||||||
|
window.mozRequestAnimationFrame ||
|
||||||
|
window.oRequestAnimationFrame ||
|
||||||
|
window.msRequestAnimationFrame ||
|
||||||
|
function (callback) {
|
||||||
|
return window.setTimeout(callback, this.options.frameInterval);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
if (this.context === null) {
|
||||||
|
this.context = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
const count = this.options.maxCount;
|
||||||
|
while (this.particles.length < count) {
|
||||||
|
this.particles.push(this.resetParticle({} as ConfettiParticle, canvas.width, canvas.height));
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
this.runAnimation();
|
||||||
|
if (timeout) {
|
||||||
|
window.setTimeout(this.stop, timeout || 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop = async () => {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.particles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
|
||||||
|
particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
|
||||||
|
particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
|
||||||
|
particle.x = Math.random() * width;
|
||||||
|
particle.y = Math.random() * height - height;
|
||||||
|
particle.diameter = Math.random() * 10 + 5;
|
||||||
|
particle.tilt = Math.random() * 10 - 10;
|
||||||
|
particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
|
||||||
|
particle.tiltAngle = Math.random() * Math.PI;
|
||||||
|
return particle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private runAnimation = (): void => {
|
||||||
|
if (this.particles.length === 0) {
|
||||||
|
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||||
|
//animationTimer = null;
|
||||||
|
} else {
|
||||||
|
const now = Date.now();
|
||||||
|
const delta = now - this.lastFrameTime;
|
||||||
|
if (!this.supportsAnimationFrame || delta > this.options.frameInterval) {
|
||||||
|
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||||
|
this.updateParticles();
|
||||||
|
this.drawParticles(this.context);
|
||||||
|
this.lastFrameTime = now - (delta % this.options.frameInterval);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(this.runAnimation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private drawParticles = (context: CanvasRenderingContext2D): void => {
|
||||||
|
let particle;
|
||||||
|
let x; let x2; let y2;
|
||||||
|
for (let i = 0; i < this.particles.length; i++) {
|
||||||
|
particle = this.particles[i];
|
||||||
|
this.context.beginPath();
|
||||||
|
context.lineWidth = particle.diameter;
|
||||||
|
x2 = particle.x + particle.tilt;
|
||||||
|
x = x2 + particle.diameter / 2;
|
||||||
|
y2 = particle.y + particle.tilt + particle.diameter / 2;
|
||||||
|
if (this.options.gradient) {
|
||||||
|
const gradient = context.createLinearGradient(x, particle.y, x2, y2);
|
||||||
|
gradient.addColorStop(0, particle.color);
|
||||||
|
gradient.addColorStop(1.0, particle.color2);
|
||||||
|
context.strokeStyle = gradient;
|
||||||
|
} else {
|
||||||
|
context.strokeStyle = particle.color;
|
||||||
|
}
|
||||||
|
context.moveTo(x, particle.y);
|
||||||
|
context.lineTo(x2, y2);
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateParticles = () => {
|
||||||
|
const width = this.context.canvas.width;
|
||||||
|
const height = this.context.canvas.height;
|
||||||
|
let particle: ConfettiParticle;
|
||||||
|
this.waveAngle += 0.01;
|
||||||
|
for (let i = 0; i < this.particles.length; i++) {
|
||||||
|
particle = this.particles[i];
|
||||||
|
if (!this.isRunning && particle.y < -15) {
|
||||||
|
particle.y = height + 100;
|
||||||
|
} else {
|
||||||
|
particle.tiltAngle += particle.tiltAngleIncrement;
|
||||||
|
particle.x += Math.sin(this.waveAngle) - 0.5;
|
||||||
|
particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.options.speed) * 0.5;
|
||||||
|
particle.tilt = Math.sin(particle.tiltAngle) * 15;
|
||||||
|
}
|
||||||
|
if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
|
||||||
|
if (this.isRunning && this.particles.length <= this.options.maxCount) {
|
||||||
|
this.resetParticle(particle, width, height);
|
||||||
|
} else {
|
||||||
|
this.particles.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertToHex = (data: string): Array<string> => {
|
||||||
|
const contentBodyToHexArray = [];
|
||||||
|
if (!data) return contentBodyToHexArray;
|
||||||
|
let hex;
|
||||||
|
if (data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
hex = data.codePointAt(i).toString(16);
|
||||||
|
contentBodyToHexArray.push(hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentBodyToHexArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isConfettiEmoji = (content: { msgtype: string, body: string }): boolean => {
|
||||||
|
const hexArray = convertToHex(content.body);
|
||||||
|
return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ import {Key} from "../../../Keyboard";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {isConfettiEmoji} from "../elements/Confetti";
|
import {isConfettiEmoji} from "../elements/effects/confetti";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
|
@ -318,10 +318,8 @@ export default class SendMessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dis.dispatch({action: "message_sent"});
|
dis.dispatch({action: "message_sent"});
|
||||||
if (!SettingsStore.getValue('dontShowChatEffects')) {
|
if (isConfettiEmoji(content)) {
|
||||||
if (isConfettiEmoji(content)) {
|
dis.dispatch({action: 'effects.confetti'});
|
||||||
dis.dispatch({action: 'confetti'});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||||
'showAvatarChanges',
|
'showAvatarChanges',
|
||||||
'showDisplaynameChanges',
|
'showDisplaynameChanges',
|
||||||
'showImages',
|
'showImages',
|
||||||
'dontShowChatEffects',
|
'showChatEffects',
|
||||||
'Pill.shouldShowPillAvatar',
|
'Pill.shouldShowPillAvatar',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -510,7 +510,7 @@
|
||||||
"Manually verify all remote sessions": "Manually verify all remote sessions",
|
"Manually verify all remote sessions": "Manually verify all remote sessions",
|
||||||
"IRC display name width": "IRC display name width",
|
"IRC display name width": "IRC display name width",
|
||||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||||
"Don't show chat effects": "Don't show chat effects",
|
"Show chat effects": "Show chat effects",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading logs": "Uploading logs",
|
"Uploading logs": "Uploading logs",
|
||||||
|
|
|
@ -622,10 +622,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Enable experimental, compact IRC style layout"),
|
displayName: _td("Enable experimental, compact IRC style layout"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"dontShowChatEffects": {
|
"showChatEffects": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("Don't show chat effects"),
|
displayName: _td("Show chat effects"),
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
"Widgets.pinned": {
|
"Widgets.pinned": {
|
||||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||||
|
|
Loading…
Reference in a new issue