From 4394a20f87922e768852dd693b7d3eb8ef3f90f4 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 18 Aug 2020 09:56:38 +0200 Subject: [PATCH 0001/1836] setting added to User Settings -> Preferences -> Timeline as an opt out for users with german translation --- .../views/settings/tabs/user/PreferencesUserSettingsTab.js | 1 + src/i18n/strings/de_DE.json | 3 ++- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.ts | 5 +++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index a77815a68c..6ed2fc2e39 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -49,6 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'showAvatarChanges', 'showDisplaynameChanges', 'showImages', + 'dontShowChatEffects', ]; static ADVANCED_SETTINGS = [ diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 09dbcb2e18..edfe21d9d6 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2361,5 +2361,6 @@ "%(brand)s encountered an error during upload of:": "%(brand)s hat einen Fehler festgestellt beim hochladen von:", "Use your account to sign in to the latest version of the app at ": "Verwende dein Konto um dich an der neusten Version der App anzumelden", "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", - "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot" + "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", + "Don't show chat effects": "Chat Effekte nicht zeigen" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 974a96406f..98aee655fe 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -503,6 +503,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", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 714d80f983..59a3a4799b 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -586,4 +586,9 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Enable experimental, compact IRC style layout"), default: false, }, + "dontShowChatEffects": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Don't show chat effects"), + default: false, + }, }; From ecd4d6e19ef58f6c0b99a94890a5cd82a53e7c2a Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 18 Aug 2020 17:57:51 +0200 Subject: [PATCH 0002/1836] test commit for confetti changes --- src/SlashCommands.tsx | 13 ++ src/components/structures/RoomView.js | 7 +- src/components/views/elements/Confetti.js | 209 ++++++++++++++++++++++ src/i18n/strings/de_DE.json | 3 +- src/i18n/strings/en_EN.json | 1 + 5 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 src/components/views/elements/Confetti.js diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 2063ad3149..2d4d484899 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -44,6 +44,7 @@ import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; +import {func} from "prop-types"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -1026,6 +1027,18 @@ export const Commands = [ }, category: CommandCategories.actions, }), + new Command({ + command: "confetti", + description: _td("Throws confetti animation in the chat room"), + args: '/confetti + ', + runFn: function(roomId, args, command) { + return success((async () => { + const cli = MatrixClientPeg.get(); + await cli.sendHtmlMessage(roomId, args); + })()); + }, + category: CommandCategories.messages, + }), // Command definitions for autocompletion ONLY: // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9a61523941..85cb1df848 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -57,6 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; import {Action} from "../../dispatcher/actions"; import {SettingLevel} from "../../settings/SettingLevel"; +import Confetti from "../views/elements/Confetti"; const DEBUG = false; let debuglog = function() {}; @@ -67,7 +68,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = console.log.bind(console); } - +let confetti; export default createReactClass({ displayName: 'RoomView', propTypes: { @@ -624,12 +625,14 @@ export default createReactClass({ ev.preventDefault(); } }, - onAction: function(payload) { switch (payload.action) { case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); + confetti = new Confetti('100', '100'); + console.log('confetti sent'); + confetti.animateConfetti('test', 'message'); break; case 'post_sticker_message': this.injectSticker( diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js new file mode 100644 index 0000000000..e9dc2c34c0 --- /dev/null +++ b/src/components/views/elements/Confetti.js @@ -0,0 +1,209 @@ +import React from "react"; +import SettingsStore from "../../../../lib/settings/SettingsStore"; +import PropTypes from "prop-types"; + +export default class Confetti extends React.Component { + displayName: 'confetti'; + constructor(props) { + super(props); + this.animateConfetti = this.animateConfetti.bind(this); + this.confetti.start = this.startConfetti; + this.startConfetti = this.startConfetti.bind(this); + this.confetti.stop = this.stopConfetti; + this.confetti.remove = this.removeConfetti; + this.confetti.isRunning = this.isConfettiRunning; + } + static propTypes = { + width: PropTypes.string.isRequired, + height: PropTypes.string.isRequired, + } + confetti = { + //set max confetti count + maxCount: 150, + //set 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, + start: null, + }; + 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,"]; + streamingConfetti = false; + animationTimer = null; + lastFrameTime = Date.now(); + particles = []; + waveAngle = 0; + context = null; + supportsAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame; + + resetParticle(particle, width, height) { + particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")"); + particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.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; + } + + startConfetti(timeout) { + const width = window.innerWidth; + const height = window.innerHeight; + window.requestAnimationFrame = () => { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, this.confetti.frameInterval); + }; + }; + let canvas = document.getElementById("confetti-canvas"); + if (canvas === null) { + canvas = document.createElement("canvas"); + canvas.setAttribute("id", "confetti-canvas"); + canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0"); + document.body.prepend(canvas); + canvas.width = width; + canvas.height = height; + window.addEventListener("resize", function () { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }, true); + this.context = canvas.getContext("2d"); + } else if (this.context === null) { + this.context = canvas.getContext("2d"); + } + const count = this.confetti.maxCount; + while (this.particles.length < count) { + this.particles.push(this.resetParticle({}, width, height)); + } + this.streamingConfetti = true; + this.runAnimation(); + if (timeout) { + window.setTimeout(this.stopConfetti, timeout); + } + } + + stopConfetti() { + this.streamingConfetti = false; + } + + runAnimation() { + if (this.particles.length === 0) { + this.context.clearRect(0, 0, window.innerWidth, window.innerHeight); + this.animationTimer = null; + } else { + const now = Date.now(); + const delta = now - this.lastFrameTime; + if (!this.supportsAnimationFrame || delta > this.confetti.frameInterval) { + this.context.clearRect(0, 0, window.innerWidth, window.innerHeight); + this.updateParticles(); + this.drawParticles(this.context); + this.lastFrameTime = now - (delta % this.confetti.frameInterval); + } + this.animationTimer = requestAnimationFrame(this.runAnimation); + } + } + + removeConfetti() { + stop(); + this.particles = []; + } + + isConfettiRunning() { + return this.streamingConfetti; + } + + drawParticles(context) { + let particle; + let x; + let x2; + let y2; + for (let i = 0; i < this.particles.length; i++) { + particle = this.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; + context.strokeStyle = particle.color; + context.moveTo(x, particle.y); + context.lineTo(x2, y2); + context.stroke(); + } + } + + updateParticles() { + const width = window.innerWidth; + const height = window.innerHeight; + let particle; + this.waveAngle += 0.01; + for (let i = 0; i < this.particles.length; i++) { + particle = this.particles[i]; + if (!this.streamingConfetti && 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.confetti.speed) * 0.5; + particle.tilt = Math.sin(particle.tiltAngle) * 15; + } + if (particle.x > width + 20 || particle.x < -20 || particle.y > height) { + if (this.streamingConfetti && this.particles.length <= this.confetti.maxCount) { + this.resetParticle(particle, width, height); + } else { + this.particles.splice(i, 1); + i--; + } + } + } + } + + convertToHex(content) { + const contentBodyToHexArray = []; + let hex; + for (let i = 0; i < content.body.length; i++) { + hex = content.body.codePointAt(i).toString(16); + contentBodyToHexArray.push(hex); + } + return contentBodyToHexArray; + } + + isChatEffectsDisabled() { + console.log('return value', SettingsStore.getValue('dontShowChatEffects')); + return SettingsStore.getValue('dontShowChatEffects'); + } + + isConfettiEmoji(content) { + const hexArray = this.convertToHex(content); + return !!(hexArray.includes('1f389') || hexArray.includes('1f38a')); + } + + animateConfetti(userId, message) { + // const shortendUserId = userId.slice(1).split(":").slice(0, 1); + console.log('in animate confetti method'); + if (!this.isChatEffectsDisabled()) { + this.confetti.start(3000); + } + if (!message) { + return ('*' + userId + ' throws confetti '); + } + } + + render() { + return ( ); + } +} diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index edfe21d9d6..e4311c2111 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2362,5 +2362,6 @@ "Use your account to sign in to the latest version of the app at ": "Verwende dein Konto um dich an der neusten Version der App anzumelden", "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", - "Don't show chat effects": "Chat Effekte nicht zeigen" + "Don't show chat effects": "Chat Effekte nicht zeigen", + "Throws confetti animation in the chat room": "Throws confetti animation in the chat room" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 98aee655fe..f09ec685ee 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -213,6 +213,7 @@ "Thank you!": "Thank you!", "Opens chat with the given user": "Opens chat with the given user", "Sends a message to the given user": "Sends a message to the given user", + "Throws confetti animation in the chat room": "Throws confetti animation in the chat room", "Displays action": "Displays action", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", From 69227dd456bb9d7d78e16157dcabda2603345ae3 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 10:26:20 +0200 Subject: [PATCH 0003/1836] translations added --- src/i18n/strings/de_DE.json | 3 ++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index e4311c2111..5e5639942b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2363,5 +2363,6 @@ "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", "Don't show chat effects": "Chat Effekte nicht zeigen", - "Throws confetti animation in the chat room": "Throws confetti animation in the chat room" + "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti", + " sends confetti": " sendet Konfetti" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f09ec685ee..efd68d06a6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -213,7 +213,8 @@ "Thank you!": "Thank you!", "Opens chat with the given user": "Opens chat with the given user", "Sends a message to the given user": "Sends a message to the given user", - "Throws confetti animation in the chat room": "Throws confetti animation in the chat room", + "Sends the given message with confetti": "Sends the given message with confetti", + " sends confetti": " sends confetti", "Displays action": "Displays action", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", From 34cee20140d4da373fc0be630da4e11709409ed9 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 10:43:41 +0200 Subject: [PATCH 0004/1836] added confetti on command /confetti --- src/SlashCommands.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 2d4d484899..8322512b73 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -45,6 +45,7 @@ import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; import {func} from "prop-types"; +import SettingsStore from "./settings/SettingsStore"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -1029,15 +1030,24 @@ export const Commands = [ }), new Command({ command: "confetti", - description: _td("Throws confetti animation in the chat room"), - args: '/confetti + ', - runFn: function(roomId, args, command) { + description: _td("Sends the given message with confetti"), + args: '', + runFn: function(roomId, args) { return success((async () => { - const cli = MatrixClientPeg.get(); - await cli.sendHtmlMessage(roomId, args); + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); + const userName = userId.slice(1).split(":").slice(0, 1); + const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects'); + if (!args || isChatEffectsDisabled) { + args = '*' + userName + _td(' sends confetti'); + } + if (!isChatEffectsDisabled) { + dis.dispatch({action: 'confetti'}); + } + cli.sendHtmlMessage(roomId, args); })()); }, - category: CommandCategories.messages, + category: CommandCategories.actions, }), // Command definitions for autocompletion ONLY: From a7567b2e31bb403a05490a299e7ca17fd595760c Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 10:44:32 +0200 Subject: [PATCH 0005/1836] confetti animationsd handeled on roomViewTimeline --- src/components/structures/RoomView.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 85cb1df848..e48063530d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -57,7 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; import {Action} from "../../dispatcher/actions"; import {SettingLevel} from "../../settings/SettingLevel"; -import Confetti from "../views/elements/Confetti"; +import {animateConfetti, forceStopConfetti} from "../views/elements/Confetti"; const DEBUG = false; let debuglog = function() {}; @@ -68,7 +68,6 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = console.log.bind(console); } -let confetti; export default createReactClass({ displayName: 'RoomView', propTypes: { @@ -511,6 +510,7 @@ export default createReactClass({ 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); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -630,9 +630,9 @@ export default createReactClass({ case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); - confetti = new Confetti('100', '100'); - console.log('confetti sent'); - confetti.animateConfetti('test', 'message'); + break; + case 'confetti': + animateConfetti(this._roomView.current.offsetWidth); break; case 'post_sticker_message': this.injectSticker( @@ -750,6 +750,18 @@ export default createReactClass({ }); } } + if (!SettingsStore.getValue('dontShowChatEffects')) { + this.context.on('Event.decrypted', this.onEventDecrypted); + } + }, + onEventDecrypted(ev) { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; + this.handleConfetti(); + }, + handleConfetti() { + if (this.context.isInitialSyncComplete()) { + dis.dispatch({action: 'confetti'}); + } }, onRoomName: function(room) { @@ -786,6 +798,7 @@ export default createReactClass({ this._calculateRecommendedVersion(room); this._updateE2EStatus(room); this._updatePermissions(room); + forceStopConfetti(); }, _calculateRecommendedVersion: async function(room) { From 77de63bf4b06c5235e00c74673f2c0082a064195 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 10:44:49 +0200 Subject: [PATCH 0006/1836] confetti file added --- src/components/views/elements/Confetti.js | 252 +++++++++++----------- 1 file changed, 121 insertions(+), 131 deletions(-) diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js index e9dc2c34c0..df2b004ce0 100644 --- a/src/components/views/elements/Confetti.js +++ b/src/components/views/elements/Confetti.js @@ -1,52 +1,48 @@ -import React from "react"; -import SettingsStore from "../../../../lib/settings/SettingsStore"; -import PropTypes from "prop-types"; +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, +}; -export default class Confetti extends React.Component { - displayName: 'confetti'; - constructor(props) { - super(props); - this.animateConfetti = this.animateConfetti.bind(this); - this.confetti.start = this.startConfetti; - this.startConfetti = this.startConfetti.bind(this); - this.confetti.stop = this.stopConfetti; - this.confetti.remove = this.removeConfetti; - this.confetti.isRunning = this.isConfettiRunning; - } - static propTypes = { - width: PropTypes.string.isRequired, - height: PropTypes.string.isRequired, - } - confetti = { - //set max confetti count - maxCount: 150, - //set 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, - start: null, - }; - 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,"]; - streamingConfetti = false; - animationTimer = null; - lastFrameTime = Date.now(); - particles = []; - waveAngle = 0; - context = null; - supportsAnimationFrame = window.requestAnimationFrame || +(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; - resetParticle(particle, width, height) { - particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")"); - particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")"); + 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; @@ -56,154 +52,148 @@ export default class Confetti extends React.Component { return particle; } - startConfetti(timeout) { - const width = window.innerWidth; + 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); + } + animationTimer = requestAnimationFrame(runAnimation); + } + } + + function startConfetti(roomWidth, timeout) { + const width = roomWidth; const height = window.innerHeight; - window.requestAnimationFrame = () => { + window.requestAnimationFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, this.confetti.frameInterval); + function (callback) { + return window.setTimeout(callback, confetti.frameInterval); }; - }; + })(); let canvas = document.getElementById("confetti-canvas"); if (canvas === null) { canvas = document.createElement("canvas"); canvas.setAttribute("id", "confetti-canvas"); - canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0"); + canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0"); document.body.prepend(canvas); canvas.width = width; canvas.height = height; - window.addEventListener("resize", function () { - canvas.width = window.innerWidth; + window.addEventListener("resize", function() { + canvas.width = roomWidth; canvas.height = window.innerHeight; }, true); - this.context = canvas.getContext("2d"); - } else if (this.context === null) { - this.context = canvas.getContext("2d"); + context = canvas.getContext("2d"); + } else if (context === null) { + context = canvas.getContext("2d"); } - const count = this.confetti.maxCount; - while (this.particles.length < count) { - this.particles.push(this.resetParticle({}, width, height)); + const count = confetti.maxCount; + while (particles.length < count) { + particles.push(resetParticle({}, width, height)); } - this.streamingConfetti = true; - this.runAnimation(); + streamingConfetti = true; + runAnimation(); if (timeout) { - window.setTimeout(this.stopConfetti, timeout); + window.setTimeout(stopConfetti, timeout); } } - stopConfetti() { - this.streamingConfetti = false; + function stopConfetti() { + streamingConfetti = false; } - runAnimation() { - if (this.particles.length === 0) { - this.context.clearRect(0, 0, window.innerWidth, window.innerHeight); - this.animationTimer = null; - } else { - const now = Date.now(); - const delta = now - this.lastFrameTime; - if (!this.supportsAnimationFrame || delta > this.confetti.frameInterval) { - this.context.clearRect(0, 0, window.innerWidth, window.innerHeight); - this.updateParticles(); - this.drawParticles(this.context); - this.lastFrameTime = now - (delta % this.confetti.frameInterval); - } - this.animationTimer = requestAnimationFrame(this.runAnimation); - } - } - - removeConfetti() { + function removeConfetti() { stop(); - this.particles = []; + particles = []; } - isConfettiRunning() { - return this.streamingConfetti; + function isConfettiRunning() { + return streamingConfetti; } - drawParticles(context) { + function drawParticles(context) { let particle; - let x; - let x2; - let y2; - for (let i = 0; i < this.particles.length; i++) { - particle = this.particles[i]; + 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; - context.strokeStyle = particle.color; + 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(); } } - updateParticles() { + function updateParticles() { const width = window.innerWidth; const height = window.innerHeight; let particle; - this.waveAngle += 0.01; - for (let i = 0; i < this.particles.length; i++) { - particle = this.particles[i]; - if (!this.streamingConfetti && particle.y < -15) { + 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(this.waveAngle) - 0.5; - particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.confetti.speed) * 0.5; + 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 (this.streamingConfetti && this.particles.length <= this.confetti.maxCount) { - this.resetParticle(particle, width, height); + if (streamingConfetti && particles.length <= confetti.maxCount) { + resetParticle(particle, width, height); } else { - this.particles.splice(i, 1); + particles.splice(i, 1); i--; } } } } +})(); - convertToHex(content) { - const contentBodyToHexArray = []; - let hex; +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; - } - - isChatEffectsDisabled() { - console.log('return value', SettingsStore.getValue('dontShowChatEffects')); - return SettingsStore.getValue('dontShowChatEffects'); - } - - isConfettiEmoji(content) { - const hexArray = this.convertToHex(content); - return !!(hexArray.includes('1f389') || hexArray.includes('1f38a')); - } - - animateConfetti(userId, message) { - // const shortendUserId = userId.slice(1).split(":").slice(0, 1); - console.log('in animate confetti method'); - if (!this.isChatEffectsDisabled()) { - this.confetti.start(3000); - } - if (!message) { - return ('*' + userId + ' throws confetti '); - } - } - - render() { - return ( ); } + return contentBodyToHexArray; +} + +export function isConfettiEmoji(content) { + const hexArray = convertToHex(content); + return !!(hexArray.includes('1f389') || hexArray.includes('1f38a')); +} + +export function animateConfetti(roomWidth) { + confetti.start(roomWidth, 3000); +} +export function forceStopConfetti() { + console.log('confetti should stop'); + confetti.remove(); } From 03b2a529ef681e0e0777af57418f74ebba458954 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 11:29:46 +0200 Subject: [PATCH 0007/1836] remove unused var --- src/components/views/elements/Confetti.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js index df2b004ce0..371c26a4b5 100644 --- a/src/components/views/elements/Confetti.js +++ b/src/components/views/elements/Confetti.js @@ -34,7 +34,7 @@ const confetti = { "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 animationTimer = null; let lastFrameTime = Date.now(); let particles = []; let waveAngle = 0; @@ -55,7 +55,7 @@ const confetti = { function runAnimation() { if (particles.length === 0) { context.clearRect(0, 0, window.innerWidth, window.innerHeight); - animationTimer = null; + //animationTimer = null; } else { const now = Date.now(); const delta = now - lastFrameTime; @@ -65,7 +65,7 @@ const confetti = { drawParticles(context); lastFrameTime = now - (delta % confetti.frameInterval); } - animationTimer = requestAnimationFrame(runAnimation); + requestAnimationFrame(runAnimation); } } From 2a8b1e0ccd58628214a496b0b25f18ad96755997 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 11:55:23 +0200 Subject: [PATCH 0008/1836] remove space before function parentheses and maximum allowed line --- src/components/views/elements/Confetti.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js index 371c26a4b5..7d4faa3a17 100644 --- a/src/components/views/elements/Confetti.js +++ b/src/components/views/elements/Confetti.js @@ -72,13 +72,13 @@ const confetti = { function startConfetti(roomWidth, timeout) { const width = roomWidth; const height = window.innerHeight; - window.requestAnimationFrame = (function () { + window.requestAnimationFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || - function (callback) { + function(callback) { return window.setTimeout(callback, confetti.frameInterval); }; })(); @@ -86,7 +86,8 @@ const confetti = { if (canvas === null) { canvas = document.createElement("canvas"); canvas.setAttribute("id", "confetti-canvas"); - canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0"); + canvas.setAttribute("style", + "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0"); document.body.prepend(canvas); canvas.width = width; canvas.height = height; From b79cf1e7ad00cd06ae0b38c8b37612877ec59481 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 13:59:11 +0200 Subject: [PATCH 0009/1836] updated translated string --- src/SlashCommands.tsx | 2 +- src/i18n/strings/de_DE.json | 1 - src/i18n/strings/en_EN.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 8322512b73..ba0aea73f0 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1039,7 +1039,7 @@ export const Commands = [ const userName = userId.slice(1).split(":").slice(0, 1); const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects'); if (!args || isChatEffectsDisabled) { - args = '*' + userName + _td(' sends confetti'); + args = _t("* %(userName)s sends confetti", {userName}); } if (!isChatEffectsDisabled) { dis.dispatch({action: 'confetti'}); diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 5e5639942b..46ce139e6e 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2364,5 +2364,4 @@ "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", "Don't show chat effects": "Chat Effekte nicht zeigen", "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti", - " sends confetti": " sendet Konfetti" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index efd68d06a6..78a2d51c56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -214,7 +214,7 @@ "Opens chat with the given user": "Opens chat with the given user", "Sends a message to the given user": "Sends a message to the given user", "Sends the given message with confetti": "Sends the given message with confetti", - " sends confetti": " sends confetti", + "* %(userName)s sends confetti": "* %(userName)s sends confetti", "Displays action": "Displays action", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", From 5b7ccb5a7837e134d28795b7cb8ddc68716ca7c2 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 14:04:20 +0200 Subject: [PATCH 0010/1836] fix indentation spaces and readability line spaces --- src/components/structures/RoomView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e48063530d..240d300751 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -68,6 +68,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = console.log.bind(console); } + export default createReactClass({ displayName: 'RoomView', propTypes: { @@ -624,6 +625,7 @@ export default createReactClass({ ev.stopPropagation(); ev.preventDefault(); } + }, onAction: function(payload) { switch (payload.action) { @@ -632,7 +634,7 @@ export default createReactClass({ this._checkIfAlone(this.state.room); break; case 'confetti': - animateConfetti(this._roomView.current.offsetWidth); + animateConfetti(this._roomView.current.offsetWidth); break; case 'post_sticker_message': this.injectSticker( @@ -756,12 +758,12 @@ export default createReactClass({ }, onEventDecrypted(ev) { if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.handleConfetti(); + this.handleConfetti(); }, handleConfetti() { if (this.context.isInitialSyncComplete()) { - dis.dispatch({action: 'confetti'}); - } + dis.dispatch({action: 'confetti'}); + } }, onRoomName: function(room) { From f1c7139711f87dd818f9143fc6ec032e9ce41509 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Mon, 24 Aug 2020 14:25:06 +0200 Subject: [PATCH 0011/1836] remove not needed comma --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 46ce139e6e..b5a69d7e72 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2363,5 +2363,5 @@ "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", "Don't show chat effects": "Chat Effekte nicht zeigen", - "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti", + "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti" } From eef654e0e3189a8fcba03c8463d62ecbe2a3e745 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 11:09:10 +0200 Subject: [PATCH 0012/1836] fix indentation and remove console.log --- src/components/views/elements/Confetti.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js index 7d4faa3a17..b0f88dedb7 100644 --- a/src/components/views/elements/Confetti.js +++ b/src/components/views/elements/Confetti.js @@ -192,9 +192,8 @@ export function isConfettiEmoji(content) { } export function animateConfetti(roomWidth) { - confetti.start(roomWidth, 3000); + confetti.start(roomWidth, 3000); } export function forceStopConfetti() { - console.log('confetti should stop'); confetti.remove(); } From d41ffb1b4be9eeff5330c9e3ca5891cf22bb7f46 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 11:09:57 +0200 Subject: [PATCH 0013/1836] pass ev to handleConfetti in order to check content before dispatch --- src/components/structures/RoomView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 240d300751..b24d6efa2a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -57,7 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; import {Action} from "../../dispatcher/actions"; import {SettingLevel} from "../../settings/SettingLevel"; -import {animateConfetti, forceStopConfetti} from "../views/elements/Confetti"; +import {animateConfetti, forceStopConfetti, isConfettiEmoji} from "../views/elements/Confetti"; const DEBUG = false; let debuglog = function() {}; @@ -758,11 +758,13 @@ export default createReactClass({ }, onEventDecrypted(ev) { if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.handleConfetti(); + this.handleConfetti(ev); }, - handleConfetti() { + handleConfetti(ev) { if (this.context.isInitialSyncComplete()) { - dis.dispatch({action: 'confetti'}); + if (isConfettiEmoji(ev.getContent())) { + dis.dispatch({action: 'confetti'}); + } } }, From 43f266bfe333c2b6c5ece0be86ea5089e5e11c80 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 11:11:20 +0200 Subject: [PATCH 0014/1836] remove unused import fix if condition trying (pass the dispatcher to sendHtmlMessage) --- src/SlashCommands.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index ba0aea73f0..6b321ce092 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -44,7 +44,6 @@ import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; -import {func} from "prop-types"; import SettingsStore from "./settings/SettingsStore"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 @@ -1038,13 +1037,11 @@ export const Commands = [ const userId = cli.getUserId(); const userName = userId.slice(1).split(":").slice(0, 1); const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects'); - if (!args || isChatEffectsDisabled) { + if ((!args) || (!args && isChatEffectsDisabled)) { args = _t("* %(userName)s sends confetti", {userName}); } - if (!isChatEffectsDisabled) { - dis.dispatch({action: 'confetti'}); - } - cli.sendHtmlMessage(roomId, args); + cli.sendHtmlMessage(roomId, args, + dis.dispatch({action: 'confetti'})); })()); }, category: CommandCategories.actions, From cc71531493df1e5e095b7f173909cbb4606a4f16 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 13:36:04 +0200 Subject: [PATCH 0015/1836] reverted German language translations --- src/i18n/strings/de_DE.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 2cea1519df..3d5ba3722e 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2409,6 +2409,4 @@ "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Wenn du jetzt abbrichst, kannst du verschlüsselte Nachrichten und Daten verlieren, wenn du den Zugriff auf deine Logins verlierst.", "You can also set up Secure Backup & manage your keys in Settings.": "Du kannst auch in den Einstellungen eine Sicherung erstellen & deine Schlüssel verwalten.", "Set up Secure backup": "Sicheres Backup einrichten" - "Don't show chat effects": "Chat Effekte nicht zeigen", - "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti" } From 4527755f7e2df6f3b6622f1cd740469df608d587 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 16:18:01 +0200 Subject: [PATCH 0016/1836] updated translation --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 029551eb34..223e063762 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -210,7 +210,7 @@ "Opens chat with the given user": "Opens chat with the given user", "Sends a message to the given user": "Sends a message to the given user", "Sends the given message with confetti": "Sends the given message with confetti", - "* %(userName)s sends confetti": "* %(userName)s sends confetti", + "sends confetti": "sends confetti", "Displays action": "Displays action", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", From 5753c964317ab20b1682874416d00cdf9e6c5820 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Tue, 25 Aug 2020 16:39:57 +0200 Subject: [PATCH 0017/1836] a workaround to make ocnfetti work on recipient side. changed the implementation of on.Event.decrypted function --- src/SlashCommands.tsx | 13 +++++++------ src/components/structures/RoomView.js | 13 ++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index abd4f5449b..03aec46e46 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1029,15 +1029,16 @@ export const Commands = [ args: '', runFn: function(roomId, args) { return success((async () => { - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); - const userName = userId.slice(1).split(":").slice(0, 1); const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects'); if ((!args) || (!args && isChatEffectsDisabled)) { - args = _t("* %(userName)s sends confetti", {userName}); + args = _t("sends confetti"); + MatrixClientPeg.get().sendEmoteMessage(roomId, args); + } else { + MatrixClientPeg.get().sendHtmlMessage(roomId, args); + } + if (!isChatEffectsDisabled) { + dis.dispatch({action: 'confetti'}); } - cli.sendHtmlMessage(roomId, args, - dis.dispatch({action: 'confetti'})); })()); }, category: CommandCategories.actions, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index dfc92526c7..d5ccbf1c8c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -511,7 +511,6 @@ export default createReactClass({ 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); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -753,16 +752,16 @@ export default createReactClass({ } } if (!SettingsStore.getValue('dontShowChatEffects')) { - this.context.on('Event.decrypted', this.onEventDecrypted); + this.context.on("Event.decrypted", (ev) => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; + this.handleConfetti(ev); + }); } }, - onEventDecrypted(ev) { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.handleConfetti(ev); - }, handleConfetti(ev) { if (this.context.isInitialSyncComplete()) { - if (isConfettiEmoji(ev.getContent())) { + const messageBody = _t('sends confetti'); + if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) { dis.dispatch({action: 'confetti'}); } } From 95051a42b1f2755f52a980ef4521edc88ab728b0 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Wed, 26 Aug 2020 18:56:23 +0200 Subject: [PATCH 0018/1836] checking for unreadMessages before sending confetti throwing the confetti on the sender's side change sendHtmlMessage to sendTextMessage in slashCommands --- src/SlashCommands.tsx | 2 +- src/components/structures/RoomView.js | 17 ++++++++++------- .../views/rooms/SendMessageComposer.js | 7 +++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 03aec46e46..28eaa8123b 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1034,7 +1034,7 @@ export const Commands = [ args = _t("sends confetti"); MatrixClientPeg.get().sendEmoteMessage(roomId, args); } else { - MatrixClientPeg.get().sendHtmlMessage(roomId, args); + MatrixClientPeg.get().sendTextMessage(roomId, args); } if (!isChatEffectsDisabled) { dis.dispatch({action: 'confetti'}); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d5ccbf1c8c..92f43c75ca 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -189,6 +189,7 @@ export default createReactClass({ 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); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); @@ -511,6 +512,7 @@ export default createReactClass({ 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); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -751,15 +753,16 @@ export default createReactClass({ }); } } - if (!SettingsStore.getValue('dontShowChatEffects')) { - this.context.on("Event.decrypted", (ev) => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.handleConfetti(ev); - }); - } + }, + onEventDecrypted(ev) { + if (!SettingsStore.getValue('dontShowChatEffects')) { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || + this.state.room.getUnreadNotificationCount() === 0) return; + this.handleConfetti(ev); + } }, handleConfetti(ev) { - if (this.context.isInitialSyncComplete()) { + if (this.state.matrixClientIsReady) { const messageBody = _t('sends confetti'); if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) { dis.dispatch({action: 'confetti'}); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 6a7b2fc753..0b873a9bab 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -44,6 +44,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RateLimitedFunc from '../../../ratelimitedfunc'; import {Action} from "../../../dispatcher/actions"; +import {isConfettiEmoji} from "../elements/Confetti"; +import SettingsStore from "../../../settings/SettingsStore"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -313,6 +315,11 @@ export default class SendMessageComposer extends React.Component { }); } dis.dispatch({action: "message_sent"}); + if (!SettingsStore.getValue('dontShowChatEffects')) { + if (isConfettiEmoji(content)) { + dis.dispatch({action: 'confetti'}); + } + } } this.sendHistoryManager.save(this.model); From 1c2e05e925529afe71910fa43cc6257c15f6cd03 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 31 Aug 2020 14:40:16 -0400 Subject: [PATCH 0019/1836] initial version of device rehydration support --- src/CrossSigningManager.js | 2 +- src/Login.js | 60 +++++++++++++++++++++++++++++++++++++- src/MatrixClientPeg.ts | 28 ++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 676c41d7d7..43d089010c 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -40,7 +40,7 @@ export class AccessCancelledError extends Error { } } -async function confirmToDismiss() { +export async function confirmToDismiss() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), diff --git a/src/Login.js b/src/Login.js index 04805b4af9..4e46fc3665 100644 --- a/src/Login.js +++ b/src/Login.js @@ -18,7 +18,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import Modal from './Modal'; +import * as sdk from './index'; +import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; +import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { @@ -159,12 +164,18 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { + let rehydrationKeyInfo; + let rehydrationKey; + const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, + cryptoCallbacks: { + getDehydrationKey + } }); - const data = await client.login(loginType, loginParams); + const data = await client.loginWithRehydration(null, loginType, loginParams); const wellknown = data.well_known; if (wellknown) { @@ -185,5 +196,52 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, + rehydrationKeyInfo, + rehydrationKey, + olmAccount: data._olm_account, }; } + +async function getDehydrationKey(keyInfo) { + const inputToKey = async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; + const AccessSecretStorageDialog = + sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + // FIXME: + return true; + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + return key; +} diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index be16f5fe10..61b7a04069 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -42,6 +42,9 @@ export interface IMatrixClientCreds { accessToken: string; guest: boolean; pickleKey?: string; + rehydrationKey?: Uint8Array; + rehydrationKeyInfo?: {[props: string]: any}; + olmAccount?: any; } // TODO: Move this to the js-sdk @@ -248,12 +251,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { private createClient(creds: IMatrixClientCreds): void { // TODO: Make these opts typesafe with the js-sdk - const opts = { + const opts: any = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), @@ -268,6 +269,23 @@ class _MatrixClientPeg implements IMatrixClientPeg { cryptoCallbacks: {}, }; + if (creds.olmAccount) { + opts.deviceToImport = { + olmDevice: { + pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + sessions: [], + pickleKey: "DEFAULT_KEY", + }, + userId: creds.userId, + deviceId: creds.deviceId, + }; + } else { + opts.userId = creds.userId; + opts.deviceId = creds.deviceId; + } + + // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info + // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. @@ -275,6 +293,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { this.matrixClient = createMatrixClient(opts); + if (creds.rehydrationKey) { + this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + } + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); From 4398f1eb949c8f4b5b3e61c51c756bec3c9c97d8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 16:28:42 -0400 Subject: [PATCH 0020/1836] support setting up dehydration from blank account, SSO support, and other fixes --- src/CrossSigningManager.js | 26 +++++++++++ src/Lifecycle.js | 44 ++++++++++++++++++- src/Login.js | 18 +++++--- src/MatrixClientPeg.ts | 24 +++++++--- .../CreateSecretStorageDialog.js | 5 +++ 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 43d089010c..111fc26889 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,6 +30,16 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; let secretStorageKeys = {}; let secretStorageBeingAccessed = false; +let dehydrationInfo = {}; + +export function cacheDehydrationKey(key, keyInfo = {}) { + dehydrationInfo = {key, keyInfo}; +} + +export function getDehydrationKeyCache() { + return dehydrationInfo; +} + function isCachingAllowed() { return secretStorageBeingAccessed; } @@ -64,6 +74,22 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return [name, secretStorageKeys[name]]; } + // if we dehydrated a device, see if that key works for SSSS + if (dehydrationInfo.key) { + try { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + const key = dehydrationInfo.key; + // Save to cache to avoid future prompts in the current session + if (isCachingAllowed()) { + secretStorageKeys[name] = key; + } + dehydrationInfo = {}; + return [name, key]; + } + } catch {} + dehydrationInfo = {}; + } + const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( diff --git a/src/Lifecycle.js b/src/Lifecycle.js index d2de31eb80..9a84d4e1f4 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -42,6 +42,7 @@ import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; +import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -311,6 +312,25 @@ async function _restoreFromLocalStorage(opts) { console.log("No pickle key available"); } + const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info"); + const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON); + const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key"); + const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64); + const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account"); + let olmAccount; + if (rehydrationOlmPickle) { + olmAccount = new global.Olm.Account(); + try { + olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle); + } catch { + olmAccount.free(); + olmAccount = undefined; + } + } + sessionStorage.removeItem("mx_rehydration_key_info"); + sessionStorage.removeItem("mx_rehydration_key"); + sessionStorage.removeItem("mx_rehydration_account"); + console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ userId: userId, @@ -320,6 +340,9 @@ async function _restoreFromLocalStorage(opts) { identityServerUrl: isUrl, guest: isGuest, pickleKey: pickleKey, + rehydrationKey: rehydrationKey, + rehydrationKeyInfo: rehydrationKeyInfo, + olmAccount: olmAccount, }, false); return true; } else { @@ -463,7 +486,13 @@ async function _doSetLoggedIn(credentials, clearStorage) { if (localStorage) { try { - _persistCredentialsToLocalStorage(credentials); + // drop dehydration key and olm account before persisting. (Those + // get persisted for token login, but aren't needed at this point.) + const strippedCredentials = Object.assign({}, credentials); + delete strippedCredentials.rehydrationKeyInfo; + delete strippedCredentials.rehydrationKey; + delete strippedCredentials.olmAcconut; + _persistCredentialsToLocalStorage(strippedCredentials); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -528,6 +557,19 @@ function _persistCredentialsToLocalStorage(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } + // Temporarily save dehydration information if it's provided. This is + // needed for token logins, because the page reloads after the login, so we + // can't keep it in memory. + if (credentials.rehydrationKeyInfo) { + sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo)); + } + if (credentials.rehydrationKey) { + sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey)); + } + if (credentials.olmAccount) { + sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY")); + } + console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.js b/src/Login.js index 4e46fc3665..0563952c5d 100644 --- a/src/Login.js +++ b/src/Login.js @@ -20,7 +20,12 @@ limitations under the License. import Modal from './Modal'; import * as sdk from './index'; -import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; +import { + AccessCancelledError, + cacheDehydrationKey, + confirmToDismiss, + getDehydrationKeyCache, +} from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -164,9 +169,6 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { - let rehydrationKeyInfo; - let rehydrationKey; - const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, @@ -190,14 +192,16 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { } } + const dehydrationKeyCache = getDehydrationKeyCache(); + return { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, - rehydrationKeyInfo, - rehydrationKey, + rehydrationKeyInfo: dehydrationKeyCache.keyInfo, + rehydrationKey: dehydrationKeyCache.key, olmAccount: data._olm_account, }; } @@ -243,5 +247,7 @@ async function getDehydrationKey(keyInfo) { throw new AccessCancelledError(); } const key = await inputToKey(input); + // need to copy the key because rehydration (unpickling) will clobber it + cacheDehydrationKey(new Uint8Array(key), keyInfo); return key; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 61b7a04069..18af378fac 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { crossSigningCallbacks } from './CrossSigningManager'; +import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { @@ -270,33 +270,43 @@ class _MatrixClientPeg implements IMatrixClientPeg { }; if (creds.olmAccount) { + console.log("got a dehydrated account"); opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), sessions: [], - pickleKey: "DEFAULT_KEY", + pickleKey: creds.pickleKey || "DEFAULT_KEY", }, userId: creds.userId, deviceId: creds.deviceId, }; + creds.olmAccount.free(); } else { opts.userId = creds.userId; opts.deviceId = creds.deviceId; } - // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info - // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - this.matrixClient = createMatrixClient(opts); + // set dehydration key after cross-signing gets set up -- we wait until + // cross-signing is set up because we want to cross-sign the dehydrated + // key + const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey + opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { + const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); + this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase}); + return [name, key]; + } if (creds.rehydrationKey) { - this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } + this.matrixClient = createMatrixClient(opts); + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 53b3033330..b1c9dc5a60 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -304,6 +304,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } + const dehydrationKeyInfo = + this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase + ? {passphrase: this._recoveryKey.keyInfo.passphrase} + : {}; + await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo); this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { From db61d343f5f9f2dc8552b97d9243c0ebfe8baa94 Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 30 Aug 2020 20:17:08 +1200 Subject: [PATCH 0021/1836] Add option to send/edit a message with Ctrl + Enter / Command + Enter When editing multi-line text this option helps to prevent accidentally sending a message too early. With this option, Enter just inserts a new line. For example, composing programming code in a dev chat becomes much easier when Enter just inserts a new line instead of sending the message. Signed-off-by: Clemens Zeidler --- src/components/views/rooms/EditMessageComposer.js | 8 ++++++-- src/components/views/rooms/SendMessageComposer.js | 8 ++++++-- .../settings/tabs/user/PreferencesUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.ts | 5 +++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 78c7de887d..636c5b27ff 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -29,9 +29,10 @@ import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; import {EventStatus} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; -import {Key} from "../../../Keyboard"; +import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import SettingsStore from "../../../settings/SettingsStore"; function _isReply(mxEvent) { const relatesTo = mxEvent.getContent()["m.relates_to"]; @@ -135,7 +136,10 @@ export default class EditMessageComposer extends React.Component { if (event.metaKey || event.altKey || event.shiftKey) { return; } - if (event.key === Key.ENTER) { + const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); + const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) + : event.key === Key.ENTER; + if (send) { this._sendEdit(); event.preventDefault(); } else if (event.key === Key.ESCAPE) { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 25dcf8ccd5..dd1b67c989 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -39,11 +39,12 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; -import {Key} from "../../../Keyboard"; +import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RateLimitedFunc from '../../../ratelimitedfunc'; import {Action} from "../../../dispatcher/actions"; +import SettingsStore from "../../../settings/SettingsStore"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -122,7 +123,10 @@ export default class SendMessageComposer extends React.Component { return; } const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; - if (event.key === Key.ENTER && !hasModifier) { + const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); + const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) + : event.key === Key.ENTER && !hasModifier; + if (send) { this._sendMessage(); event.preventDefault(); } else if (event.key === Key.ARROW_UP) { diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index a77815a68c..64208cb8cd 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -33,6 +33,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', + 'MessageComposerInput.ctrlEnterToSend', ]; static TIMELINE_SETTINGS = [ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 47063bdae4..277d9c5952 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -477,6 +477,7 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", + "Use Ctrl + Enter to send a message (Mac: Command + Enter)": "Use Ctrl + Enter to send a message (Mac: Command + Enter)", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 95861e11df..d2d268b2bb 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -321,6 +321,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Show typing notifications"), default: true, }, + "MessageComposerInput.ctrlEnterToSend": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Use Ctrl + Enter to send a message (Mac: Command + Enter)"), + default: false, + }, "MessageComposerInput.autoReplaceEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Automatically replace plain text Emoji'), From 9031c58aebd08b7c6ab07e173f9895006491af5c Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Tue, 8 Sep 2020 21:46:09 +1200 Subject: [PATCH 0022/1836] Make settings label platform specific --- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 277d9c5952..a66478ddc9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -477,7 +477,8 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", - "Use Ctrl + Enter to send a message (Mac: Command + Enter)": "Use Ctrl + Enter to send a message (Mac: Command + Enter)", + "Use Command + Enter to send a message": "Use Command + Enter to send a message", + "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d2d268b2bb..afe9a50c1e 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -32,6 +32,7 @@ import UseSystemFontController from './controllers/UseSystemFontController'; import { SettingLevel } from "./SettingLevel"; import SettingController from "./controllers/SettingController"; import { RightPanelPhases } from "../stores/RightPanelStorePhases"; +import { isMac } from '../Keyboard'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -323,7 +324,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "MessageComposerInput.ctrlEnterToSend": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td("Use Ctrl + Enter to send a message (Mac: Command + Enter)"), + displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"), default: false, }, "MessageComposerInput.autoReplaceEmoji": { From b1b7215532596acac7fe17fd83f65ca317dd8e7d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 18:08:17 -0400 Subject: [PATCH 0023/1836] fix lint and merge issues --- src/Login.js | 6 +++--- src/SecurityManager.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Login.js b/src/Login.js index 0563952c5d..c04b086afa 100644 --- a/src/Login.js +++ b/src/Login.js @@ -25,7 +25,7 @@ import { cacheDehydrationKey, confirmToDismiss, getDehydrationKeyCache, -} from "./CrossSigningManager"; +} from "./SecurityManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -173,8 +173,8 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { baseUrl: hsUrl, idBaseUrl: isUrl, cryptoCallbacks: { - getDehydrationKey - } + getDehydrationKey, + }, }); const data = await client.loginWithRehydration(null, loginType, loginParams); diff --git a/src/SecurityManager.js b/src/SecurityManager.js index e8bd63d2ff..967c0cc266 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -91,7 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { // if we dehydrated a device, see if that key works for SSSS if (dehydrationInfo.key) { try { - if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) { const key = dehydrationInfo.key; // Save to cache to avoid future prompts in the current session if (isCachingAllowed()) { From 4e2397a79db8242f6ff04f9b5c5e61693d21533c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 20:53:39 -0400 Subject: [PATCH 0024/1836] doc fixes and minor code improvement --- src/MatrixClientPeg.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index a5fa0fb3cf..84bc610896 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -271,11 +271,12 @@ class _MatrixClientPeg implements IMatrixClientPeg { if (creds.olmAccount) { console.log("got a dehydrated account"); + const pickleKey = creds.pickleKey || "DEFAULT_KEY"; opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(pickleKey), sessions: [], - pickleKey: creds.pickleKey || "DEFAULT_KEY", + pickleKey: pickleKey, }, userId: creds.userId, deviceId: creds.deviceId, @@ -293,7 +294,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { // set dehydration key after cross-signing gets set up -- we wait until // cross-signing is set up because we want to cross-sign the dehydrated - // key + // device const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); @@ -302,6 +303,8 @@ class _MatrixClientPeg implements IMatrixClientPeg { } if (creds.rehydrationKey) { + // cache the key so that the SSSS prompt tries using it without + // prompting the user cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } From 0604c86779cdea98ace30bdd78eb4db6888ffc40 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sat, 19 Sep 2020 15:30:00 +0100 Subject: [PATCH 0025/1836] added katex package and import --- package.json | 1 + src/HtmlUtils.tsx | 1 + yarn.lock | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/package.json b/package.json index 156cbb1bc8..7aa3df136b 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "highlight.js": "^10.1.2", "html-entities": "^1.3.1", "is-ip": "^2.0.0", + "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index bd314c2e5f..99acbfcb0c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -26,6 +26,7 @@ import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; +import katex from 'katex'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; diff --git a/yarn.lock b/yarn.lock index efc1f0eae1..34b99708fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5607,6 +5607,13 @@ jsx-ast-utils@^2.4.1: array-includes "^3.1.1" object.assign "^4.1.0" +katex@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9" + integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg== + dependencies: + commander "^2.19.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" From becc79d67a29a0886f4a6f800daabebae16d655c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 12:59:22 +0100 Subject: [PATCH 0026/1836] send tex math as data-mx-maths attribute --- src/HtmlUtils.tsx | 26 +++++++++++++++++++++++++- src/editor/serialize.ts | 23 ++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 99acbfcb0c..344fb3514c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -27,6 +27,7 @@ import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; +import { AllHtmlEntities } from 'html-entities'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -236,7 +237,8 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = { allowedAttributes: { // custom ones first: font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix - span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix + span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix + div: ['data-mx-maths'], a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix img: ['src', 'width', 'height', 'alt', 'title'], ol: ['start'], @@ -409,6 +411,27 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); + if (true) { // TODO: add katex setting + const mathDelimiters = [ + { left: "
.*?
", display: true }, + { left: ".*?", display: false } + ]; + + mathDelimiters.forEach(function (d) { + var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + + safeBody = safeBody.replace(reg, function(match, p1) { + return katex.renderToString( + AllHtmlEntities.decode(p1), + { + throwOnError: false, + displayMode: d.display, + output: "mathml" + }) + }); + }); + } + } } finally { delete sanitizeParams.textFilter; @@ -450,6 +473,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts 'markdown-body': isHtmlMessage && !emojiBody, }); + return isDisplayedWithHtml ? { @@ -38,7 +39,27 @@ export function mdSerialize(model: EditorModel) { } export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { - const md = mdSerialize(model); + var md = mdSerialize(model); + + if (true) { // TODO: add katex setting + const mathDelimiters = [ // TODO: make customizable + { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, + { left: "\\$\\$", right: "\\$\\$", display: false } + ]; + + mathDelimiters.forEach(function (d) { + var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + md = md.replace(reg, function(match, p1) { + const p1e = AllHtmlEntities.encode(p1); + if (d.display == true) { + return `
${p1e}
`; + } else { + return `${p1e}`; + } + }); + }); + } + const parser = new Markdown(md); if (!parser.isPlainText() || forceHTML) { return parser.toHTML(); From e78734bbf6b2fbf1ebee530921998ff97c56f203 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 14:20:35 +0100 Subject: [PATCH 0027/1836] Deserialize back to math delimiters for editing --- src/HtmlUtils.tsx | 4 +++- src/editor/deserialize.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 344fb3514c..46bc7b441c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -534,7 +534,6 @@ export function checkBlockNode(node: Node) { case "H6": case "PRE": case "BLOCKQUOTE": - case "DIV": case "P": case "UL": case "OL": @@ -547,6 +546,9 @@ export function checkBlockNode(node: Node) { case "TH": case "TD": return true; + case "DIV": + // don't treat math nodes as block nodes for deserializing + return !(node as HTMLElement).hasAttribute("data-mx-maths"); default: return false; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index ec697b193c..edaa330e50 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -130,6 +130,18 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl } break; } + case "DIV": + case "SPAN": { + // math nodes are translated back into delimited latex strings + if (n.hasAttribute("data-mx-maths")) { + const delim = (n.nodeName == "SPAN") ? "$$" : "$$$"; + const tex = n.getAttribute("data-mx-maths"); + return partCreator.plain(delim + tex + delim); + } else if (!checkDescendInto(n)) { + return partCreator.plain(n.textContent); + } + break; + } case "OL": state.listIndex.push((n).start || 1); /* falls through */ From 428a6b94ff5c34533b8684e5ae8b019a4dbec07c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 15:07:12 +0100 Subject: [PATCH 0028/1836] math off by default, enable with latex_maths flag --- src/HtmlUtils.tsx | 4 +++- src/editor/serialize.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 46bc7b441c..047a891847 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,6 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; +import SdkConfig from './SdkConfig'; import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; @@ -50,6 +51,7 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); // Regex pattern for whitespace characters const WHITESPACE_REGEX = new RegExp("\\s", "g"); + const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; @@ -411,7 +413,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - if (true) { // TODO: add katex setting + if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ { left: "
.*?
", display: true }, { left: ".*?", display: false } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 8ec590cba5..72a551a4a3 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -19,6 +19,7 @@ import Markdown from '../Markdown'; import {makeGenericPermalink} from "../utils/permalinks/Permalinks"; import EditorModel from "./model"; import { AllHtmlEntities } from 'html-entities'; +import SdkConfig from '../SdkConfig'; export function mdSerialize(model: EditorModel) { return model.parts.reduce((html, part) => { @@ -41,7 +42,7 @@ export function mdSerialize(model: EditorModel) { export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) { var md = mdSerialize(model); - if (true) { // TODO: add katex setting + if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ // TODO: make customizable { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, { left: "\\$\\$", right: "\\$\\$", display: false } From e4448ae1ad87cbd3e47c73a589012494ec7d4189 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 16:52:29 +0100 Subject: [PATCH 0029/1836] send fallback in pre tags, not code --- src/editor/serialize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 72a551a4a3..c0d9509ffa 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { - return `
${p1e}
`; + return `
${p1e}
`; } else { - return `${p1e}`; + return `
${p1e}
`; } }); }); From 7e6d7053e0a6c55f082153a521de079c7db2d77c Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 17:02:27 +0100 Subject: [PATCH 0030/1836] Revert "send fallback in pre tags, not code" (code looks better) This reverts commit e4448ae1ad87cbd3e47c73a589012494ec7d4189. --- src/editor/serialize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index c0d9509ffa..72a551a4a3 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { - return `
${p1e}
`; + return `
${p1e}
`; } else { - return `
${p1e}
`; + return `${p1e}`; } }); }); From 1f24b5b90c9fe6a743db17d14b726e1aefd15f6f Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Sun, 20 Sep 2020 17:48:42 +0100 Subject: [PATCH 0031/1836] made math display slightly larger --- res/css/structures/_RoomView.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 572c7166d2..571c34fcb0 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -205,6 +205,10 @@ limitations under the License. clear: both; } +.mx_RoomView_MessageList .katex { + font-size: 1.3em; +} + li.mx_RoomView_myReadMarker_container { height: 0px; margin: 0px; From 24a1834f9b37993b79ec92c1c3081d6aa7777d37 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 21 Sep 2020 09:00:24 +0100 Subject: [PATCH 0032/1836] support multi-line and escaped $ --- src/HtmlUtils.tsx | 6 +++--- src/editor/serialize.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 047a891847..569b1662fe 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -415,12 +415,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts safeBody = sanitizeHtml(formattedBody, sanitizeParams); if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ - { left: "
.*?
", display: true }, - { left: ".*?", display: false } + { pattern: "
(.|\\s)*?
", display: true }, + { pattern: "(.|\\s)*?", display: false } ]; mathDelimiters.forEach(function (d) { - var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + var reg = RegExp(d.pattern, "gm"); safeBody = safeBody.replace(reg, function(match, p1) { return katex.renderToString( diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 72a551a4a3..d0a28266eb 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -44,12 +44,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = if (SdkConfig.get()['latex_maths']) { const mathDelimiters = [ // TODO: make customizable - { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true }, - { left: "\\$\\$", right: "\\$\\$", display: false } + { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true }, + { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false } ]; mathDelimiters.forEach(function (d) { - var reg = RegExp(d.left + "(.*?)" + d.right, "g"); + var reg = RegExp(d.pattern, "gm"); md = md.replace(reg, function(match, p1) { const p1e = AllHtmlEntities.encode(p1); if (d.display == true) { From 4df8754aad0333c840eceb1892faa9f3c90f2405 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 21 Sep 2020 11:00:39 +0100 Subject: [PATCH 0033/1836] allow custom latex delimiters in config.json --- src/editor/deserialize.ts | 10 ++++++++-- src/editor/serialize.ts | 26 ++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index edaa330e50..e27eecd2db 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom"; import { checkBlockNode } from "../HtmlUtils"; import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks"; import { PartCreator } from "./parts"; +import SdkConfig from "../SdkConfig"; function parseAtRoomMentions(text: string, partCreator: PartCreator) { const ATROOM = "@room"; @@ -134,9 +135,14 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl case "SPAN": { // math nodes are translated back into delimited latex strings if (n.hasAttribute("data-mx-maths")) { - const delim = (n.nodeName == "SPAN") ? "$$" : "$$$"; + const delimLeft = (n.nodeName == "SPAN") ? + (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$"; + const delimRight = (n.nodeName == "SPAN") ? + (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" : + (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$"; const tex = n.getAttribute("data-mx-maths"); - return partCreator.plain(delim + tex + delim); + return partCreator.plain(delimLeft + tex + delimRight); } else if (!checkDescendInto(n)) { return partCreator.plain(n.textContent); } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index d0a28266eb..da8ae4e820 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -43,21 +43,19 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = var md = mdSerialize(model); if (SdkConfig.get()['latex_maths']) { - const mathDelimiters = [ // TODO: make customizable - { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true }, - { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false } - ]; + const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] || + "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$"; + const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] || + "\\$\\$(([^$]|\\\\\\$)*)\\$\\$"; - mathDelimiters.forEach(function (d) { - var reg = RegExp(d.pattern, "gm"); - md = md.replace(reg, function(match, p1) { - const p1e = AllHtmlEntities.encode(p1); - if (d.display == true) { - return `
${p1e}
`; - } else { - return `${p1e}`; - } - }); + md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) { + const p1e = AllHtmlEntities.encode(p1); + return `
${p1e}
`; + }); + + md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) { + const p1e = AllHtmlEntities.encode(p1); + return `${p1e}`; }); } From 342f1d5b438710daafc0ee0f4c1a94af8dcbf9ee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Sep 2020 14:36:16 -0600 Subject: [PATCH 0034/1836] Extremely bad support for "temporary widgets" --- src/FromWidgetPostMessageApi.js | 10 +- src/Modal.tsx | 2 +- src/WidgetMessaging.js | 30 ++++ .../views/dialogs/TempWidgetDialog.tsx | 155 ++++++++++++++++++ src/i18n/strings/en_EN.json | 4 + src/stores/TempWidgetStore.ts | 54 ++++++ src/widgets/WidgetApi.ts | 38 ++++- 7 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 src/components/views/dialogs/TempWidgetDialog.tsx create mode 100644 src/stores/TempWidgetStore.ts diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index d5d7c08d50..c5a25468a8 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,8 +24,10 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; -import {Capability} from "./widgets/WidgetApi"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; import {objectClone} from "./utils/objects"; +import {Action} from "./dispatcher/actions"; +import {TempWidgetStore} from "./stores/TempWidgetStore"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -218,8 +220,12 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } - } else if (action === 'get_openid') { + } else if (action === 'get_openid' + || action === KnownWidgetActions.CloseWidget) { // Handled by caller + } else if (action === KnownWidgetActions.OpenTempWidget) { + TempWidgetStore.instance.openTempWidget(event.data.data, widgetId); + this.sendResponse(event, {}); // ack } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/Modal.tsx b/src/Modal.tsx index 0a36813961..3d95bc1a2b 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -interface IModal { +export interface IModal { elem: React.ReactNode; className?: string; beforeClosePromise?: Promise; diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c68e926ac1..6a2eeb852c 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -147,6 +147,36 @@ export default class WidgetMessaging { }); } + sendThemeInfo(themeInfo: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.UpdateThemeInfo, + data: themeInfo, + }).catch((error) => { + console.error("Failed to send theme info: ", error); + }); + } + + sendWidgetConfig(widgetConfig: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.SendWidgetConfig, + data: widgetConfig, + }).catch((error) => { + console.error("Failed to send widget info: ", error); + }); + } + + sendTempCloseInfo(info: any) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClosedWidgetResponse, + data: info, + }).catch((error) => { + console.error("Failed to send temp widget close info: ", error); + }); + } + start() { this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl); this.fromWidget.addListener("get_openid", this._onOpenIdRequest); diff --git a/src/components/views/dialogs/TempWidgetDialog.tsx b/src/components/views/dialogs/TempWidgetDialog.tsx new file mode 100644 index 0000000000..1fd3b26b5c --- /dev/null +++ b/src/components/views/dialogs/TempWidgetDialog.tsx @@ -0,0 +1,155 @@ +/* +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 * as React from 'react'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import WidgetMessaging from "../../../WidgetMessaging"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import Field from "../elements/Field"; +import { KnownWidgetActions } from "../../../widgets/WidgetApi"; +import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; + +interface IState { + messaging?: WidgetMessaging; + + androidMode: boolean; + darkTheme: boolean; + accentColor: string; +} + +interface IProps extends IDialogProps { + widgetDefinition: {url: string, data: any}; + sourceWidgetId: string; +} + +// TODO: Make a better dialog + +export default class TempWidgetDialog extends React.PureComponent { + private appFrame: React.RefObject = React.createRef(); + + constructor(props) { + super(props); + this.state = { + androidMode: false, + darkTheme: false, + accentColor: "#03b381", + }; + } + + public componentDidMount() { + // TODO: Don't violate every principle of widget creation + const messaging = new WidgetMessaging( + "TEMP_ID", + this.props.widgetDefinition.url, + this.props.widgetDefinition.url, + false, + this.appFrame.current.contentWindow, + ); + this.setState({messaging}); + } + + public componentWillUnmount() { + this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseWidget, this.onWidgetClose); + this.state.messaging.stop(); + } + + private onLoad = () => { + this.state.messaging.getCapabilities().then(caps => { + console.log("Requested capabilities: ", caps); + this.sendTheme(); + this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data); + }); + this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseWidget, this.onWidgetClose); + }; + + private sendTheme() { + if (!this.state.messaging) return; + this.state.messaging.sendThemeInfo({ + clientName: this.state.androidMode ? "element-android" : "element-web", + isDark: this.state.darkTheme, + accentColor: this.state.accentColor, + }); + } + + public static sendExitData(sourceWidgetId: string, success: boolean, data?: any) { + const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId); + if (!sourceMessaging) { + console.error("No source widget messaging for temp widget"); + return; + } + sourceMessaging.sendTempCloseInfo({success, ...data}); + } + + private onWidgetClose = (req) => { + this.props.onFinished(true); + TempWidgetDialog.sendExitData(this.props.sourceWidgetId, true, req.data); + } + + private onClientToggleChanged = (androidMode) => { + this.setState({androidMode}, () => this.sendTheme()); + }; + + private onDarkThemeChanged = (darkTheme) => { + this.setState({darkTheme}, () => this.sendTheme()); + }; + + private onAccentColorChanged = (ev) => { + this.setState({accentColor: ev.target.value}, () => this.sendTheme()); + }; + + public render() { + // TODO: Don't violate every single security principle + + const widgetUrl = this.props.widgetDefinition.url + + "?widgetId=TEMP_ID&parentUrl=" + encodeURIComponent(window.location.href); + + return +
+ + + +
+
+ ; + return