From 9f044cbc888201e72a247fd334fbf43058bdfa1a Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:56:34 +0100 Subject: [PATCH 01/14] add heart effect --- src/effects/hearts/index.ts | 192 ++++++++++++++++++++++++++++++++++++ src/effects/index.ts | 12 +++ src/i18n/strings/en_EN.json | 2 + 3 files changed, 206 insertions(+) create mode 100644 src/effects/hearts/index.ts diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts new file mode 100644 index 00000000000..a0094693553 --- /dev/null +++ b/src/effects/hearts/index.ts @@ -0,0 +1,192 @@ +/* + Copyright 2021 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 HeartOptions = { + /** + * The maximum number of hearts to render at a given time + */ + maxCount: number; + /** + * The amount of gravity to apply to the hearts + */ + gravity: number; + /** + * The maximum amount of drift (horizontal sway) to apply to the hearts. Each heart varies. + */ + maxDrift: number; + /** + * The maximum amount of tilt to apply to the heart. Each heart varies. + */ + maxRot: number; +} + +type Heart = { + x: number; + y: number; + xCol: number; + scale: number; + maximumDrift: number; + maximumRot: number; + gravity: number; + color: string, +} + +export const DefaultOptions: HeartOptions = { + maxCount: 120, + gravity: 3.2, + maxDrift: 5, + maxRot: 5, +}; + +const KEY_FRAME_INTERVAL = 15; // 15ms, roughly + +export default class Hearts implements ICanvasEffect { + private readonly options: HeartOptions; + + constructor(options: { [key: string]: any }) { + this.options = { ...DefaultOptions, ...options }; + } + + private context: CanvasRenderingContext2D | null = null; + private particles: Array = []; + private lastAnimationTime: number; + + private colours = [ + 'rgba(194,210,224,1)', + 'rgba(235,214,219,1)', + 'rgba(255,211,45,1)', + 'rgba(255,190,174,1)', + 'rgba(255,173,226,1)', + 'rgba(242,114,171,1)', + 'rgba(228,55,116,1)', + 'rgba(255,86,130,1)', + 'rgba(244,36,57,1)', + 'rgba(247,126,157,1)',//w + 'rgba(243,142,140,1)', + 'rgba(252,116,183,1)']; + + 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 Heart, 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: Heart, width: number, height: number): Heart => { + particle.color = this.colours[(Math.random() * this.colours.length) | 0]; + particle.x = Math.random() * width; + particle.y = Math.random() * height + height; + particle.xCol = particle.x; + particle.scale = (Math.random() * 0.07) + 0.04; + particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5; + particle.maximumRot = (Math.random() * this.options.maxRot) + 3.5; + particle.gravity = this.options.gravity + (Math.random() * 4.8); + 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 = 6 * particle.maximumDrift * Math.sin(0.7 * (PI2 / peakDistance) * particle.y); + particle.x += particle.xCol; // bring the particle to the right place + + let posScale = 1 / particle.scale; + let x = particle.x * posScale; + let y = particle.y * posScale; + + this.context.save(); + this.context.scale(particle.scale, particle.scale); + this.context.beginPath(); + + // Rotate the heart about its centre. + // The tilt of the heart is modelled similarly to its horizontal drift, + // using a sine function. + this.context.translate(248 + x, 215 + y); + this.context.rotate((1 / 10) * particle.maximumRot * Math.sin((PI2 / peakDistance) * particle.y * 0.8)); + this.context.translate(-248 - x, -215 - y); + + // Use bezier curves to draw a heart using pre-calculated coordinates. + this.context.moveTo(140 + x, 20 + y); + this.context.bezierCurveTo(73 + x, 20 + y, 20 + x, 74 + y, 20 + x, 140 + y); + this.context.bezierCurveTo(20 + x, 275 + y, 156 + x, 310 + y, 248 + x, 443 + y); + this.context.bezierCurveTo(336 + x, 311 + y, 477 + x, 270 + y, 477 + x, 140 + y); + this.context.bezierCurveTo(477 + x, 74 + y, 423 + x, 20 + y, 357 + x, 20 + y); + this.context.bezierCurveTo(309 + x, 20 + y, 267 + x, 48 + y, 248 + x, 89 + y); + this.context.bezierCurveTo(229 + x, 48 + y, 188 + x, 20 + y, 140 + x, 20 + y); + this.context.closePath(); + + this.context.fillStyle = particle.color; + this.context.fill(); + + this.context.restore(); + + // Remove any dead hearts after a 100px wide margin. + if (particle.y < -100 ) { + console.log("disappear") + const idx = this.particles.indexOf(particle); + this.particles.splice(idx, 1); + } + } + } +} diff --git a/src/effects/index.ts b/src/effects/index.ts index 8ecb80020d4..be1ab9c43e0 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -20,6 +20,7 @@ import { Effect } from "./effect"; import { FireworksOptions } from "./fireworks"; import { SnowfallOptions } from "./snowfall"; import { SpaceInvadersOptions } from "./spaceinvaders"; +import { HeartOptions } from "./hearts"; /** * This configuration defines room effects that can be triggered by custom message types and emojis @@ -73,6 +74,17 @@ export const CHAT_EFFECTS: Array> = [ gravity: 0.01, }, } as Effect, + { + emojis: ["💝", "💖", "💞", "💕"], + msgType: "io.element.effect.hearts", + command: "hearts", + description: () => _td("Sends the given message with hearts"), + fallbackMessage: () => _t("sends hearts") + " 💝", + options: { + maxCount: 120, + gravity: 3.2, + }, + } as Effect, ]; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 17d6f64c460..def5c093646 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -889,6 +889,8 @@ "sends snowfall": "sends snowfall", "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", "sends space invaders": "sends space invaders", + "Sends the given message with hearts": "Sends the given message with hearts", + "sends hearts": "sends hearts", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", From 4d8aecf02363ef11418f94e77d24a774b780b1ee Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Tue, 15 Jun 2021 14:09:45 +0100 Subject: [PATCH 02/14] clean up syntax --- src/effects/hearts/index.ts | 6 ++---- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index a0094693553..6a3705d20e0 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -128,17 +128,16 @@ export default class Hearts implements ICanvasEffect { this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); this.lastAnimationTime = Date.now(); - this.animateAndRenderSnowflakes(); + this.animateAndRenderHearts(); } requestAnimationFrame(this.renderLoop); } }; - private animateAndRenderSnowflakes() { + private animateAndRenderHearts() { if (!this.context || !this.context.canvas) { return; } - const height = this.context.canvas.height; for (const particle of arrayFastClone(this.particles)) { particle.y -= particle.gravity; @@ -183,7 +182,6 @@ export default class Hearts implements ICanvasEffect { // Remove any dead hearts after a 100px wide margin. if (particle.y < -100 ) { - console.log("disappear") const idx = this.particles.indexOf(particle); this.particles.splice(idx, 1); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index def5c093646..53d27270b71 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -889,7 +889,7 @@ "sends snowfall": "sends snowfall", "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", "sends space invaders": "sends space invaders", - "Sends the given message with hearts": "Sends the given message with hearts", + "Sends the given message with hearts": "Sends the given message with hearts", "sends hearts": "sends hearts", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", From 08c16946774f8748d3e98eebef99cc614dd61ae8 Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Tue, 15 Jun 2021 14:14:38 +0100 Subject: [PATCH 03/14] convert indentation to spaces --- src/effects/hearts/index.ts | 318 ++++++++++++++++++------------------ src/effects/index.ts | 4 +- 2 files changed, 160 insertions(+), 162 deletions(-) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index 6a3705d20e0..c971b79eba4 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -17,174 +17,174 @@ import ICanvasEffect from '../ICanvasEffect'; import { arrayFastClone } from "../../utils/arrays"; export type HeartOptions = { - /** - * The maximum number of hearts to render at a given time - */ - maxCount: number; - /** - * The amount of gravity to apply to the hearts - */ - gravity: number; - /** - * The maximum amount of drift (horizontal sway) to apply to the hearts. Each heart varies. - */ - maxDrift: number; - /** - * The maximum amount of tilt to apply to the heart. Each heart varies. - */ - maxRot: number; + /** + * The maximum number of hearts to render at a given time + */ + maxCount: number; + /** + * The amount of gravity to apply to the hearts + */ + gravity: number; + /** + * The maximum amount of drift (horizontal sway) to apply to the hearts. Each heart varies. + */ + maxDrift: number; + /** + * The maximum amount of tilt to apply to the heart. Each heart varies. + */ + maxRot: number; } type Heart = { - x: number; - y: number; - xCol: number; - scale: number; - maximumDrift: number; - maximumRot: number; - gravity: number; - color: string, + x: number; + y: number; + xCol: number; + scale: number; + maximumDrift: number; + maximumRot: number; + gravity: number; + color: string, } export const DefaultOptions: HeartOptions = { - maxCount: 120, - gravity: 3.2, - maxDrift: 5, - maxRot: 5, + maxCount: 120, + gravity: 3.2, + maxDrift: 5, + maxRot: 5, }; const KEY_FRAME_INTERVAL = 15; // 15ms, roughly export default class Hearts implements ICanvasEffect { - private readonly options: HeartOptions; - - constructor(options: { [key: string]: any }) { - this.options = { ...DefaultOptions, ...options }; - } - - private context: CanvasRenderingContext2D | null = null; - private particles: Array = []; - private lastAnimationTime: number; - - private colours = [ - 'rgba(194,210,224,1)', - 'rgba(235,214,219,1)', - 'rgba(255,211,45,1)', - 'rgba(255,190,174,1)', - 'rgba(255,173,226,1)', - 'rgba(242,114,171,1)', - 'rgba(228,55,116,1)', - 'rgba(255,86,130,1)', - 'rgba(244,36,57,1)', - 'rgba(247,126,157,1)',//w - 'rgba(243,142,140,1)', - 'rgba(252,116,183,1)']; - - 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 Heart, 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: Heart, width: number, height: number): Heart => { - particle.color = this.colours[(Math.random() * this.colours.length) | 0]; - particle.x = Math.random() * width; - particle.y = Math.random() * height + height; - particle.xCol = particle.x; - particle.scale = (Math.random() * 0.07) + 0.04; - particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5; - particle.maximumRot = (Math.random() * this.options.maxRot) + 3.5; - particle.gravity = this.options.gravity + (Math.random() * 4.8); - 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.animateAndRenderHearts(); - } - requestAnimationFrame(this.renderLoop); - } - }; - - private animateAndRenderHearts() { - if (!this.context || !this.context.canvas) { - return; - } - 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 = 6 * particle.maximumDrift * Math.sin(0.7 * (PI2 / peakDistance) * particle.y); - particle.x += particle.xCol; // bring the particle to the right place - - let posScale = 1 / particle.scale; - let x = particle.x * posScale; - let y = particle.y * posScale; - - this.context.save(); - this.context.scale(particle.scale, particle.scale); - this.context.beginPath(); - - // Rotate the heart about its centre. - // The tilt of the heart is modelled similarly to its horizontal drift, - // using a sine function. - this.context.translate(248 + x, 215 + y); - this.context.rotate((1 / 10) * particle.maximumRot * Math.sin((PI2 / peakDistance) * particle.y * 0.8)); - this.context.translate(-248 - x, -215 - y); - - // Use bezier curves to draw a heart using pre-calculated coordinates. - this.context.moveTo(140 + x, 20 + y); - this.context.bezierCurveTo(73 + x, 20 + y, 20 + x, 74 + y, 20 + x, 140 + y); - this.context.bezierCurveTo(20 + x, 275 + y, 156 + x, 310 + y, 248 + x, 443 + y); - this.context.bezierCurveTo(336 + x, 311 + y, 477 + x, 270 + y, 477 + x, 140 + y); - this.context.bezierCurveTo(477 + x, 74 + y, 423 + x, 20 + y, 357 + x, 20 + y); - this.context.bezierCurveTo(309 + x, 20 + y, 267 + x, 48 + y, 248 + x, 89 + y); - this.context.bezierCurveTo(229 + x, 48 + y, 188 + x, 20 + y, 140 + x, 20 + y); - this.context.closePath(); - - this.context.fillStyle = particle.color; - this.context.fill(); - - this.context.restore(); - - // Remove any dead hearts after a 100px wide margin. - if (particle.y < -100 ) { - const idx = this.particles.indexOf(particle); - this.particles.splice(idx, 1); - } - } - } + private readonly options: HeartOptions; + + constructor(options: { [key: string]: any }) { + this.options = { ...DefaultOptions, ...options }; + } + + private context: CanvasRenderingContext2D | null = null; + private particles: Array = []; + private lastAnimationTime: number; + + private colours = [ + 'rgba(194,210,224,1)', + 'rgba(235,214,219,1)', + 'rgba(255,211,45,1)', + 'rgba(255,190,174,1)', + 'rgba(255,173,226,1)', + 'rgba(242,114,171,1)', + 'rgba(228,55,116,1)', + 'rgba(255,86,130,1)', + 'rgba(244,36,57,1)', + 'rgba(247,126,157,1)',//w + 'rgba(243,142,140,1)', + 'rgba(252,116,183,1)']; + + 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 Heart, 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: Heart, width: number, height: number): Heart => { + particle.color = this.colours[(Math.random() * this.colours.length) | 0]; + particle.x = Math.random() * width; + particle.y = Math.random() * height + height; + particle.xCol = particle.x; + particle.scale = (Math.random() * 0.07) + 0.04; + particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5; + particle.maximumRot = (Math.random() * this.options.maxRot) + 3.5; + particle.gravity = this.options.gravity + (Math.random() * 4.8); + 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.animateAndRenderHearts(); + } + requestAnimationFrame(this.renderLoop); + } + }; + + private animateAndRenderHearts() { + if (!this.context || !this.context.canvas) { + return; + } + 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 = 6 * particle.maximumDrift * Math.sin(0.7 * (PI2 / peakDistance) * particle.y); + particle.x += particle.xCol; // bring the particle to the right place + + let posScale = 1 / particle.scale; + let x = particle.x * posScale; + let y = particle.y * posScale; + + this.context.save(); + this.context.scale(particle.scale, particle.scale); + this.context.beginPath(); + + // Rotate the heart about its centre. + // The tilt of the heart is modelled similarly to its horizontal drift, + // using a sine function. + this.context.translate(248 + x, 215 + y); + this.context.rotate((1 / 10) * particle.maximumRot * Math.sin((PI2 / peakDistance) * particle.y * 0.8)); + this.context.translate(-248 - x, -215 - y); + + // Use bezier curves to draw a heart using pre-calculated coordinates. + this.context.moveTo(140 + x, 20 + y); + this.context.bezierCurveTo(73 + x, 20 + y, 20 + x, 74 + y, 20 + x, 140 + y); + this.context.bezierCurveTo(20 + x, 275 + y, 156 + x, 310 + y, 248 + x, 443 + y); + this.context.bezierCurveTo(336 + x, 311 + y, 477 + x, 270 + y, 477 + x, 140 + y); + this.context.bezierCurveTo(477 + x, 74 + y, 423 + x, 20 + y, 357 + x, 20 + y); + this.context.bezierCurveTo(309 + x, 20 + y, 267 + x, 48 + y, 248 + x, 89 + y); + this.context.bezierCurveTo(229 + x, 48 + y, 188 + x, 20 + y, 140 + x, 20 + y); + this.context.closePath(); + + this.context.fillStyle = particle.color; + this.context.fill(); + + this.context.restore(); + + // Remove any dead hearts after a 100px wide margin. + if (particle.y < -100 ) { + const idx = this.particles.indexOf(particle); + this.particles.splice(idx, 1); + } + } + } } diff --git a/src/effects/index.ts b/src/effects/index.ts index be1ab9c43e0..500e37625f9 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -74,7 +74,7 @@ export const CHAT_EFFECTS: Array> = [ gravity: 0.01, }, } as Effect, - { + { emojis: ["💝", "💖", "💞", "💕"], msgType: "io.element.effect.hearts", command: "hearts", @@ -86,5 +86,3 @@ export const CHAT_EFFECTS: Array> = [ }, } as Effect, ]; - - From 9f6ef9eb13772e12550b0180798d8f2de5a9bab2 Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Tue, 15 Jun 2021 14:29:37 +0100 Subject: [PATCH 04/14] appease the linter --- src/effects/hearts/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index c971b79eba4..56447e810d5 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -76,7 +76,7 @@ export default class Hearts implements ICanvasEffect { 'rgba(228,55,116,1)', 'rgba(255,86,130,1)', 'rgba(244,36,57,1)', - 'rgba(247,126,157,1)',//w + 'rgba(247,126,157,1)', 'rgba(243,142,140,1)', 'rgba(252,116,183,1)']; @@ -150,9 +150,9 @@ export default class Hearts implements ICanvasEffect { particle.x = 6 * particle.maximumDrift * Math.sin(0.7 * (PI2 / peakDistance) * particle.y); particle.x += particle.xCol; // bring the particle to the right place - let posScale = 1 / particle.scale; - let x = particle.x * posScale; - let y = particle.y * posScale; + const posScale = 1 / particle.scale; + const x = particle.x * posScale; + const y = particle.y * posScale; this.context.save(); this.context.scale(particle.scale, particle.scale); From 03a5c2c8ecfa270aa0bb38e2c44b8244df65e2ec Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Wed, 6 Oct 2021 15:37:17 +0100 Subject: [PATCH 05/14] hopefully resolve merge conflict --- src/effects/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/index.ts b/src/effects/index.ts index 4812c91bce1..500e37625f9 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -84,5 +84,5 @@ export const CHAT_EFFECTS: Array> = [ maxCount: 120, gravity: 3.2, }, - } as Effect, + } as Effect, ]; From 53b6fd828a0bd05c4b2a2bb7ca09e4a01abc3bbc Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Wed, 6 Oct 2021 15:38:37 +0100 Subject: [PATCH 06/14] add missing semicolons --- src/effects/hearts/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index 56447e810d5..9ef54b021fe 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -33,7 +33,7 @@ export type HeartOptions = { * The maximum amount of tilt to apply to the heart. Each heart varies. */ maxRot: number; -} +}; type Heart = { x: number; @@ -43,8 +43,8 @@ type Heart = { maximumDrift: number; maximumRot: number; gravity: number; - color: string, -} + color: string; +}; export const DefaultOptions: HeartOptions = { maxCount: 120, @@ -97,11 +97,11 @@ export default class Hearts implements ICanvasEffect { if (timeout) { window.setTimeout(this.stop, timeout); } - } + }; public stop = async () => { this.isRunning = false; - } + }; private resetParticle = (particle: Heart, width: number, height: number): Heart => { particle.color = this.colours[(Math.random() * this.colours.length) | 0]; @@ -113,7 +113,7 @@ export default class Hearts implements ICanvasEffect { particle.maximumRot = (Math.random() * this.options.maxRot) + 3.5; particle.gravity = this.options.gravity + (Math.random() * 4.8); return particle; - } + }; private renderLoop = (): void => { if (!this.context || !this.context.canvas) { From 5e83d9b9109f5f1922555484f7492596cc5966ff Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Wed, 6 Oct 2021 17:54:22 +0100 Subject: [PATCH 07/14] add label to toggle switch --- src/components/views/settings/SetIntegrationManager.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.tsx b/src/components/views/settings/SetIntegrationManager.tsx index e083efae0ef..9db5c58b586 100644 --- a/src/components/views/settings/SetIntegrationManager.tsx +++ b/src/components/views/settings/SetIntegrationManager.tsx @@ -79,10 +79,13 @@ export default class SetIntegrationManager extends React.Component { _t("Manage integrations") } { managerName } - + - { bodyText }

{ _t( From f2a51abfae3965343acfdc107d6e3f587d7cbe75 Mon Sep 17 00:00:00 2001 From: ColonisationCaptain <52425971+ColonisationCaptain@users.noreply.github.com> Date: Thu, 7 Oct 2021 00:42:58 +0100 Subject: [PATCH 08/14] Revert "add label to toggle switch" This reverts commit 5e83d9b9109f5f1922555484f7492596cc5966ff. --- src/components/views/settings/SetIntegrationManager.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.tsx b/src/components/views/settings/SetIntegrationManager.tsx index 9db5c58b586..e083efae0ef 100644 --- a/src/components/views/settings/SetIntegrationManager.tsx +++ b/src/components/views/settings/SetIntegrationManager.tsx @@ -79,13 +79,10 @@ export default class SetIntegrationManager extends React.Component { _t("Manage integrations") } { managerName } - + + { bodyText }

{ _t( From 3dea9c1f031806de74f2ed9ab270d85a54eb2660 Mon Sep 17 00:00:00 2001 From: CicadaCinema <52425971+CicadaCinema@users.noreply.github.com> Date: Sun, 13 Feb 2022 13:28:00 +0000 Subject: [PATCH 09/14] remove extra space --- src/effects/hearts/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index 9ef54b021fe..8fd3a8ecbe7 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -181,7 +181,7 @@ export default class Hearts implements ICanvasEffect { this.context.restore(); // Remove any dead hearts after a 100px wide margin. - if (particle.y < -100 ) { + if (particle.y < -100) { const idx = this.particles.indexOf(particle); this.particles.splice(idx, 1); } From e1b68b0f05b37f5d2ceb77e50f97547c67f6f81b Mon Sep 17 00:00:00 2001 From: CicadaCinema <52425971+CicadaCinema@users.noreply.github.com> Date: Mon, 21 Feb 2022 22:04:04 +0000 Subject: [PATCH 10/14] replace gift heart with emoji heart in timeline message --- src/effects/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/index.ts b/src/effects/index.ts index 2c131d806d3..47f36484d41 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -91,7 +91,7 @@ export const CHAT_EFFECTS: Array> = [ msgType: "io.element.effect.hearts", command: "hearts", description: () => _td("Sends the given message with hearts"), - fallbackMessage: () => _t("sends hearts") + " 💝", + fallbackMessage: () => _t("sends hearts") + " 💞", options: { maxCount: 120, gravity: 3.2, From bc87cd9a7f81b80a83a6cea564507de2acf9724b Mon Sep 17 00:00:00 2001 From: CicadaCinema <52425971+CicadaCinema@users.noreply.github.com> Date: Tue, 8 Mar 2022 00:10:13 +0000 Subject: [PATCH 11/14] reduce number of emoji triggers --- src/effects/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/index.ts b/src/effects/index.ts index 47f36484d41..d165dd0ef51 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -87,7 +87,7 @@ export const CHAT_EFFECTS: Array> = [ }, } as Effect, { - emojis: ["💝", "💖", "💞", "💕"], + emojis: ["💝"], msgType: "io.element.effect.hearts", command: "hearts", description: () => _td("Sends the given message with hearts"), From da6d6d101c81d7190b0d6b740d29f34a841bc46f Mon Sep 17 00:00:00 2001 From: CicadaCinema <52425971+CicadaCinema@users.noreply.github.com> Date: Thu, 10 Mar 2022 14:36:37 +0000 Subject: [PATCH 12/14] copyright should be myself --- src/effects/hearts/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/effects/hearts/index.ts b/src/effects/hearts/index.ts index 8fd3a8ecbe7..1fb743f99c6 100644 --- a/src/effects/hearts/index.ts +++ b/src/effects/hearts/index.ts @@ -1,5 +1,6 @@ /* Copyright 2021 The Matrix.org Foundation C.I.C. + Copyright 2022 Arseny Uskov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 579a166113a00b44b4e5515c590a65ce936cb728 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 8 Apr 2022 20:48:57 +0200 Subject: [PATCH 13/14] Fix: Remove jittery timeline scrolling after jumping to an event (#8263) * Fix: Remove jittery timeline scrolling after jumping to an event * Fix: Remove onUserScroll handler and merge it with onScroll * Fix: Reset scrollIntoView state earlier Co-authored-by: Janne Mareike Koschinski --- src/components/structures/MessagePanel.tsx | 6 +- src/components/structures/RightPanel.tsx | 1 + src/components/structures/RoomView.tsx | 44 ++++++---- src/components/structures/ScrollPanel.tsx | 15 +--- src/components/structures/ThreadView.tsx | 12 ++- src/components/structures/TimelinePanel.tsx | 86 +++++++++++-------- .../views/right_panel/TimelineCard.tsx | 25 +++--- src/dispatcher/dispatch-actions/threads.ts | 2 + src/dispatcher/payloads/ViewRoomPayload.ts | 1 + src/stores/RoomViewStore.tsx | 9 ++ .../right-panel/RightPanelStoreIPanelState.ts | 4 + 11 files changed, 118 insertions(+), 87 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 781bdb282c9..a4791a1ec57 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react'; +import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; @@ -170,9 +170,6 @@ interface IProps { // callback which is called when the panel is scrolled. onScroll?(event: Event): void; - // callback which is called when the user interacts with the room timeline - onUserScroll(event: SyntheticEvent): void; - // callback which is called when more content is needed. onFillRequest?(backwards: boolean): Promise; @@ -1030,7 +1027,6 @@ export default class MessagePanel extends React.Component { ref={this.scrollPanel} className={classes} onScroll={this.props.onScroll} - onUserScroll={this.props.onUserScroll} onFillRequest={this.props.onFillRequest} onUnfillRequest={this.props.onUnfillRequest} style={style} diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 5b1bf3f1edc..5c2eaaf9a4e 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -231,6 +231,7 @@ export default class RightPanel extends React.Component { mxEvent={cardState.threadHeadEvent} initialEvent={cardState.initialEvent} isInitialEventHighlighted={cardState.isInitialEventHighlighted} + initialEventScrollIntoView={cardState.initialEventScrollIntoView} permalinkCreator={this.props.permalinkCreator} e2eStatus={this.props.e2eStatus} />; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5ef6f2282f9..46183cdadb2 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -155,6 +155,8 @@ export interface IRoomState { initialEventPixelOffset?: number; // Whether to highlight the event scrolled to isInitialEventHighlighted?: boolean; + // Whether to scroll the event into view + initialEventScrollIntoView?: boolean; replyToEvent?: MatrixEvent; numUnreadMessages: number; searchTerm?: string; @@ -404,7 +406,8 @@ export class RoomView extends React.Component { const roomId = RoomViewStore.instance.getRoomId(); - const newState: Pick = { + // This convoluted type signature ensures we get IntelliSense *and* correct typing + const newState: Partial & Pick = { roomId, roomAlias: RoomViewStore.instance.getRoomAlias(), roomLoading: RoomViewStore.instance.isRoomLoading(), @@ -443,22 +446,29 @@ export class RoomView extends React.Component { ); } + // If we have an initial event, we want to reset the event pixel offset to ensure it ends up + // visible + newState.initialEventPixelOffset = null; + const thread = initialEvent?.getThread(); if (thread && !initialEvent?.isThreadRoot) { showThread({ rootEvent: thread.rootEvent, initialEvent, highlighted: RoomViewStore.instance.isInitialEventHighlighted(), + scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(), }); } else { newState.initialEventId = initialEventId; newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted(); + newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView(); if (thread && initialEvent?.isThreadRoot) { showThread({ rootEvent: thread.rootEvent, initialEvent, highlighted: RoomViewStore.instance.isInitialEventHighlighted(), + scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(), }); } } @@ -758,19 +768,6 @@ export class RoomView extends React.Component { } } - private onUserScroll = () => { - if (this.state.initialEventId && this.state.isInitialEventHighlighted) { - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.state.room.roomId, - event_id: this.state.initialEventId, - highlighted: false, - replyingToEvent: this.state.replyToEvent, - metricsTrigger: undefined, // room doesn't change - }); - } - }; - private onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId), @@ -1301,6 +1298,22 @@ export class RoomView extends React.Component { this.updateTopUnreadMessagesBar(); }; + private resetJumpToEvent = (eventId?: string) => { + if (this.state.initialEventId && this.state.initialEventScrollIntoView && + this.state.initialEventId === eventId) { + debuglog("Removing scroll_into_view flag from initial event"); + dis.dispatch({ + action: Action.ViewRoom, + room_id: this.state.room.roomId, + event_id: this.state.initialEventId, + highlighted: this.state.isInitialEventHighlighted, + scroll_into_view: false, + replyingToEvent: this.state.replyToEvent, + metricsTrigger: undefined, // room doesn't change + }); + } + }; + private injectSticker(url: string, info: object, text: string, threadId: string | null) { if (this.context.isGuest()) { dis.dispatch({ action: 'require_registration' }); @@ -2051,9 +2064,10 @@ export class RoomView extends React.Component { hidden={hideMessagePanel} highlightedEventId={highlightedEventId} eventId={this.state.initialEventId} + eventScrollIntoView={this.state.initialEventScrollIntoView} eventPixelOffset={this.state.initialEventPixelOffset} onScroll={this.onMessageListScroll} - onUserScroll={this.onUserScroll} + onEventScrolledIntoView={this.resetJumpToEvent} onReadMarkerUpdated={this.updateTopUnreadMessagesBar} showUrlPreview={this.state.showUrlPreview} className={this.messagePanelClassNames} diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index d580f02bc09..359c10509d4 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react"; +import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import Timer from '../../utils/Timer'; @@ -109,10 +109,6 @@ interface IProps { /* onScroll: a callback which is called whenever any scroll happens. */ onScroll?(event: Event): void; - - /* onUserScroll: callback which is called when the user interacts with the room timeline - */ - onUserScroll?(event: SyntheticEvent): void; } /* This component implements an intelligent scrolling list. @@ -593,29 +589,21 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ public handleScrollKey = (ev: KeyboardEvent) => { - let isScrolling = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case KeyBindingAction.ScrollUp: this.scrollRelative(-1); - isScrolling = true; break; case KeyBindingAction.ScrollDown: this.scrollRelative(1); - isScrolling = true; break; case KeyBindingAction.JumpToFirstMessage: this.scrollToTop(); - isScrolling = true; break; case KeyBindingAction.JumpToLatestMessage: this.scrollToBottom(); - isScrolling = true; break; } - if (isScrolling && this.props.onUserScroll) { - this.props.onUserScroll(ev); - } }; /* Scroll the panel to bring the DOM node with the scroll token @@ -965,7 +953,6 @@ export default class ScrollPanel extends React.Component { diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index fca2c146fbe..12dd84685d9 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -61,6 +61,7 @@ interface IProps { e2eStatus?: E2EStatus; initialEvent?: MatrixEvent; isInitialEventHighlighted?: boolean; + initialEventScrollIntoView?: boolean; } interface IState { @@ -215,13 +216,15 @@ export default class ThreadView extends React.Component { } }; - private resetHighlightedEvent = (): void => { - if (this.props.initialEvent && this.props.isInitialEventHighlighted) { + private resetJumpToEvent = (event?: string): void => { + if (this.props.initialEvent && this.props.initialEventScrollIntoView && + event === this.props.initialEvent?.getId()) { dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, event_id: this.props.initialEvent?.getId(), - highlighted: false, + highlighted: this.props.isInitialEventHighlighted, + scroll_into_view: false, replyingToEvent: this.state.replyToEvent, metricsTrigger: undefined, // room doesn't change }); @@ -372,7 +375,8 @@ export default class ThreadView extends React.Component { editState={this.state.editState} eventId={this.props.initialEvent?.getId()} highlightedEventId={highlightedEventId} - onUserScroll={this.resetHighlightedEvent} + eventScrollIntoView={this.props.initialEventScrollIntoView} + onEventScrolledIntoView={this.resetJumpToEvent} onPaginationRequest={this.onPaginationRequest} /> } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index db2f06bef82..f5efe05a4ce 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode, SyntheticEvent } from 'react'; +import React, { createRef, ReactNode } from 'react'; import ReactDOM from "react-dom"; import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; @@ -91,6 +91,9 @@ interface IProps { // id of an event to jump to. If not given, will go to the end of the live timeline. eventId?: string; + // whether we should scroll the event into view + eventScrollIntoView?: boolean; + // where to position the event given by eventId, in pixels from the bottom of the viewport. // If not given, will try to put the event half way down the viewport. eventPixelOffset?: number; @@ -124,8 +127,7 @@ interface IProps { // callback which is called when the panel is scrolled. onScroll?(event: Event): void; - // callback which is called when the user interacts with the room timeline - onUserScroll?(event: SyntheticEvent): void; + onEventScrolledIntoView?(eventId?: string): void; // callback which is called when the read-up-to mark is updated. onReadMarkerUpdated?(): void; @@ -327,9 +329,11 @@ class TimelinePanel extends React.Component { const differentEventId = newProps.eventId != this.props.eventId; const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId; - if (differentEventId || differentHighlightedEventId) { - logger.log("TimelinePanel switching to eventId " + newProps.eventId + - " (was " + this.props.eventId + ")"); + const differentAvoidJump = newProps.eventScrollIntoView && !this.props.eventScrollIntoView; + if (differentEventId || differentHighlightedEventId || differentAvoidJump) { + logger.log("TimelinePanel switching to " + + "eventId " + newProps.eventId + " (was " + this.props.eventId + "), " + + "scrollIntoView: " + newProps.eventScrollIntoView + " (was " + this.props.eventScrollIntoView + ")"); return this.initTimeline(newProps); } } @@ -1123,7 +1127,41 @@ class TimelinePanel extends React.Component { offsetBase = 0.5; } - return this.loadTimeline(initialEvent, pixelOffset, offsetBase); + return this.loadTimeline(initialEvent, pixelOffset, offsetBase, props.eventScrollIntoView); + } + + private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void { + const doScroll = () => { + if (eventId) { + debuglog("TimelinePanel scrolling to eventId " + eventId + + " at position " + (offsetBase * 100) + "% + " + pixelOffset); + this.messagePanel.current.scrollToEvent( + eventId, + pixelOffset, + offsetBase, + ); + } else { + debuglog("TimelinePanel scrolling to bottom"); + this.messagePanel.current.scrollToBottom(); + } + }; + + debuglog("TimelinePanel scheduling scroll to event"); + this.props.onEventScrolledIntoView?.(eventId); + // Ensure the correct scroll position pre render, if the messages have already been loaded to DOM, + // to avoid it jumping around + doScroll(); + + // Ensure the correct scroll position post render for correct behaviour. + // + // requestAnimationFrame runs our code immediately after the DOM update but before the next repaint. + // + // If the messages have just been loaded for the first time, this ensures we'll repeat setting the + // correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and + // updated the DOM. + window.requestAnimationFrame(() => { + doScroll(); + }); } /** @@ -1139,8 +1177,10 @@ class TimelinePanel extends React.Component { * @param {number?} offsetBase the reference point for the pixelOffset. 0 * means the top of the container, 1 means the bottom, and fractional * values mean somewhere in the middle. If omitted, it defaults to 0. + * + * @param {boolean?} scrollIntoView whether to scroll the event into view. */ - private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void { + private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void { this.timelineWindow = new TimelineWindow( MatrixClientPeg.get(), this.props.timelineSet, { windowLimit: this.props.timelineCap }); @@ -1176,32 +1216,9 @@ class TimelinePanel extends React.Component { return; } - const doScroll = () => { - if (eventId) { - debuglog("TimelinePanel scrolling to eventId " + eventId); - this.messagePanel.current.scrollToEvent( - eventId, - pixelOffset, - offsetBase, - ); - } else { - debuglog("TimelinePanel scrolling to bottom"); - this.messagePanel.current.scrollToBottom(); - } - }; - - // Ensure the correct scroll position pre render, if the messages have already been loaded to DOM, to - // avoid it jumping around - doScroll(); - - // Ensure the correct scroll position post render for correct behaviour. - // - // requestAnimationFrame runs our code immediately after the DOM update but before the next repaint. - // - // If the messages have just been loaded for the first time, this ensures we'll repeat setting the - // correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and updated - // the DOM. - window.requestAnimationFrame(doScroll); + if (scrollIntoView) { + this.scrollIntoView(eventId, pixelOffset, offsetBase); + } if (this.props.sendReadReceiptOnLoad) { this.sendReadReceipt(); @@ -1651,7 +1668,6 @@ class TimelinePanel extends React.Component { ourUserId={MatrixClientPeg.get().credentials.userId} stickyBottom={stickyBottom} onScroll={this.onMessageListScroll} - onUserScroll={this.props.onUserScroll} onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour} diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index c5aff580995..7f6060ca2d7 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -146,19 +146,6 @@ export default class TimelineCard extends React.Component { } }; - private onUserScroll = (): void => { - if (this.state.initialEventId && this.state.isInitialEventHighlighted) { - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.props.room.roomId, - event_id: this.state.initialEventId, - highlighted: false, - replyingToEvent: this.state.replyToEvent, - metricsTrigger: undefined, // room doesn't change - }); - } - }; - private onScroll = (): void => { const timelinePanel = this.timelinePanel.current; if (!timelinePanel) return; @@ -171,6 +158,17 @@ export default class TimelineCard extends React.Component { atEndOfLiveTimeline: false, }); } + + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { + dis.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + event_id: this.state.initialEventId, + highlighted: false, + replyingToEvent: this.state.replyToEvent, + metricsTrigger: undefined, // room doesn't change + }); + } }; private onMeasurement = (narrow: boolean): void => { @@ -263,7 +261,6 @@ export default class TimelineCard extends React.Component { resizeNotifier={this.props.resizeNotifier} highlightedEventId={highlightedEventId} onScroll={this.onScroll} - onUserScroll={this.onUserScroll} /> diff --git a/src/dispatcher/dispatch-actions/threads.ts b/src/dispatcher/dispatch-actions/threads.ts index 0cd12e89de9..db01d0b5af2 100644 --- a/src/dispatcher/dispatch-actions/threads.ts +++ b/src/dispatcher/dispatch-actions/threads.ts @@ -26,6 +26,7 @@ export const showThread = (props: { rootEvent: MatrixEvent; initialEvent?: MatrixEvent; highlighted?: boolean; + scroll_into_view?: boolean; push?: boolean; }) => { const push = props.push ?? false; @@ -35,6 +36,7 @@ export const showThread = (props: { threadHeadEvent: props.rootEvent, initialEvent: props.initialEvent, isInitialEventHighlighted: props.highlighted, + initialEventScrollIntoView: props.scroll_into_view, }, }; if (push) { diff --git a/src/dispatcher/payloads/ViewRoomPayload.ts b/src/dispatcher/payloads/ViewRoomPayload.ts index d85c07e6667..376fce2d58d 100644 --- a/src/dispatcher/payloads/ViewRoomPayload.ts +++ b/src/dispatcher/payloads/ViewRoomPayload.ts @@ -32,6 +32,7 @@ export interface ViewRoomPayload extends Pick { event_id?: string; // the event to ensure is in view if any highlighted?: boolean; // whether to highlight `event_id` + scroll_into_view?: boolean; // whether to scroll `event_id` into view should_peek?: boolean; // whether we should peek the room if we are not yet joined joining?: boolean; // whether we have already sent a join request for this room via_servers?: string[]; // the list of servers to join via if no room_alias is provided diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index aec723d13da..77909ae6129 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -62,6 +62,8 @@ const INITIAL_STATE = { initialEventPixelOffset: null, // Whether to highlight the initial event isInitialEventHighlighted: false, + // whether to scroll `event_id` into view + initialEventScrollIntoView: true, // The room alias of the room (or null if not originally specified in view_room) roomAlias: null, @@ -291,6 +293,7 @@ export class RoomViewStore extends Store { roomAlias: payload.room_alias, initialEventId: payload.event_id, isInitialEventHighlighted: payload.highlighted, + initialEventScrollIntoView: payload.scroll_into_view ?? true, roomLoading: false, roomLoadError: null, // should peek by default @@ -333,6 +336,7 @@ export class RoomViewStore extends Store { initialEventId: null, initialEventPixelOffset: null, isInitialEventHighlighted: null, + initialEventScrollIntoView: true, roomAlias: payload.room_alias, roomLoading: true, roomLoadError: null, @@ -475,6 +479,11 @@ export class RoomViewStore extends Store { return this.state.isInitialEventHighlighted; } + // Whether to avoid jumping to the initial event + public initialEventScrollIntoView() { + return this.state.initialEventScrollIntoView; + } + // The room alias of the room (or null if not originally specified in view_room) public getRoomAlias() { return this.state.roomAlias; diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index 066cc1e9efa..0e580ed077e 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -34,6 +34,7 @@ export interface IRightPanelCardState { threadHeadEvent?: MatrixEvent; initialEvent?: MatrixEvent; isInitialEventHighlighted?: boolean; + initialEventScrollIntoView?: boolean; } export interface IRightPanelCardStateStored { @@ -47,6 +48,7 @@ export interface IRightPanelCardStateStored { threadHeadEventId?: string; initialEventId?: string; isInitialEventHighlighted?: boolean; + initialEventScrollIntoView?: boolean; } export interface IRightPanelCard { @@ -87,6 +89,7 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard widgetId: state.widgetId, spaceId: state.spaceId, isInitialEventHighlighted: state.isInitialEventHighlighted, + initialEventScrollIntoView: state.initialEventScrollIntoView, threadHeadEventId: !!state?.threadHeadEvent?.getId() ? panelState.state.threadHeadEvent.getId() : undefined, memberInfoEventId: !!state?.memberInfoEvent?.getId() ? @@ -106,6 +109,7 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): widgetId: stateStored.widgetId, spaceId: stateStored.spaceId, isInitialEventHighlighted: stateStored.isInitialEventHighlighted, + initialEventScrollIntoView: stateStored.initialEventScrollIntoView, threadHeadEvent: !!stateStored?.threadHeadEventId ? room.findEventById(stateStored.threadHeadEventId) : undefined, memberInfoEvent: !!stateStored?.memberInfoEventId ? From ad5d1ad7351e16fc938b78bafe30f4e194a51bdc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 Apr 2022 14:54:06 -0600 Subject: [PATCH 14/14] Update src/effects/index.ts --- src/effects/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/index.ts b/src/effects/index.ts index d165dd0ef51..3468d4c5b6a 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -91,7 +91,7 @@ export const CHAT_EFFECTS: Array> = [ msgType: "io.element.effect.hearts", command: "hearts", description: () => _td("Sends the given message with hearts"), - fallbackMessage: () => _t("sends hearts") + " 💞", + fallbackMessage: () => _t("sends hearts") + " 💝", options: { maxCount: 120, gravity: 3.2,