mirror of https://github.com/vector-im/riot-web
Merge remote-tracking branch 'origin/develop' into dbkr/call_ui_fixups
commit
12474bb434
|
@ -341,7 +341,8 @@ export default class CallHandler {
|
|||
title: _t("Answered Elsewhere"),
|
||||
description: _t("The call was answered on another device."),
|
||||
});
|
||||
} else {
|
||||
} else if (oldState !== CallState.Fledgling) {
|
||||
// don't play the end-call sound for calls that never got off the ground
|
||||
this.play(AudioID.CallEnd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,6 +166,32 @@ export const Commands = [
|
|||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
new Command({
|
||||
command: 'tableflip',
|
||||
args: '<message>',
|
||||
description: _td('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'),
|
||||
runFn: function(roomId, args) {
|
||||
let message = '(╯°□°)╯︵ ┻━┻';
|
||||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
new Command({
|
||||
command: 'unflip',
|
||||
args: '<message>',
|
||||
description: _td('Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message'),
|
||||
runFn: function(roomId, args) {
|
||||
let message = '┬──┬ ノ( ゜-゜ノ)';
|
||||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
new Command({
|
||||
command: 'lenny',
|
||||
args: '<message>',
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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 FireworksOptions = {
|
||||
/**
|
||||
* max fireworks count
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* gravity value that firework adds to shift from it's start position
|
||||
*/
|
||||
gravity: number;
|
||||
}
|
||||
|
||||
type FireworksParticle = {
|
||||
/**
|
||||
* color
|
||||
*/
|
||||
color: string;
|
||||
/**
|
||||
* x,y are the point where the particle starts to position on canvas
|
||||
*/
|
||||
x: number;
|
||||
y: number;
|
||||
/**
|
||||
* vx,vy shift values from x and y
|
||||
*/
|
||||
vx: number;
|
||||
vy: number;
|
||||
/**
|
||||
* the alpha opacity of the firework particle (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number;
|
||||
/**
|
||||
* w,h width and height
|
||||
*/
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export const DefaultOptions: FireworksOptions = {
|
||||
maxCount: 500,
|
||||
gravity: 0.05,
|
||||
};
|
||||
|
||||
export default class Fireworks implements ICanvasEffect {
|
||||
private readonly options: FireworksOptions;
|
||||
|
||||
constructor(options: { [key: string]: any }) {
|
||||
this.options = {...DefaultOptions, ...options};
|
||||
}
|
||||
|
||||
private context: CanvasRenderingContext2D | null = null;
|
||||
private supportsAnimationFrame = window.requestAnimationFrame;
|
||||
private particles: Array<FireworksParticle> = [];
|
||||
public isRunning: boolean;
|
||||
|
||||
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.isRunning = true;
|
||||
this.context = canvas.getContext('2d');
|
||||
this.supportsAnimationFrame.call(window, this.updateWorld);
|
||||
if (timeout) {
|
||||
window.setTimeout(this.stop, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private updateWorld = () => {
|
||||
if (!this.isRunning && this.particles.length === 0) return;
|
||||
this.update();
|
||||
this.paint();
|
||||
this.supportsAnimationFrame.call(window, this.updateWorld);
|
||||
}
|
||||
|
||||
private update = () => {
|
||||
if (this.particles.length < this.options.maxCount && this.isRunning) {
|
||||
this.createFirework();
|
||||
}
|
||||
const alive = [];
|
||||
for (let i=0; i<this.particles.length; i++) {
|
||||
if (this.move(this.particles[i])) {
|
||||
alive.push(this.particles[i]);
|
||||
}
|
||||
}
|
||||
this.particles = alive;
|
||||
}
|
||||
|
||||
private paint = () => {
|
||||
if (!this.context || !this.context.canvas) return;
|
||||
this.context.globalCompositeOperation = 'destination-out';
|
||||
this.context.fillStyle = "rgba(0,0,0,0.5)";
|
||||
this.context.fillRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
this.context.globalCompositeOperation = 'lighter';
|
||||
for (let i=0; i<this.particles.length; i++) {
|
||||
this.drawParticle(this.particles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private createFirework = () => {
|
||||
if (!this.context || !this.context.canvas) return;
|
||||
const width = this.context.canvas.width;
|
||||
const height = this.context.canvas.height;
|
||||
const xPoint = Math.random() * (width - 200) + 100;
|
||||
const yPoint = Math.random() * (height - 200) + 100;
|
||||
const nFire = Math.random() * 50 + 100;
|
||||
const color = "rgb("+(~~(Math.random()*200+55))+","
|
||||
+(~~(Math.random()*200+55))+","+(~~(Math.random()*200+55))+")";
|
||||
for (let i=0; i<nFire; i++) {
|
||||
const particle = <FireworksParticle>{};
|
||||
particle.color = color;
|
||||
particle.w = particle.h = Math.random() * 4 + 1;
|
||||
particle.x = xPoint - particle.w / 2;
|
||||
particle.y = yPoint - particle.h / 2;
|
||||
particle.vx = (Math.random()-0.5)*10;
|
||||
particle.vy = (Math.random()-0.5)*10;
|
||||
particle.alpha = Math.random()*.5+.5;
|
||||
const vy = Math.sqrt(25 - particle.vx * particle.vx);
|
||||
if (Math.abs(particle.vy) > vy) {
|
||||
particle.vy = particle.vy > 0 ? vy: -vy;
|
||||
}
|
||||
this.particles.push(particle);
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async () => {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private drawParticle = (particle: FireworksParticle): void => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
this.context.save();
|
||||
this.context.beginPath();
|
||||
|
||||
this.context.translate(particle.x + particle.w / 2, particle.y + particle.h / 2);
|
||||
this.context.arc(0, 0, particle.w, 0, Math.PI * 2);
|
||||
this.context.fillStyle = particle.color;
|
||||
this.context.globalAlpha = particle.alpha;
|
||||
|
||||
this.context.closePath();
|
||||
this.context.fill();
|
||||
this.context.restore();
|
||||
}
|
||||
|
||||
|
||||
private move = (particle: FireworksParticle) => {
|
||||
particle.x += particle.vx;
|
||||
particle.vy += this.options.gravity;
|
||||
particle.y += particle.vy;
|
||||
particle.alpha -= 0.01;
|
||||
return !(particle.x <= -particle.w || particle.x >= screen.width ||
|
||||
particle.y >= screen.height ||
|
||||
particle.alpha <= 0);
|
||||
}
|
||||
}
|
|
@ -47,23 +47,47 @@ type ConfettiOptions = {
|
|||
/**
|
||||
* max confetti count
|
||||
*/
|
||||
maxCount: number,
|
||||
maxCount: number;
|
||||
/**
|
||||
* particle animation speed
|
||||
*/
|
||||
speed: number,
|
||||
speed: number;
|
||||
/**
|
||||
* the confetti animation frame interval in milliseconds
|
||||
*/
|
||||
frameInterval: number,
|
||||
frameInterval: number;
|
||||
/**
|
||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number,
|
||||
alpha: number;
|
||||
/**
|
||||
* use gradient instead of solid particle color
|
||||
*/
|
||||
gradient: boolean,
|
||||
gradient: boolean;
|
||||
};
|
||||
type FireworksOptions = {
|
||||
/**
|
||||
* max fireworks count
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* gravity value that firework adds to shift from it's start position
|
||||
*/
|
||||
gravity: number;
|
||||
}
|
||||
type SnowfallOptions = {
|
||||
/**
|
||||
* The maximum number of snowflakes to render at a given time
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* The amount of gravity to apply to the snowflakes
|
||||
*/
|
||||
gravity: number;
|
||||
/**
|
||||
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||
*/
|
||||
maxDrift: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,6 +108,29 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
|
|||
gradient: false,
|
||||
},
|
||||
} as Effect<ConfettiOptions>,
|
||||
{
|
||||
emojis: ['🎆'],
|
||||
msgType: 'nic.custom.fireworks',
|
||||
command: 'fireworks',
|
||||
description: () => _td("Sends the given message with fireworks"),
|
||||
fallbackMessage: () => _t("sends fireworks") + " 🎆",
|
||||
options: {
|
||||
maxCount: 500,
|
||||
gravity: 0.05,
|
||||
},
|
||||
} as Effect<FireworksOptions>,
|
||||
{
|
||||
emojis: ['❄', '🌨'],
|
||||
msgType: 'io.element.effect.snowfall',
|
||||
command: 'snowfall',
|
||||
description: () => _td("Sends the given message with snowfall"),
|
||||
fallbackMessage: () => _t("sends snowfall") + " ❄",
|
||||
options: {
|
||||
maxCount: 200,
|
||||
gravity: 0.05,
|
||||
maxDrift: 5,
|
||||
},
|
||||
} as Effect<SnowfallOptions>,
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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';
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
|
||||
export type SnowfallOptions = {
|
||||
/**
|
||||
* The maximum number of snowflakes to render at a given time
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* The amount of gravity to apply to the snowflakes
|
||||
*/
|
||||
gravity: number;
|
||||
/**
|
||||
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||
*/
|
||||
maxDrift: number;
|
||||
}
|
||||
|
||||
type Snowflake = {
|
||||
x: number;
|
||||
y: number;
|
||||
xCol: number;
|
||||
diameter: number;
|
||||
maximumDrift: number;
|
||||
gravity: number;
|
||||
}
|
||||
|
||||
export const DefaultOptions: SnowfallOptions = {
|
||||
maxCount: 200,
|
||||
gravity: 0.05,
|
||||
maxDrift: 5,
|
||||
};
|
||||
|
||||
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
|
||||
|
||||
export default class Snowfall implements ICanvasEffect {
|
||||
private readonly options: SnowfallOptions;
|
||||
|
||||
constructor(options: { [key: string]: any }) {
|
||||
this.options = {...DefaultOptions, ...options};
|
||||
}
|
||||
|
||||
private context: CanvasRenderingContext2D | null = null;
|
||||
private particles: Array<Snowflake> = [];
|
||||
private lastAnimationTime: number;
|
||||
|
||||
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 Snowflake, canvas.width, canvas.height));
|
||||
}
|
||||
this.isRunning = true;
|
||||
requestAnimationFrame(this.renderLoop);
|
||||
if (timeout) {
|
||||
window.setTimeout(this.stop, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async () => {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private resetParticle = (particle: Snowflake, width: number, height: number): Snowflake => {
|
||||
particle.x = Math.random() * width;
|
||||
particle.y = Math.random() * -height;
|
||||
particle.xCol = particle.x;
|
||||
particle.diameter = (Math.random() * 7) + 4;
|
||||
particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5;
|
||||
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
|
||||
return particle;
|
||||
}
|
||||
|
||||
private renderLoop = (): 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 timeDelta = Date.now() - this.lastAnimationTime;
|
||||
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
|
||||
// Clear the screen first
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
|
||||
this.lastAnimationTime = Date.now();
|
||||
this.animateAndRenderSnowflakes();
|
||||
}
|
||||
requestAnimationFrame(this.renderLoop);
|
||||
}
|
||||
};
|
||||
|
||||
private animateAndRenderSnowflakes() {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
const height = this.context.canvas.height;
|
||||
for (const particle of arrayFastClone(this.particles)) {
|
||||
particle.y += particle.gravity;
|
||||
|
||||
// We treat the drift as a sine function to have a more fluid-like movement instead
|
||||
// of a pong-like movement off walls of the X column. This means that for
|
||||
// $x=A\sin(\frac{2\pi}{P}y)$ we use the `maximumDrift` as the amplitude (A) and a
|
||||
// large multiplier to create a very long waveform through P.
|
||||
const peakDistance = 75 * particle.maximumDrift;
|
||||
const PI2 = Math.PI * 2;
|
||||
particle.x = particle.maximumDrift * Math.sin((PI2 / peakDistance) * particle.y);
|
||||
particle.x += particle.xCol; // bring the particle to the right place
|
||||
|
||||
const radius = particle.diameter / 2;
|
||||
this.context.save();
|
||||
this.context.beginPath();
|
||||
this.context.ellipse(particle.x, particle.y, radius, radius, 0, 0, 360);
|
||||
this.context.fillStyle = '#eaeaea'; // grey so it shows up on the light theme
|
||||
this.context.fill();
|
||||
this.context.closePath();
|
||||
this.context.restore();
|
||||
|
||||
// Remove any dead snowflakes
|
||||
const maxBounds = radius * 4; // make sure it's *really* off screen
|
||||
if (particle.y > (height + maxBounds)) {
|
||||
const idx = this.particles.indexOf(particle);
|
||||
this.particles.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -411,6 +411,8 @@
|
|||
"Command error": "Command error",
|
||||
"Usage": "Usage",
|
||||
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
||||
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
||||
"Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message",
|
||||
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown",
|
||||
"Sends a message as html, without interpreting it as markdown": "Sends a message as html, without interpreting it as markdown",
|
||||
|
@ -847,6 +849,10 @@
|
|||
"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",
|
||||
"Sends the given message with fireworks": "Sends the given message with fireworks",
|
||||
"sends fireworks": "sends fireworks",
|
||||
"Sends the given message with snowfall": "Sends the given message with snowfall",
|
||||
"sends snowfall": "sends snowfall",
|
||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
|
|
Loading…
Reference in New Issue