Merge branch 'feature_confetti#14676' into develop
This commit is contained in:
commit
87dbc82a9f
11 changed files with 521 additions and 0 deletions
|
@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import {UIFeature} from "./settings/UIFeature";
|
||||||
|
import {CHAT_EFFECTS} from "./effects"
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from "./CallHandler";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
|
@ -78,6 +79,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"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1094,6 +1096,30 @@ export const Commands = [
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
hideCompletionAfterSpace: true,
|
hideCompletionAfterSpace: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
...CHAT_EFFECTS.map((effect) => {
|
||||||
|
return new Command({
|
||||||
|
command: effect.command,
|
||||||
|
description: effect.description(),
|
||||||
|
args: '<message>',
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
return success((async () => {
|
||||||
|
if (!args) {
|
||||||
|
args = effect.fallbackMessage();
|
||||||
|
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
|
||||||
|
} else {
|
||||||
|
const content = {
|
||||||
|
msgtype: effect.msgType,
|
||||||
|
body: args,
|
||||||
|
};
|
||||||
|
MatrixClientPeg.get().sendMessage(roomId, content);
|
||||||
|
}
|
||||||
|
dis.dispatch({action: `effects.${effect.command}`});
|
||||||
|
})());
|
||||||
|
},
|
||||||
|
category: CommandCategories.effects,
|
||||||
|
})
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// build a map from names and aliases to the Command objects.
|
// build a map from names and aliases to the Command objects.
|
||||||
|
|
|
@ -69,6 +69,9 @@ import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
|
import {containsEmoji} from '../../effects/utils';
|
||||||
|
import {CHAT_EFFECTS} from '../../effects';
|
||||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import WidgetStore from "../../stores/WidgetStore";
|
import WidgetStore from "../../stores/WidgetStore";
|
||||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
|
@ -248,6 +251,8 @@ 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);
|
||||||
|
@ -581,6 +586,8 @@ 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);
|
||||||
|
@ -781,6 +788,27 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onEventDecrypted = (ev) => {
|
||||||
|
if (ev.isDecryptionFailure()) return;
|
||||||
|
this.handleEffects(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEvent = (ev) => {
|
||||||
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||||
|
this.handleEffects(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleEffects = (ev) => {
|
||||||
|
if (!this.state.room ||
|
||||||
|
!this.state.matrixClientIsReady ||
|
||||||
|
this.state.room.getUnreadNotificationCount() === 0) return;
|
||||||
|
CHAT_EFFECTS.forEach(effect => {
|
||||||
|
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
|
||||||
|
dis.dispatch({action: `effects.${effect.command}`});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
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();
|
||||||
|
@ -1946,9 +1974,14 @@ 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}>
|
||||||
|
{showChatEffects && this.roomView.current &&
|
||||||
|
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||||
|
}
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
<RoomHeader
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
|
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
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 from '../../../effects/ICanvasEffect';
|
||||||
|
import {CHAT_EFFECTS} from '../../../effects'
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
|
||||||
|
|
||||||
|
const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
|
||||||
|
if (!name) return null;
|
||||||
|
let effect: ICanvasEffect | null = effectsRef.current[name] || null;
|
||||||
|
if (effect === null) {
|
||||||
|
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
|
||||||
|
try {
|
||||||
|
const { default: Effect } = await import(`../../../effects/${name}`);
|
||||||
|
effect = new Effect(options);
|
||||||
|
effectsRef.current[name] = effect;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effect;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resize = () => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
canvasRef.current.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dispatcherRef = dis.register(onAction);
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
window.addEventListener('resize', resize, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dis.unregister(dispatcherRef);
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||||
|
for (const effect in currentEffects) {
|
||||||
|
const effectModule: ICanvasEffect = currentEffects[effect];
|
||||||
|
if (effectModule && effectModule.isRunning) {
|
||||||
|
effectModule.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={roomWidth}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
zIndex: 999999,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EffectsOverlay;
|
|
@ -42,6 +42,8 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} 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 {containsEmoji} from "../../../effects/utils";
|
||||||
|
import {CHAT_EFFECTS} from '../../../effects';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
|
||||||
|
@ -326,6 +328,11 @@ export default class SendMessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dis.dispatch({action: "message_sent"});
|
dis.dispatch({action: "message_sent"});
|
||||||
|
CHAT_EFFECTS.forEach((effect) => {
|
||||||
|
if (containsEmoji(content, effect.emojis)) {
|
||||||
|
dis.dispatch({action: `effects.${effect.command}`});
|
||||||
|
}
|
||||||
|
});
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||||
'showAvatarChanges',
|
'showAvatarChanges',
|
||||||
'showDisplaynameChanges',
|
'showDisplaynameChanges',
|
||||||
'showImages',
|
'showImages',
|
||||||
|
'showChatEffects',
|
||||||
'Pill.shouldShowPillAvatar',
|
'Pill.shouldShowPillAvatar',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
47
src/effects/ICanvasEffect.ts
Normal file
47
src/effects/ICanvasEffect.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
191
src/effects/confetti/index.ts
Normal file
191
src/effects/confetti/index.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
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 ICanvasEffect from '../ICanvasEffect';
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/effects/index.ts
Normal file
89
src/effects/index.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
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 { _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
|
||||||
|
*/
|
||||||
|
export const CHAT_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>,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
24
src/effects/utils.ts
Normal file
24
src/effects/utils.ts
Normal file
|
@ -0,0 +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.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
|
@ -406,6 +406,7 @@
|
||||||
"Messages": "Messages",
|
"Messages": "Messages",
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Advanced": "Advanced",
|
"Advanced": "Advanced",
|
||||||
|
"Effects": "Effects",
|
||||||
"Other": "Other",
|
"Other": "Other",
|
||||||
"Command error": "Command error",
|
"Command error": "Command error",
|
||||||
"Usage": "Usage",
|
"Usage": "Usage",
|
||||||
|
@ -826,6 +827,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",
|
||||||
|
"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",
|
||||||
|
@ -844,6 +846,8 @@
|
||||||
"When rooms are upgraded": "When rooms are upgraded",
|
"When rooms are upgraded": "When rooms are upgraded",
|
||||||
"My Ban List": "My Ban List",
|
"My Ban List": "My Ban List",
|
||||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||||
|
"Sends the given message with confetti": "Sends the given message with confetti",
|
||||||
|
"sends confetti": "sends confetti",
|
||||||
"Video Call": "Video Call",
|
"Video Call": "Video Call",
|
||||||
"Voice Call": "Voice Call",
|
"Voice Call": "Voice Call",
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
|
|
|
@ -634,6 +634,11 @@ 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,
|
||||||
},
|
},
|
||||||
|
"showChatEffects": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td("Show chat effects"),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
"Widgets.pinned": {
|
"Widgets.pinned": {
|
||||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||||
default: {},
|
default: {},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue