refactored effects dir and changed effects exported name

This commit is contained in:
nurjinn jafar 2020-11-27 14:54:21 +01:00
parent ede67684e4
commit 6ce5d3b044
9 changed files with 110 additions and 48 deletions

View file

@ -69,9 +69,9 @@ import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
import {containsEmoji} from '../views/elements/effects/effectUtilities';
import effects from '../views/elements/effects'
import EffectsOverlay from "../views/elements/EffectsOverlay";
import {containsEmoji} from '../../effects/effectUtilities';
import {CHAT_EFFECTS} from '../../effects'
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore";
@ -802,9 +802,9 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!this.state.room ||
!this.state.matrixClientIsReady ||
this.state.room.getUnreadNotificationCount() === 0) return;
effects.forEach(effect => {
CHAT_EFFECTS.forEach(effect => {
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
dis.dispatch({action: `effects.${effect.command}`});
dis.dispatch({action: `CHAT_EFFECTS.${effect.command}`});
}
})
};

View file

@ -1,7 +1,24 @@
/*
Copyright 2020 Nurjin Jafar
Copyright 2020 Nordeck IT + Consulting GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FunctionComponent, useEffect, useRef } from 'react';
import dis from '../../../../dispatcher/dispatcher';
import ICanvasEffect, { ICanvasEffectConstructable } from './ICanvasEffect.js';
import effects from './index'
import dis from '../../../dispatcher/dispatcher';
import ICanvasEffect, { ICanvasEffectConstructable } from '../../../effects/ICanvasEffect.js';
import {CHAT_EFFECTS} from '../../../effects'
export type EffectsOverlayProps = {
roomWidth: number;
@ -15,7 +32,7 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
if (!name) return null;
let effect: ICanvasEffect | null = effectsRef.current[name] || null;
if (effect === null) {
const options = effects.find((e) => e.command === name)?.options
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
try {
const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`./${name}`);
effect = new Effect(options);

View file

@ -1,29 +0,0 @@
/**
* Defines the constructor of a canvas based room effect
*/
export interface ICanvasEffectConstructable {
/**
* @param {{[key:string]:any}} options? Optional animation options
* @returns ICanvasEffect Returns a new instance of the canvas effect
*/
new(options?: { [key: string]: any }): ICanvasEffect
}
/**
* Defines the interface of a canvas based room effect
*/
export default interface ICanvasEffect {
/**
* @param {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
* @param {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
*/
start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
/**
* Stops the current animation
*/
stop: () => Promise<void>,
/**
* Returns a value that defines if the animation is currently running
*/
isRunning: boolean
}

View file

@ -1,197 +0,0 @@
import ICanvasEffect from '../ICanvasEffect';
declare global {
interface Window {
mozRequestAnimationFrame: any;
oRequestAnimationFrame: any;
msRequestAnimationFrame: any;
}
}
export type ConfettiOptions = {
/**
* max confetti count
*/
maxCount: number,
/**
* particle animation speed
*/
speed: number,
/**
* the confetti animation frame interval in milliseconds
*/
frameInterval: number,
/**
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
*/
alpha: number,
/**
* use gradient instead of solid particle color
*/
gradient: boolean,
}
type ConfettiParticle = {
color: string,
color2: string,
x: number,
y: number,
diameter: number,
tilt: number,
tiltAngleIncrement: number,
tiltAngle: number,
}
export const DefaultOptions: ConfettiOptions = {
maxCount: 150,
speed: 3,
frameInterval: 15,
alpha: 1.0,
gradient: false,
};
export default class Confetti implements ICanvasEffect {
private readonly options: ConfettiOptions;
constructor(options: { [key: string]: any }) {
this.options = {...DefaultOptions, ...options};
}
private context: CanvasRenderingContext2D | null = 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 = 3000) => {
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);
};
})();
this.context = canvas.getContext('2d');
this.particles = [];
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);
}
}
public stop = async () => {
this.isRunning = false;
}
private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
if (this.options.gradient) {
particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
} else {
particle.color2 = particle.color;
}
particle.x = Math.random() * width;
particle.y = Math.random() * -height;
particle.diameter = Math.random() * 10 + 5;
particle.tilt = Math.random() * -10;
particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
particle.tiltAngle = Math.random() * Math.PI;
return particle;
}
private runAnimation = (): void => {
if (!this.context || !this.context.canvas) {
return;
}
if (this.particles.length === 0) {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
} 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 => {
if (!this.context || !this.context.canvas) {
return;
}
let x; let x2; let y2;
for (const particle of this.particles) {
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 = () => {
if (!this.context || !this.context.canvas) {
return;
}
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--;
}
}
}
}
}

View file

@ -1,8 +0,0 @@
/**
* Checks a message if it contains one of the provided emojis
* @param {Object} content The message
* @param {Array<string>} emojis The list of emojis to check for
*/
export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
return emojis.some((emoji) => content.body && content.body.includes(emoji));
}

View file

@ -1,75 +0,0 @@
import { _t, _td } from "../../../../languageHandler";
export type Effect<TOptions extends { [key: string]: any }> = {
/**
* one or more emojis that will trigger this effect
*/
emojis: Array<string>;
/**
* the matrix message type that will trigger this effect
*/
msgType: string;
/**
* the room command to trigger this effect
*/
command: string;
/**
* a function that returns the translated description of the effect
*/
description: () => string;
/**
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
*/
fallbackMessage: () => string;
/**
* animation options
*/
options: TOptions;
}
type ConfettiOptions = {
/**
* max confetti count
*/
maxCount: number,
/**
* particle animation speed
*/
speed: number,
/**
* the confetti animation frame interval in milliseconds
*/
frameInterval: number,
/**
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
*/
alpha: number,
/**
* use gradient instead of solid particle color
*/
gradient: boolean,
}
/**
* This configuration defines room effects that can be triggered by custom message types and emojis
*/
const effects: Array<Effect<{ [key: string]: any }>> = [
{
emojis: ['🎊', '🎉'],
msgType: 'nic.custom.confetti',
command: 'confetti',
description: () => _td("Sends the given message with confetti"),
fallbackMessage: () => _t("sends confetti") + " 🎉",
options: {
maxCount: 150,
speed: 3,
frameInterval: 15,
alpha: 1.0,
gradient: false,
},
} as Effect<ConfettiOptions>,
];
export default effects;

View file

@ -42,8 +42,8 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
import {containsEmoji} from "../elements/effects/effectUtilities";
import effects from '../elements/effects';
import {containsEmoji} from "../../../effects/effectUtilities";
import {CHAT_EFFECTS} from '../../../effects';
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics";
@ -328,7 +328,7 @@ export default class SendMessageComposer extends React.Component {
});
}
dis.dispatch({action: "message_sent"});
effects.forEach((effect) => {
CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(content, effect.emojis)) {
dis.dispatch({action: `effects.${effect.command}`});
}