Extensibility, TypeScript and lazy loading

This commit is contained in:
Steffen Kolmer 2020-10-19 21:25:01 +02:00
parent 41160ff08e
commit 607e33feba
11 changed files with 296 additions and 297 deletions

View file

@ -77,6 +77,7 @@ export const CommandCategories = {
"actions": _td("Actions"),
"admin": _td("Admin"),
"advanced": _td("Advanced"),
"effects": _td("Effects"),
"other": _td("Other"),
};
@ -1045,19 +1046,16 @@ export const Commands = [
args: '<message>',
runFn: function(roomId, args) {
return success((async () => {
const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
if ((!args) || (!args && isChatEffectsDisabled)) {
if (!args) {
args = _t("sends confetti");
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
} else {
MatrixClientPeg.get().sendTextMessage(roomId, args);
}
if (!isChatEffectsDisabled) {
dis.dispatch({action: 'confetti'});
}
dis.dispatch({action: 'effects.confetti'});
})());
},
category: CommandCategories.actions,
category: CommandCategories.effects,
}),
// Command definitions for autocompletion ONLY:

View file

@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel";
import {animateConfetti, forceStopConfetti, isConfettiEmoji} from "../views/elements/Confetti";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {IMatrixClientCreds} from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
@ -73,7 +72,7 @@ import TintableSvg from "../views/elements/TintableSvg";
import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
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;
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("userTrustStatusChanged", this.onUserVerificationChanged);
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
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
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("userTrustStatusChanged", this.onUserVerificationChanged);
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);
@ -693,9 +688,6 @@ export default class RoomView extends React.Component<IProps, IState> {
case 'message_sent':
this.checkIfAlone(this.state.room);
break;
case 'confetti':
//TODO: animateConfetti(this.roomView.current.offsetWidth);
break;
case 'post_sticker_message':
this.injectSticker(
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) => {
if (this.state.room && room.roomId == this.state.room.roomId) {
this.forceUpdate();
@ -2070,11 +2040,13 @@ export default class RoomView extends React.Component<IProps, IState> {
mx_RoomView_inCall: Boolean(activeCall),
});
const showChatEffects = SettingsStore.getValue('showChatEffects');
return (
<RoomContext.Provider value={this.state}>
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{this.roomView.current &&
<ConfettiOverlay roomWidth={this.roomView.current.offsetWidth} />
{showChatEffects && this.roomView.current &&
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
}
<ErrorBoundary>
<RoomHeader

View file

@ -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();
}

View file

@ -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,
}}
/>
)
}

View 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;

View file

@ -0,0 +1,5 @@
export default interface ICanvasEffect {
start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
stop: () => Promise<void>,
isRunning: boolean
}

View 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'));
}

View file

@ -42,7 +42,7 @@ import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
import {isConfettiEmoji} from "../elements/Confetti";
import {isConfettiEmoji} from "../elements/effects/confetti";
import SettingsStore from "../../../settings/SettingsStore";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
@ -318,10 +318,8 @@ export default class SendMessageComposer extends React.Component {
});
}
dis.dispatch({action: "message_sent"});
if (!SettingsStore.getValue('dontShowChatEffects')) {
if (isConfettiEmoji(content)) {
dis.dispatch({action: 'confetti'});
}
if (isConfettiEmoji(content)) {
dis.dispatch({action: 'effects.confetti'});
}
}

View file

@ -49,7 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showAvatarChanges',
'showDisplaynameChanges',
'showImages',
'dontShowChatEffects',
'showChatEffects',
'Pill.shouldShowPillAvatar',
];

View file

@ -510,7 +510,7 @@
"Manually verify all remote sessions": "Manually verify all remote sessions",
"IRC display name width": "IRC display name width",
"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 logs": "Collecting logs",
"Uploading logs": "Uploading logs",

View file

@ -622,10 +622,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Enable experimental, compact IRC style layout"),
default: false,
},
"dontShowChatEffects": {
"showChatEffects": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Don't show chat effects"),
default: false,
displayName: _td("Show chat effects"),
default: true,
},
"Widgets.pinned": {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,