From 5771406dd7d0c691082515461b0fa5d8d28594d2 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 18 Mar 2019 23:21:21 +0000
Subject: [PATCH 01/28] Add View Servers in Room to Devtools
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
res/css/views/dialogs/_DevtoolsDialog.scss | 6 ++-
.../views/dialogs/DevtoolsDialog.js | 44 +++++++++++++++++++
src/i18n/strings/en_EN.json | 1 +
3 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index 815e8408b5..f38191445c 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -18,7 +18,11 @@ limitations under the License.
margin: 10px 0;
}
-.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
+.mx_DevTools_ServersInRoomList_button {
+ cursor: default !important;
+}
+
+.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button, .mx_DevTools_RoomStateExplorer_query {
margin-bottom: 10px;
width: 100%;
}
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 3db516a74d..f7ddb542ae 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -551,11 +551,55 @@ class AccountDataExplorer extends DevtoolsComponent {
}
}
+class ServersInRoomList extends DevtoolsComponent {
+ static getLabel() { return _t('View Servers in Room'); }
+
+ static propTypes = {
+ onBack: PropTypes.func.isRequired,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.onQuery = this.onQuery.bind(this);
+
+ const room = MatrixClientPeg.get().getRoom(this.context.roomId);
+ const servers = new Set();
+ room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
+ this.servers = Array.from(servers).map(s =>
+ );
+
+ this.state = {
+ query: '',
+ };
+ }
+
+ onQuery(query) {
+ this.setState({ query });
+ }
+
+ render() {
+ return
+
+
+ { this.servers }
+
+
+
+
+
+
;
+ }
+}
+
const Entries = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
AccountDataExplorer,
+ ServersInRoomList,
];
export default class DevtoolsDialog extends React.Component {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e23be021e8..60fd373306 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1095,6 +1095,7 @@
"Filter results": "Filter results",
"Explore Room State": "Explore Room State",
"Explore Account Data": "Explore Account Data",
+ "View Servers in Room": "View Servers in Room",
"Toolbox": "Toolbox",
"Developer Tools": "Developer Tools",
"An error has occurred.": "An error has occurred.",
From c636f890b5a6c97c872cec3583620e93395d4c31 Mon Sep 17 00:00:00 2001
From: Travis Ralston
Date: Mon, 6 May 2019 09:55:27 -0600
Subject: [PATCH 02/28] Add configuration flag to disable minimum password
requirements
The configuration flag is intentionally long and annoying - the vast majority of people should not need this. The flag is intended to be used in development environments where accounts are often registered with no intention of them sticking around.
---
src/components/views/auth/RegistrationForm.js | 16 ++++++++++++++--
src/i18n/strings/en_EN.json | 1 +
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 6e55581af0..eb77e125af 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -76,6 +76,7 @@ module.exports = React.createClass({
password: "",
passwordConfirm: "",
passwordComplexity: null,
+ passwordUnsafe: false,
};
},
@@ -270,12 +271,23 @@ module.exports = React.createClass({
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
+ const unsafe = complexity.score < PASSWORD_MIN_SCORE;
+ const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
+ passwordUnsafe: unsafe,
});
- return complexity.score >= PASSWORD_MIN_SCORE;
+ return allowUnsafe || !unsafe;
+ },
+ valid: function() {
+ // Unsafe passwords that are valid are only possible through a
+ // configuration flag. We'll print some helper text to signal
+ // to the user that their password is allowed, but unsafe.
+ if (this.state.passwordUnsafe) {
+ return _t("Password is allowed, but unsafe");
+ }
+ return _t("Nice, strong password!");
},
- valid: () => _t("Nice, strong password!"),
invalid: function() {
const complexity = this.state.passwordComplexity;
if (!complexity) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index eaea057b36..273ca8a571 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1327,6 +1327,7 @@
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
"Enter password": "Enter password",
+ "Password is allowed, but unsafe": "Password is allowed, but unsafe",
"Nice, strong password!": "Nice, strong password!",
"Keep going...": "Keep going...",
"Passwords don't match": "Passwords don't match",
From 92a932158df32e8360c649df1c5280390652595b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 9 May 2019 22:12:21 +0100
Subject: [PATCH 03/28] Command to change avatar for a single room, including
upload of mxc res
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/SlashCommands.js | 43 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index f72ba1e005..8757f5108a 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -30,6 +31,7 @@ import MultiInviter from './utils/MultiInviter';
import { linkifyAndSanitizeHtml } from './HtmlUtils';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import WidgetUtils from "./utils/WidgetUtils";
+import Promise from "bluebird";
class Command {
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
@@ -208,6 +210,47 @@ export const CommandMap = {
},
}),
+ roomavatar: new Command({
+ name: 'roomavatar',
+ args: '',
+ description: _td('Changes your avatar in this current room only'),
+ runFn: function(roomId, args) {
+ const cli = MatrixClientPeg.get();
+ const room = cli.getRoom(roomId);
+ const userId = cli.getUserId();
+
+ let promise = Promise.resolve(args);
+ if (!args) {
+ promise = new Promise((resolve) => {
+ const fileSelector = document.createElement('input');
+ fileSelector.setAttribute('type', 'file');
+ fileSelector.onchange = (ev) => {
+ const file = ev.target.files[0];
+
+ const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
+ Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
+ file,
+ onFinished: (shouldContinue) => {
+ if (shouldContinue) resolve(cli.uploadContent(file));
+ },
+ });
+ };
+
+ fileSelector.click();
+ });
+ }
+
+ return success(promise.then((url) => {
+ const ev = room.currentState.getStateEvents('m.room.member', userId);
+ const content = {
+ ...ev ? ev.getContent() : { membership: 'join' },
+ avatar_url: url,
+ };
+ return cli.sendStateEvent(roomId, 'm.room.member', content, userId);
+ }));
+ },
+ }),
+
tint: new Command({
name: 'tint',
args: ' []',
From cd5a460b2b257029ee0e1ff8d1544ea3a88fa317 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 9 May 2019 22:30:45 +0100
Subject: [PATCH 04/28] Iterate PR based on feedback
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
res/css/views/dialogs/_DevtoolsDialog.scss | 1 +
src/components/views/dialogs/DevtoolsDialog.js | 4 +---
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index f38191445c..2f01f3ecc6 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -19,6 +19,7 @@ limitations under the License.
}
.mx_DevTools_ServersInRoomList_button {
+ /* Set the cursor back to default as `.mx_Dialog button` sets it to pointer */
cursor: default !important;
}
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index f7ddb542ae..0835c41bb9 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -561,8 +561,6 @@ class ServersInRoomList extends DevtoolsComponent {
constructor(props, context) {
super(props, context);
- this.onQuery = this.onQuery.bind(this);
-
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
const servers = new Set();
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
@@ -576,7 +574,7 @@ class ServersInRoomList extends DevtoolsComponent {
};
}
- onQuery(query) {
+ onQuery = (query) => {
this.setState({ query });
}
From 3beb70814c8b699f3d3d81da8dd36015bd635d14 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Tue, 7 May 2019 15:40:02 +0100
Subject: [PATCH 05/28] Fix indentation in TimelinePanel
---
src/components/structures/TimelinePanel.js | 50 +++++++++++-----------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index aa278f2349..e065c5719e 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -1193,9 +1193,9 @@ const TimelinePanel = React.createClass({
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return (
-
+
);
}
@@ -1217,28 +1217,28 @@ const TimelinePanel = React.createClass({
);
return (
);
},
From 74189b21646fd7e4ab95fa6fa67767cf5ebf448f Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Tue, 7 May 2019 16:05:56 +0100
Subject: [PATCH 06/28] Fix indentation in MessagePanel
---
src/components/structures/MessagePanel.js | 36 +++++++++++++----------
1 file changed, 20 insertions(+), 16 deletions(-)
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b57b659136..b0f6699bc1 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -511,22 +511,26 @@ module.exports = React.createClass({
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
-
-
- ,
+
+
+ ,
);
return ret;
From 8903f65fcbb09cf154da767e8598c41ec5f619d6 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Wed, 8 May 2019 15:06:48 +0100
Subject: [PATCH 07/28] Enable client-side aggregations for reactions
If reactions are enabled, we need to enable client-side aggregation in the
`MatrixClient` to access the data.
---
src/MatrixClientPeg.js | 5 ++++-
src/i18n/strings/en_EN.json | 2 +-
src/settings/Settings.js | 2 +-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 763eddbd5d..cd40c7874e 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -175,6 +175,8 @@ class MatrixClientPeg {
}
_createClient(creds: MatrixClientCreds) {
+ const aggregateRelations = SettingsStore.isFeatureEnabled("feature_reactions");
+
const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
@@ -183,7 +185,8 @@ class MatrixClientPeg {
deviceId: creds.deviceId,
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
- verificationMethods: [verificationMethods.SAS]
+ verificationMethods: [verificationMethods.SAS],
+ unstableClientRelationAggregation: aggregateRelations,
};
this.matrixClient = createMatrixClient(opts);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index eaea057b36..b8db74cdeb 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -297,7 +297,7 @@
"Show recent room avatars above the room list": "Show recent room avatars above the room list",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header",
- "React to messages with emoji": "React to messages with emoji",
+ "React to messages with emoji (refresh to apply changes)": "React to messages with emoji (refresh to apply changes)",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 4be1b67227..1c3ca4fd0f 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -120,7 +120,7 @@ export const SETTINGS = {
},
"feature_reactions": {
isFeature: true,
- displayName: _td("React to messages with emoji"),
+ displayName: _td("React to messages with emoji (refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
From 8fdb59a90984e4b81e6f515d89c998bb41a381f4 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Wed, 8 May 2019 18:16:27 +0100
Subject: [PATCH 08/28] Use basic read path from JS SDK for reactions
This displays existing reactions correctly in the action bar and reaction row,
but it doesn't yet update after a new reaction is sent.
---
src/components/structures/MessagePanel.js | 4 ++
src/components/structures/TimelinePanel.js | 5 +++
.../views/messages/MessageActionBar.js | 4 ++
.../views/messages/ReactionDimension.js | 38 ++++++++++++++++++-
src/components/views/messages/ReactionsRow.js | 25 +++---------
src/components/views/rooms/EventTile.js | 23 +++++++++--
6 files changed, 76 insertions(+), 23 deletions(-)
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b0f6699bc1..2037217710 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -92,6 +92,9 @@ module.exports = React.createClass({
// show timestamps always
alwaysShowTimestamps: PropTypes.bool,
+
+ // helper function to access relations for an event
+ getRelationsForEvent: PropTypes.func,
},
componentWillMount: function() {
@@ -529,6 +532,7 @@ module.exports = React.createClass({
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
+ getRelationsForEvent={this.props.getRelationsForEvent}
/>
,
);
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index e065c5719e..17a062be98 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -1168,6 +1168,10 @@ const TimelinePanel = React.createClass({
});
},
+ getRelationsForEvent(...args) {
+ return this.props.timelineSet.getRelationsForEvent(...args);
+ },
+
render: function() {
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
@@ -1239,6 +1243,7 @@ const TimelinePanel = React.createClass({
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
+ getRelationsForEvent={this.getRelationsForEvent}
/>
);
},
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index 9a482c9e6e..d2ac9adcf0 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -28,6 +28,8 @@ import { isContentActionable } from '../../../utils/EventUtils';
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions: PropTypes.object,
permalinkCreator: PropTypes.object,
getTile: PropTypes.func,
getReplyThread: PropTypes.func,
@@ -113,6 +115,7 @@ export default class MessageActionBar extends React.PureComponent {
return ;
}
@@ -135,6 +138,7 @@ export default class MessageActionBar extends React.PureComponent {
return ;
}
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index 3b72aabe15..675d99e187 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -18,20 +18,56 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+
export default class ReactionDimension extends React.PureComponent {
static propTypes = {
options: PropTypes.array.isRequired,
title: PropTypes.string,
+ // The Relations model from the JS SDK for reactions
+ reactions: PropTypes.object,
};
constructor(props) {
super(props);
this.state = {
- selected: null,
+ selected: this.getInitialSelection(),
};
}
+ getInitialSelection() {
+ const myReactions = this.getMyReactions();
+ if (!myReactions) {
+ return null;
+ }
+ const { options } = this.props;
+ let selected = null;
+ for (const { key, content } of options) {
+ const reactionExists = myReactions.some(mxEvent => {
+ return mxEvent.getContent()["m.relates_to"].key === content;
+ });
+ if (reactionExists) {
+ if (selected) {
+ // If there are multiple selected values (only expected to occur via
+ // non-Riot clients), then act as if none are selected.
+ return null;
+ }
+ selected = key;
+ }
+ }
+ return selected;
+ }
+
+ getMyReactions() {
+ const reactions = this.props.reactions;
+ if (!reactions) {
+ return null;
+ }
+ const userId = MatrixClientPeg.get().getUserId();
+ return reactions.getAnnotationsBySender()[userId];
+ }
+
onOptionClick = (ev) => {
const { key } = ev.target.dataset;
this.toggleDimensionValue(key);
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index a4299b9853..7242fa9144 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -20,37 +20,24 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import { isContentActionable } from '../../../utils/EventUtils';
-// TODO: Actually load reactions from the timeline
-// Since we don't yet load reactions, let's inject some dummy data for testing the UI
-// only. The UI assumes these are already sorted into the order we want to present,
-// presumably highest vote first.
-const SAMPLE_REACTIONS = {
- "👍": 4,
- "👎": 2,
- "🙂": 1,
-};
-
export default class ReactionsRow extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions: PropTypes.object,
}
render() {
- const { mxEvent } = this.props;
+ const { mxEvent, reactions } = this.props;
- if (!isContentActionable(mxEvent)) {
- return null;
- }
-
- const content = mxEvent.getContent();
- // TODO: Remove this once we load real reactions
- if (!content.body || content.body !== "reactions test") {
+ if (!reactions || !isContentActionable(mxEvent)) {
return null;
}
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
- const items = Object.entries(SAMPLE_REACTIONS).map(([content, count]) => {
+ const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
+ const count = events.length;
return
: null;
- let reactions;
+ let reactionsRow;
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
- reactions = ;
}
@@ -750,7 +767,7 @@ module.exports = withMatrixClient(React.createClass({
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} />
{ keyRequestInfo }
- { reactions }
+ { reactionsRow }
{ actionBar }
{
From 6a59143ffb334a896b8e596f4ea34c967df59e31 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Thu, 9 May 2019 15:14:58 +0100
Subject: [PATCH 09/28] Update reactions when redacted
This updates the reaction state in the reaction row and action bar when a
reaction is redacted.
Part of https://github.com/vector-im/riot-web/issues/9574
---
.../views/messages/ReactionDimension.js | 32 ++++++++++++++++--
src/components/views/messages/ReactionsRow.js | 33 ++++++++++++++++++-
2 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index 675d99e187..015e69d6b6 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -32,11 +32,36 @@ export default class ReactionDimension extends React.PureComponent {
super(props);
this.state = {
- selected: this.getInitialSelection(),
+ selected: this.getSelection(),
};
+
+ if (props.reactions) {
+ props.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
}
- getInitialSelection() {
+ componentWillReceiveProps(nextProps) {
+ if (this.props.reactions !== nextProps.reactions) {
+ nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ this.setState({
+ selected: this.getSelection(),
+ });
+ }
+
+ getSelection() {
const myReactions = this.getMyReactions();
if (!myReactions) {
return null;
@@ -45,6 +70,9 @@ export default class ReactionDimension extends React.PureComponent {
let selected = null;
for (const { key, content } of options) {
const reactionExists = myReactions.some(mxEvent => {
+ if (mxEvent.isRedacted()) {
+ return false;
+ }
return mxEvent.getContent()["m.relates_to"].key === content;
});
if (reactionExists) {
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index 7242fa9144..a58990a8a5 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -28,6 +28,34 @@ export default class ReactionsRow extends React.PureComponent {
reactions: PropTypes.object,
}
+ constructor(props) {
+ super(props);
+
+ if (props.reactions) {
+ props.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.reactions !== nextProps.reactions) {
+ nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ // TODO: Call `onHeightChanged` as needed
+ this.forceUpdate();
+ }
+
render() {
const { mxEvent, reactions } = this.props;
@@ -37,7 +65,10 @@ export default class ReactionsRow extends React.PureComponent {
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
- const count = events.length;
+ const count = events.size;
+ if (!count) {
+ return null;
+ }
return
Date: Thu, 9 May 2019 15:39:00 +0100
Subject: [PATCH 10/28] Remove redundant `key` vs. `content` in
`ReactionDimension`
This simplifies `ReactionDimension` by using the emoji string everywhere instead
of keeping a separate text string as well. It should improve readability as
well, as the reaction events also have a field `key` which was the emoji
content, which was easy to confuse.
---
.../views/messages/MessageActionBar.js | 24 ++-----------------
.../views/messages/ReactionDimension.js | 21 ++++++++--------
2 files changed, 13 insertions(+), 32 deletions(-)
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index d2ac9adcf0..903abfc2d2 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -102,19 +102,9 @@ export default class MessageActionBar extends React.PureComponent {
}
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
- const options = [
- {
- key: "agree",
- content: "👍",
- },
- {
- key: "disagree",
- content: "👎",
- },
- ];
return ;
}
@@ -125,19 +115,9 @@ export default class MessageActionBar extends React.PureComponent {
}
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
- const options = [
- {
- key: "like",
- content: "🙂",
- },
- {
- key: "dislike",
- content: "😔",
- },
- ];
return ;
}
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index 015e69d6b6..f7b43a418c 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -22,6 +22,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
export default class ReactionDimension extends React.PureComponent {
static propTypes = {
+ // Array of strings containing the emoji for each option
options: PropTypes.array.isRequired,
title: PropTypes.string,
// The Relations model from the JS SDK for reactions
@@ -68,12 +69,12 @@ export default class ReactionDimension extends React.PureComponent {
}
const { options } = this.props;
let selected = null;
- for (const { key, content } of options) {
+ for (const option of options) {
const reactionExists = myReactions.some(mxEvent => {
if (mxEvent.isRedacted()) {
return false;
}
- return mxEvent.getContent()["m.relates_to"].key === content;
+ return mxEvent.getContent()["m.relates_to"].key === option;
});
if (reactionExists) {
if (selected) {
@@ -81,7 +82,7 @@ export default class ReactionDimension extends React.PureComponent {
// non-Riot clients), then act as if none are selected.
return null;
}
- selected = key;
+ selected = option;
}
}
return selected;
@@ -98,12 +99,12 @@ export default class ReactionDimension extends React.PureComponent {
onOptionClick = (ev) => {
const { key } = ev.target.dataset;
- this.toggleDimensionValue(key);
+ this.toggleDimension(key);
}
- toggleDimensionValue(value) {
+ toggleDimension(key) {
const state = this.state.selected;
- const newState = state !== value ? value : null;
+ const newState = state !== key ? key : null;
this.setState({
selected: newState,
});
@@ -115,16 +116,16 @@ export default class ReactionDimension extends React.PureComponent {
const { options } = this.props;
const items = options.map(option => {
- const disabled = selected && selected !== option.key;
+ const disabled = selected && selected !== option;
const classes = classNames({
mx_ReactionDimension_disabled: disabled,
});
- return
- {option.content}
+ {option}
;
});
From 37d2f60045457e1d35f482b6fb0f4012a4d45e06 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Thu, 9 May 2019 17:59:51 +0100
Subject: [PATCH 11/28] Listen for reaction collections when they are created
The `EventTile` for events without reactions now use `Event.relationsCreated` to
listen for a future time where they come in to being.
---
src/components/views/rooms/EventTile.js | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index d40dd5a77c..5a5aaeb098 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -193,9 +193,12 @@ module.exports = withMatrixClient(React.createClass({
componentDidMount: function() {
this._suppressReadReceiptAnimation = false;
- this.props.matrixClient.on("deviceVerificationChanged",
- this.onDeviceVerificationChanged);
+ const client = this.props.matrixClient;
+ client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
+ if (SettingsStore.isFeatureEnabled("feature_reactions")) {
+ this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
+ }
},
componentWillReceiveProps: function(nextProps) {
@@ -218,6 +221,9 @@ module.exports = withMatrixClient(React.createClass({
const client = this.props.matrixClient;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
+ if (SettingsStore.isFeatureEnabled("feature_reactions")) {
+ this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
+ }
},
/** called when the event is decrypted after we show it.
@@ -486,6 +492,14 @@ module.exports = withMatrixClient(React.createClass({
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
},
+ _onReactionsCreated(relationType, eventType) {
+ if (relationType !== "m.annotation" || eventType !== "m.reaction") {
+ return;
+ }
+ this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
+ this.forceUpdate();
+ },
+
render: function() {
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
const SenderProfile = sdk.getComponent('messages.SenderProfile');
From 39bd0d8bb3c357ba15485ad98862a8339a3cc570 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Thu, 9 May 2019 18:37:52 +0100
Subject: [PATCH 12/28] Listen for additional reactions in existing collection
The various reaction UI bits will now listen for `Reactions.add` for new
reactions just like with redactions.
Part of https://github.com/vector-im/riot-web/issues/9572
---
src/components/views/messages/ReactionDimension.js | 6 ++++++
src/components/views/messages/ReactionsRow.js | 6 ++++++
2 files changed, 12 insertions(+)
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index f7b43a418c..13d43804ac 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -37,18 +37,24 @@ export default class ReactionDimension extends React.PureComponent {
};
if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.reactions !== nextProps.reactions) {
+ nextProps.reactions.on("Relations.add", this.onReactionsChange);
nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillUnmount() {
if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index a58990a8a5..c49f5cba75 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -32,18 +32,24 @@ export default class ReactionsRow extends React.PureComponent {
super(props);
if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.reactions !== nextProps.reactions) {
+ nextProps.reactions.on("Relations.add", this.onReactionsChange);
nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
componentWillUnmount() {
if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
From dc4fccd291a4e91874b767585d2f35b00a40e832 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Thu, 9 May 2019 19:11:40 +0100
Subject: [PATCH 13/28] Send and redact reaction events
This updates both the reaction row and action bar UIs to send and redact
reaction events as appropriate based on user interactions.
Fixes https://github.com/vector-im/riot-web/issues/9574
Fixes https://github.com/vector-im/riot-web/issues/9572
---
.../views/messages/MessageActionBar.js | 2 +
.../views/messages/ReactionDimension.js | 66 ++++++++++++-------
src/components/views/messages/ReactionsRow.js | 26 ++++++++
.../views/messages/ReactionsRowButton.js | 48 +++++++-------
4 files changed, 95 insertions(+), 47 deletions(-)
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index 903abfc2d2..52630d7b0e 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -106,6 +106,7 @@ export default class MessageActionBar extends React.PureComponent {
title={_t("Agree or Disagree")}
options={["👍", "👎"]}
reactions={this.props.reactions}
+ mxEvent={this.props.mxEvent}
/>;
}
@@ -119,6 +120,7 @@ export default class MessageActionBar extends React.PureComponent {
title={_t("Like or Dislike")}
options={["🙂", "😔"]}
reactions={this.props.reactions}
+ mxEvent={this.props.mxEvent}
/>;
}
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index 13d43804ac..6e8b8ddc99 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -22,6 +22,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
export default class ReactionDimension extends React.PureComponent {
static propTypes = {
+ mxEvent: PropTypes.object.isRequired,
// Array of strings containing the emoji for each option
options: PropTypes.array.isRequired,
title: PropTypes.string,
@@ -32,9 +33,7 @@ export default class ReactionDimension extends React.PureComponent {
constructor(props) {
super(props);
- this.state = {
- selected: this.getSelection(),
- };
+ this.state = this.getSelection();
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
@@ -63,35 +62,42 @@ export default class ReactionDimension extends React.PureComponent {
}
onReactionsChange = () => {
- this.setState({
- selected: this.getSelection(),
- });
+ this.setState(this.getSelection());
}
getSelection() {
const myReactions = this.getMyReactions();
if (!myReactions) {
- return null;
+ return {
+ selectedOption: null,
+ selectedReactionEvent: null,
+ };
}
const { options } = this.props;
- let selected = null;
+ let selectedOption = null;
+ let selectedReactionEvent = null;
for (const option of options) {
- const reactionExists = myReactions.some(mxEvent => {
+ const reactionForOption = myReactions.find(mxEvent => {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === option;
});
- if (reactionExists) {
- if (selected) {
- // If there are multiple selected values (only expected to occur via
- // non-Riot clients), then act as if none are selected.
- return null;
- }
- selected = option;
+ if (!reactionForOption) {
+ continue;
}
+ if (selectedOption) {
+ // If there are multiple selected values (only expected to occur via
+ // non-Riot clients), then act as if none are selected.
+ return {
+ selectedOption: null,
+ selectedReactionEvent: null,
+ };
+ }
+ selectedOption = option;
+ selectedReactionEvent = reactionForOption;
}
- return selected;
+ return { selectedOption, selectedReactionEvent };
}
getMyReactions() {
@@ -109,20 +115,34 @@ export default class ReactionDimension extends React.PureComponent {
}
toggleDimension(key) {
- const state = this.state.selected;
- const newState = state !== key ? key : null;
+ const { selectedOption, selectedReactionEvent } = this.state;
+ const newSelectedOption = selectedOption !== key ? key : null;
this.setState({
- selected: newState,
+ selectedOption: newSelectedOption,
});
- // TODO: Send the reaction event
+ if (selectedReactionEvent) {
+ MatrixClientPeg.get().redactEvent(
+ this.props.mxEvent.getRoomId(),
+ selectedReactionEvent.getId(),
+ );
+ }
+ if (newSelectedOption) {
+ MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
+ "m.relates_to": {
+ "rel_type": "m.annotation",
+ "event_id": this.props.mxEvent.getId(),
+ "key": newSelectedOption,
+ },
+ });
+ }
}
render() {
- const { selected } = this.state;
+ const { selectedOption } = this.state;
const { options } = this.props;
const items = options.map(option => {
- const disabled = selected && selected !== option;
+ const disabled = selectedOption && selectedOption !== option;
const classes = classNames({
mx_ReactionDimension_disabled: disabled,
});
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index c49f5cba75..3172b8b44c 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import { isContentActionable } from '../../../utils/EventUtils';
+import MatrixClientPeg from '../../../MatrixClientPeg';
export default class ReactionsRow extends React.PureComponent {
static propTypes = {
@@ -35,6 +36,10 @@ export default class ReactionsRow extends React.PureComponent {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
+
+ this.state = {
+ myReactions: this.getMyReactions(),
+ };
}
componentWillReceiveProps(nextProps) {
@@ -59,11 +64,24 @@ export default class ReactionsRow extends React.PureComponent {
onReactionsChange = () => {
// TODO: Call `onHeightChanged` as needed
+ this.setState({
+ myReactions: this.getMyReactions(),
+ });
this.forceUpdate();
}
+ getMyReactions() {
+ const reactions = this.props.reactions;
+ if (!reactions) {
+ return null;
+ }
+ const userId = MatrixClientPeg.get().getUserId();
+ return reactions.getAnnotationsBySender()[userId];
+ }
+
render() {
const { mxEvent, reactions } = this.props;
+ const { myReactions } = this.state;
if (!reactions || !isContentActionable(mxEvent)) {
return null;
@@ -75,10 +93,18 @@ export default class ReactionsRow extends React.PureComponent {
if (!count) {
return null;
}
+ const myReactionEvent = myReactions && myReactions.find(mxEvent => {
+ if (mxEvent.isRedacted()) {
+ return false;
+ }
+ return mxEvent.getContent()["m.relates_to"].key === content;
+ });
return ;
});
diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js
index 985479a237..721147cdb8 100644
--- a/src/components/views/messages/ReactionsRowButton.js
+++ b/src/components/views/messages/ReactionsRowButton.js
@@ -18,48 +18,48 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
+ // The event we're displaying reactions for
+ mxEvent: PropTypes.object.isRequired,
content: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
- }
-
- constructor(props) {
- super(props);
-
- // TODO: This should be derived from actual reactions you may have sent
- // once we have some API to read them.
- this.state = {
- selected: false,
- };
+ // A possible Matrix event if the current user has voted for this type
+ myReactionEvent: PropTypes.object,
}
onClick = (ev) => {
- const state = this.state.selected;
- this.setState({
- selected: !state,
- });
- // TODO: Send the reaction event
+ const { mxEvent, myReactionEvent, content } = this.props;
+ if (myReactionEvent) {
+ MatrixClientPeg.get().redactEvent(
+ mxEvent.getRoomId(),
+ myReactionEvent.getId(),
+ );
+ } else {
+ MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
+ "m.relates_to": {
+ "rel_type": "m.annotation",
+ "event_id": mxEvent.getId(),
+ "key": content,
+ },
+ });
+ }
};
render() {
- const { content, count } = this.props;
- const { selected } = this.state;
+ const { content, count, myReactionEvent } = this.props;
const classes = classNames({
mx_ReactionsRowButton: true,
- mx_ReactionsRowButton_selected: selected,
+ mx_ReactionsRowButton_selected: !!myReactionEvent,
});
- let adjustedCount = count;
- if (selected) {
- adjustedCount++;
- }
-
return
- {content} {adjustedCount}
+ {content} {count}
;
}
}
From 33513a95d74cf8742a90796bbe80a21ff5c5013d Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Fri, 10 May 2019 17:59:17 +0100
Subject: [PATCH 14/28] Change to `componentDidUpdate` to process first
reaction
This changes to "did update" and also calls the reaction change handler to
ensure that we update the state of my reactions (to know which were sent by
you).
---
src/components/views/messages/ReactionDimension.js | 9 +++++----
src/components/views/messages/ReactionsRow.js | 9 +++++----
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js
index 6e8b8ddc99..a0cf5a86ec 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -41,10 +41,11 @@ export default class ReactionDimension extends React.PureComponent {
}
}
- componentWillReceiveProps(nextProps) {
- if (this.props.reactions !== nextProps.reactions) {
- nextProps.reactions.on("Relations.add", this.onReactionsChange);
- nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
+ componentDidUpdate(prevProps) {
+ if (prevProps.reactions !== this.props.reactions) {
+ this.props.reactions.on("Relations.add", this.onReactionsChange);
+ this.props.reactions.on("Relations.redaction", this.onReactionsChange);
+ this.onReactionsChange();
}
}
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index 3172b8b44c..dad604a4d5 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -42,10 +42,11 @@ export default class ReactionsRow extends React.PureComponent {
};
}
- componentWillReceiveProps(nextProps) {
- if (this.props.reactions !== nextProps.reactions) {
- nextProps.reactions.on("Relations.add", this.onReactionsChange);
- nextProps.reactions.on("Relations.redaction", this.onReactionsChange);
+ componentDidUpdate(prevProps) {
+ if (prevProps.reactions !== this.props.reactions) {
+ this.props.reactions.on("Relations.add", this.onReactionsChange);
+ this.props.reactions.on("Relations.redaction", this.onReactionsChange);
+ this.onReactionsChange();
}
}
From 6a3708d51b7a25ce3249a19d65fd14b765c0d4c0 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Fri, 10 May 2019 18:09:32 +0100
Subject: [PATCH 15/28] Disable select for reaction buttons
---
res/css/views/messages/_ReactionsRowButton.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss
index 49e3930979..3c6d019b30 100644
--- a/res/css/views/messages/_ReactionsRowButton.scss
+++ b/res/css/views/messages/_ReactionsRowButton.scss
@@ -24,6 +24,7 @@ limitations under the License.
border-radius: 10px;
background-color: $reaction-row-button-bg-color;
cursor: pointer;
+ user-select: none;
&:hover {
border-color: $reaction-row-button-hover-border-color;
From 5cb0a81565385b33c34d9472ed355bbafa49e2b4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 10 May 2019 20:28:28 +0100
Subject: [PATCH 16/28] Change roomavatar to myroomavatar and roomnick to
match, leaving alias
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/SlashCommands.js | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 8757f5108a..a1364efbe1 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -192,8 +192,8 @@ export const CommandMap = {
},
}),
- roomnick: new Command({
- name: 'roomnick',
+ myroomnick: new Command({
+ name: 'myroomnick',
args: '',
description: _td('Changes your display nickname in the current room only'),
runFn: function(roomId, args) {
@@ -210,9 +210,9 @@ export const CommandMap = {
},
}),
- roomavatar: new Command({
- name: 'roomavatar',
- args: '',
+ myroomavatar: new Command({
+ name: 'myroomavatar',
+ args: '[]',
description: _td('Changes your avatar in this current room only'),
runFn: function(roomId, args) {
const cli = MatrixClientPeg.get();
@@ -770,6 +770,7 @@ const aliases = {
j: "join",
newballsplease: "discardsession",
goto: "join", // because it handles event permalinks magically
+ roomnick: "myroomnick",
};
From 249f3d972b2230fececd82e2525f293561a25ba9 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 10 May 2019 20:58:32 +0100
Subject: [PATCH 17/28] Close Room Settings upon Leave Room
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.../views/dialogs/RoomSettingsDialog.js | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index 05ed262078..c221289ff3 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,6 +25,7 @@ import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
+import dis from "../../../dispatcher";
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
@@ -31,6 +33,22 @@ export default class RoomSettingsDialog extends React.Component {
onFinished: PropTypes.func.isRequired,
};
+ componentWillMount() {
+ this._dispatcherRef = dis.register(this._onAction);
+ }
+
+ componentWillUnmount() {
+ dis.unregister(this._dispatcherRef);
+ }
+
+ _onAction = (payload) => {
+ // When room changes below us, close the room settings
+ // whilst the modal is open this can only be triggered when someone hits Leave Room
+ if (payload.action === 'view_next_room') {
+ this.props.onFinished();
+ }
+ };
+
_getTabs() {
const tabs = [];
From 0530a64f1905876ee2e6c38b4ee07e6d7ca84801 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sat, 11 May 2019 18:46:13 +0100
Subject: [PATCH 18/28] Fix invite via MemberInfo. Thank U JetBrains for
solving this one for me <3
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/components/views/rooms/MemberInfo.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 35161dedf7..be10a3900f 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -735,8 +735,8 @@ module.exports = withMatrixClient(React.createClass({
// we're only inviting one user.
const inviter = new MultiInviter(roomId);
await inviter.invite([member.userId]).then(() => {
- if (inviter.getCompletionState(userId) !== "invited")
- throw new Error(inviter.getErrorText(userId));
+ if (inviter.getCompletionState(member.userId) !== "invited")
+ throw new Error(inviter.getErrorText(member.userId));
});
} catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
From 8bd45ce893406d3cba3fee0c17b0528833380d47 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 12 May 2019 16:36:43 +0100
Subject: [PATCH 19/28] add /rainbow and /rainbowme Slash Commands
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/SlashCommands.js | 21 ++++++++++++++
src/utils/colour.js | 67 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+)
create mode 100644 src/utils/colour.js
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index f72ba1e005..0dc717a6fe 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -30,6 +30,7 @@ import MultiInviter from './utils/MultiInviter';
import { linkifyAndSanitizeHtml } from './HtmlUtils';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import WidgetUtils from "./utils/WidgetUtils";
+import {textToHtmlRainbow} from "./utils/colour";
class Command {
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
@@ -718,6 +719,26 @@ export const CommandMap = {
return success();
},
}),
+
+ rainbow: new Command({
+ name: "rainbow",
+ description: _td(""),
+ args: '',
+ runFn: function(roomId, args) {
+ if (!args) return reject(this.getUserId());
+ return success(MatrixClientPeg.get().sendHtmlMessage(roomId, args, textToHtmlRainbow(args)));
+ },
+ }),
+
+ rainbowme: new Command({
+ name: "rainbowme",
+ description: _td(""),
+ args: '',
+ runFn: function(roomId, args) {
+ if (!args) return reject(this.getUserId());
+ return success(MatrixClientPeg.get().sendHtmlEmote(roomId, args, textToHtmlRainbow(args)));
+ },
+ })
};
/* eslint-enable babel/no-invalid-this */
diff --git a/src/utils/colour.js b/src/utils/colour.js
new file mode 100644
index 0000000000..5d90bf0773
--- /dev/null
+++ b/src/utils/colour.js
@@ -0,0 +1,67 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+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.
+*/
+
+export function hueToRGB(h, s, l) {
+ const c = s * (1 - Math.abs(2 * l - 1));
+ const x = c * (1 - Math.abs((h / 60) % 2 - 1));
+ const m = l - c / 2;
+
+ let r = 0;
+ let g = 0;
+ let b = 0;
+
+ if (0 <= h && h < 60) {
+ r = c;
+ g = x;
+ b = 0;
+ } else if (60 <= h && h < 120) {
+ r = x;
+ g = c;
+ b = 0;
+ } else if (120 <= h && h < 180) {
+ r = 0;
+ g = c;
+ b = x;
+ } else if (180 <= h && h < 240) {
+ r = 0;
+ g = x;
+ b = c;
+ } else if (240 <= h && h < 300) {
+ r = x;
+ g = 0;
+ b = c;
+ } else if (300 <= h && h < 360) {
+ r = c;
+ g = 0;
+ b = x;
+ }
+
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
+}
+
+
+export function textToHtmlRainbow(str) {
+ const frequency = 360 / str.length;
+
+ return str.split("").map((c, i) => {
+ const [r, g, b] = hueToRGB(i * frequency, 1.0, 0.5);
+ return '' + c + '';
+ }).join("");
+}
From 4c8593a91fb71f4336258e3c71341f482273f5a2 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 12 May 2019 16:40:27 +0100
Subject: [PATCH 20/28] Add i18n
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/SlashCommands.js | 4 ++--
src/i18n/strings/en_EN.json | 2 ++
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 0dc717a6fe..06120b183f 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -722,7 +722,7 @@ export const CommandMap = {
rainbow: new Command({
name: "rainbow",
- description: _td(""),
+ description: _td("Sends the given message coloured as a rainbow"),
args: '',
runFn: function(roomId, args) {
if (!args) return reject(this.getUserId());
@@ -732,7 +732,7 @@ export const CommandMap = {
rainbowme: new Command({
name: "rainbowme",
- description: _td(""),
+ description: _td("Sends the given emote coloured as a rainbow"),
args: '',
runFn: function(roomId, args) {
if (!args) return reject(this.getUserId());
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8534091176..be82c5427a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -178,6 +178,8 @@
"The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.",
"Displays action": "Displays action",
"Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded",
+ "Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow",
+ "Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow",
"Unrecognised command:": "Unrecognised command:",
"Reason": "Reason",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
From d9b03b019676575ae32ecebeda77888af83a17c3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 12 May 2019 16:42:13 +0100
Subject: [PATCH 21/28] fix i18n
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/i18n/strings/en_EN.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index eaea057b36..5b28cd2e7a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -146,6 +146,7 @@
"Upgrade": "Upgrade",
"Changes your display nickname": "Changes your display nickname",
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
+ "Changes your avatar in this current room only": "Changes your avatar in this current room only",
"Changes colour scheme of current room": "Changes colour scheme of current room",
"Gets or sets the room topic": "Gets or sets the room topic",
"This room has no topic.": "This room has no topic.",
From 9f9a245af19bfa773aa7c5d5313caaa91686eaac Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 12 May 2019 17:14:30 +0100
Subject: [PATCH 22/28] delint
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/SlashCommands.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 06120b183f..a3d4f71894 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -738,7 +738,7 @@ export const CommandMap = {
if (!args) return reject(this.getUserId());
return success(MatrixClientPeg.get().sendHtmlEmote(roomId, args, textToHtmlRainbow(args)));
},
- })
+ }),
};
/* eslint-enable babel/no-invalid-this */
From 4ffd826e76be3ee1bcd07aa010bc0f9f6c75b7c1 Mon Sep 17 00:00:00 2001
From: Travis Ralston
Date: Sun, 12 May 2019 22:53:06 -0600
Subject: [PATCH 23/28] Flip logic for unsafe->safe
---
src/components/views/auth/RegistrationForm.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index eb77e125af..b4638bf8f4 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -76,7 +76,7 @@ module.exports = React.createClass({
password: "",
passwordConfirm: "",
passwordComplexity: null,
- passwordUnsafe: false,
+ passwordSafe: false,
};
},
@@ -271,19 +271,19 @@ module.exports = React.createClass({
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
- const unsafe = complexity.score < PASSWORD_MIN_SCORE;
+ const safe = complexity.score >= PASSWORD_MIN_SCORE;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
- passwordUnsafe: unsafe,
+ passwordSafe: safe,
});
- return allowUnsafe || !unsafe;
+ return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
- if (this.state.passwordUnsafe) {
+ if (!this.state.passwordSafe) {
return _t("Password is allowed, but unsafe");
}
return _t("Nice, strong password!");
From 34990d1c8946008f1a012d1820a18dd3617fa60f Mon Sep 17 00:00:00 2001
From: David Baker
Date: Mon, 13 May 2019 14:17:09 +0100
Subject: [PATCH 24/28] Fix bug where email was not required where it shouldn't
have been
See comment!
Fixes https://github.com/vector-im/riot-web/issues/9681
---
src/components/views/auth/RegistrationForm.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 7c083ea270..bef0b9e019 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
-Copyright 2018 New Vector Ltd
+Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -150,7 +150,11 @@ module.exports = React.createClass({
if (!field) {
continue;
}
- field.validate({ allowEmpty: false });
+ // We must wait for these validations to finish before queueing
+ // up the setState below so our setState gies in the queue after
+ // all the setStates from these validate calls (that's how we
+ // know they've finished).
+ await field.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
From 59cefbaa442377a59f53ea8f401de5709decde43 Mon Sep 17 00:00:00 2001
From: David Baker
Date: Mon, 13 May 2019 14:24:56 +0100
Subject: [PATCH 25/28] Typo
Co-Authored-By: J. Ryan Stinnett
---
src/components/views/auth/RegistrationForm.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index bef0b9e019..654387e196 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -151,7 +151,7 @@ module.exports = React.createClass({
continue;
}
// We must wait for these validations to finish before queueing
- // up the setState below so our setState gies in the queue after
+ // up the setState below so our setState goes in the queue after
// all the setStates from these validate calls (that's how we
// know they've finished).
await field.validate({ allowEmpty: false });
From 24209905e85b1ba8c713cca425f9b18e2e0cb641 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Mon, 13 May 2019 14:41:49 +0100
Subject: [PATCH 26/28] Test settings later for performance
---
src/components/views/rooms/EventTile.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 5a5aaeb098..b71b28c635 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -483,8 +483,8 @@ module.exports = withMatrixClient(React.createClass({
getReactions() {
if (
- !SettingsStore.isFeatureEnabled("feature_reactions") ||
- !this.props.getRelationsForEvent
+ !this.props.getRelationsForEvent ||
+ !SettingsStore.isFeatureEnabled("feature_reactions")
) {
return null;
}
From af9fdbaeebd0dff2465a1a4f66f9bb985a8c5a62 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Mon, 13 May 2019 14:52:55 +0100
Subject: [PATCH 27/28] Rework `reactionsCreated` in `EventTile` to use state
This changes to use component state instead of `forceUpdate`, so that it's more
obvious why an update is happening here.
---
src/components/views/rooms/EventTile.js | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index b71b28c635..1706019e94 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -182,6 +182,8 @@ module.exports = withMatrixClient(React.createClass({
verified: null,
// Whether onRequestKeysClick has been called since mounting.
previouslyRequestedKeys: false,
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions: this.getReactions(),
};
},
@@ -497,7 +499,9 @@ module.exports = withMatrixClient(React.createClass({
return;
}
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
- this.forceUpdate();
+ this.setState({
+ reactions: this.getReactions(),
+ });
},
render: function() {
@@ -612,11 +616,10 @@ module.exports = withMatrixClient(React.createClass({
}
}
- const reactions = this.getReactions();
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
const actionBar = ;
}
From e22bd456089ad76efd76f50b960ba47b1dadac60 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett"
Date: Mon, 13 May 2019 15:00:43 +0100
Subject: [PATCH 28/28] Clarify `ReactionsRow` forceUpdate usage
---
src/components/views/messages/ReactionsRow.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index dad604a4d5..ffb81e1a38 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -68,6 +68,9 @@ export default class ReactionsRow extends React.PureComponent {
this.setState({
myReactions: this.getMyReactions(),
});
+ // Using `forceUpdate` for the moment, since we know the overall set of reactions
+ // has changed (this is triggered by events for that purpose only) and
+ // `PureComponent`s shallow state / props compare would otherwise filter this out.
this.forceUpdate();
}