diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d9a01e668..988a85fd43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+Changes in [1.1.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.1) (2019-05-14)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.0...v1.1.1)
+
+ * Fix registration with email
+ [\#2970](https://github.com/matrix-org/matrix-react-sdk/pull/2970)
+ * Fix bug where email was not required where it shouldn't have been
+ [\#2969](https://github.com/matrix-org/matrix-react-sdk/pull/2969)
+
Changes in [1.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.0) (2019-05-07)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.0-rc.1...v1.1.0)
diff --git a/package.json b/package.json
index 991080cb7e..bcbf6ea29f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "1.1.0",
+ "version": "1.1.1",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index 419542036e..e66c99e95b 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -23,7 +23,7 @@ limitations under the License.
line-height: 24px;
border-radius: 4px;
background: $message-action-bar-bg-color;
- top: -13px;
+ top: -18px;
right: 8px;
user-select: none;
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;
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 527394da4d..0e2389fd1c 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -126,6 +126,15 @@ export function getStoredSessionOwner() {
return hsUrl && userId && accessToken ? userId : null;
}
+/**
+ * @returns {bool} True if the stored session is for a guest user or false if it is
+ * for a real user. If there is no stored session, return null.
+ */
+export function getStoredSessionIsGuest() {
+ const sessVars = _getLocalStorageSessionVars();
+ return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
+}
+
/**
* @param {Object} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
@@ -235,7 +244,15 @@ function _getLocalStorageSessionVars() {
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
- return {hsUrl, isUrl, accessToken, userId, deviceId};
+ let isGuest;
+ if (localStorage.getItem("mx_is_guest") !== null) {
+ isGuest = localStorage.getItem("mx_is_guest") === "true";
+ } else {
+ // legacy key name
+ isGuest = localStorage.getItem("matrix-is-guest") === "true";
+ }
+
+ return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest};
}
// returns a promise which resolves to true if a session is found in
@@ -253,15 +270,7 @@ async function _restoreFromLocalStorage() {
return false;
}
- const {hsUrl, isUrl, accessToken, userId, deviceId} = _getLocalStorageSessionVars();
-
- let isGuest;
- if (localStorage.getItem("mx_is_guest") !== null) {
- isGuest = localStorage.getItem("mx_is_guest") === "true";
- } else {
- // legacy key name
- isGuest = localStorage.getItem("matrix-is-guest") === "true";
- }
+ const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = _getLocalStorageSessionVars();
if (accessToken && userId && hsUrl) {
console.log(`Restoring session for ${userId}`);
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/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 277985ba1d..0b52cfa1bc 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -1710,14 +1710,15 @@ export default React.createClass({
// returns a promise which resolves to the new MatrixClient
onRegistered: function(credentials) {
- // XXX: This should be in state or ideally store(s) because we risk not
- // rendering the most up-to-date view of state otherwise.
- this._is_registered = true;
if (this.state.register_session_id) {
// The user came in through an email validation link. To avoid overwriting
- // their session, check to make sure the session isn't someone else.
+ // their session, check to make sure the session isn't someone else, and
+ // isn't a guest user since we'll usually have set a guest user session before
+ // starting the registration process. This isn't perfect since it's possible
+ // the user had a separate guest session they didn't actually mean to replace.
const sessionOwner = Lifecycle.getStoredSessionOwner();
- if (sessionOwner && sessionOwner !== credentials.userId) {
+ const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
+ if (sessionOwner && !sessionIsGuest && sessionOwner !== credentials.userId) {
console.log(
`Found a session for ${sessionOwner} but ${credentials.userId} is trying to verify their ` +
`email address. Restoring the session for ${sessionOwner} with warning.`,
@@ -1748,6 +1749,9 @@ export default React.createClass({
return MatrixClientPeg.get();
}
}
+ // XXX: This should be in state or ideally store(s) because we risk not
+ // rendering the most up-to-date view of state otherwise.
+ this._is_registered = true;
return Lifecycle.setLoggedIn(credentials);
},
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b57b659136..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() {
@@ -511,22 +514,27 @@ module.exports = React.createClass({
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
-
-
- ,
+
+
+ ,
);
return ret;
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index aa278f2349..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");
@@ -1193,9 +1197,9 @@ const TimelinePanel = React.createClass({
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return (
-
+
);
}
@@ -1217,28 +1221,29 @@ const TimelinePanel = React.createClass({
);
return (
);
},
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index 7c083ea270..33df42be15 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.
@@ -76,6 +76,7 @@ module.exports = React.createClass({
password: "",
passwordConfirm: "",
passwordComplexity: null,
+ passwordSafe: false,
};
},
@@ -150,7 +151,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 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 });
}
// Validation and state updates are async, so we need to wait for them to complete
@@ -270,12 +275,23 @@ module.exports = React.createClass({
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
+ const safe = complexity.score >= PASSWORD_MIN_SCORE;
+ const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
+ passwordSafe: safe,
});
- return complexity.score >= PASSWORD_MIN_SCORE;
+ 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.passwordSafe) {
+ 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/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 9fd42fb31d..2f12022140 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -197,12 +197,18 @@ export default class MImageBody extends React.Component {
// synapse only supports 800x600 thumbnails for now though,
// so we'll need to download the original image for this to work
// well for now. First, let's try a few cases that let us avoid
- // downloading the original:
- if (pixelRatio === 1.0 ||
- (!content.info || !content.info.w ||
- !content.info.h || !content.info.size)) {
- // always thumbnail. it may look a bit worse, but it'll save bandwidth.
- // which is probably desirable on a lo-dpi device anyway.
+ // downloading the original, including:
+ // - When displaying a GIF, we always want to thumbnail so that we can
+ // properly respect the user's GIF autoplay setting (which relies on
+ // thumbnailing to produce the static preview image)
+ // - On a low DPI device, always thumbnail to save bandwidth
+ // - If there's no sizing info in the event, default to thumbnail
+ const info = content.info;
+ if (
+ this._isGif() ||
+ pixelRatio === 1.0 ||
+ (!info || !info.w || !info.h || !info.size)
+ ) {
return this.context.matrixClient.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
} else {
// we should only request thumbnails if the image is bigger than 800x600
@@ -215,10 +221,10 @@ export default class MImageBody extends React.Component {
// timeline (e.g. >1MB).
const isLargerThanThumbnail = (
- content.info.w > thumbWidth ||
- content.info.h > thumbHeight
+ info.w > thumbWidth ||
+ info.h > thumbHeight
);
- const isLargeFileSize = content.info.size > 1*1024*1024;
+ const isLargeFileSize = info.size > 1*1024*1024;
if (isLargeFileSize && isLargerThanThumbnail) {
// image is too large physically and bytewise to clutter our timeline so
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index 9a482c9e6e..52630d7b0e 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,
@@ -100,19 +102,11 @@ export default class MessageActionBar extends React.PureComponent {
}
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
- const options = [
- {
- key: "agree",
- content: "👍",
- },
- {
- key: "disagree",
- content: "👎",
- },
- ];
return ;
}
@@ -122,19 +116,11 @@ 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 3b72aabe15..a0cf5a86ec 100644
--- a/src/components/views/messages/ReactionDimension.js
+++ b/src/components/views/messages/ReactionDimension.js
@@ -18,49 +18,141 @@ 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 = {
+ mxEvent: PropTypes.object.isRequired,
+ // 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
+ reactions: PropTypes.object,
};
constructor(props) {
super(props);
- this.state = {
- selected: null,
- };
+ this.state = this.getSelection();
+
+ if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
+ props.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();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ this.setState(this.getSelection());
+ }
+
+ getSelection() {
+ const myReactions = this.getMyReactions();
+ if (!myReactions) {
+ return {
+ selectedOption: null,
+ selectedReactionEvent: null,
+ };
+ }
+ const { options } = this.props;
+ let selectedOption = null;
+ let selectedReactionEvent = null;
+ for (const option of options) {
+ const reactionForOption = myReactions.find(mxEvent => {
+ if (mxEvent.isRedacted()) {
+ return false;
+ }
+ return mxEvent.getContent()["m.relates_to"].key === 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 { selectedOption, selectedReactionEvent };
+ }
+
+ 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);
+ this.toggleDimension(key);
}
- toggleDimensionValue(value) {
- const state = this.state.selected;
- const newState = state !== value ? value : null;
+ toggleDimension(key) {
+ 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.key;
+ const disabled = selectedOption && selectedOption !== option;
const classes = classNames({
mx_ReactionDimension_disabled: disabled,
});
- return
- {option.content}
+ {option}
;
});
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index a4299b9853..ffb81e1a38 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -19,42 +19,96 @@ 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,
-};
+import MatrixClientPeg from '../../../MatrixClientPeg';
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,
+ }
+
+ constructor(props) {
+ super(props);
+
+ if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
+ props.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+
+ this.state = {
+ myReactions: this.getMyReactions(),
+ };
+ }
+
+ 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();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ // TODO: Call `onHeightChanged` as needed
+ 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();
+ }
+
+ getMyReactions() {
+ const reactions = this.props.reactions;
+ if (!reactions) {
+ return null;
+ }
+ const userId = MatrixClientPeg.get().getUserId();
+ return reactions.getAnnotationsBySender()[userId];
}
render() {
- const { mxEvent } = this.props;
+ const { mxEvent, reactions } = this.props;
+ const { myReactions } = this.state;
- 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.size;
+ 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}
;
}
}
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index f9dc3df2dc..95254323fa 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -49,7 +49,7 @@ module.exports = React.createClass({
return ; // We should never have been instaniated in this case
}
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
- const permalinkCreator = new RoomPermalinkCreator(prevRoom);
+ const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
permalinkCreator.load();
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
return
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 59025bf431..1706019e94 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -159,6 +159,9 @@ module.exports = withMatrixClient(React.createClass({
// show twelve hour timestamps
isTwelveHour: PropTypes.bool,
+
+ // helper function to access relations for an event
+ getRelationsForEvent: PropTypes.func,
},
getDefaultProps: function() {
@@ -179,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(),
};
},
@@ -190,9 +195,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) {
@@ -215,6 +223,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.
@@ -472,6 +483,27 @@ module.exports = withMatrixClient(React.createClass({
return this.refs.replyThread;
},
+ getReactions() {
+ if (
+ !this.props.getRelationsForEvent ||
+ !SettingsStore.isFeatureEnabled("feature_reactions")
+ ) {
+ return null;
+ }
+ const eventId = this.props.mxEvent.getId();
+ 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.setState({
+ reactions: this.getReactions(),
+ });
+ },
+
render: function() {
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
const SenderProfile = sdk.getComponent('messages.SenderProfile');
@@ -587,6 +619,7 @@ module.exports = withMatrixClient(React.createClass({
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
const actionBar =
: null;
- let reactions;
+ let reactionsRow;
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
- reactions = ;
}
@@ -750,7 +784,7 @@ module.exports = withMatrixClient(React.createClass({
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} />
{ keyRequestInfo }
- { reactions }
+ { reactionsRow }
{ actionBar }
{
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e8e5c2b202..6747e67833 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -302,6 +302,7 @@
"Render simple counters in room header": "Render simple counters in room header",
"Custom Notification Sounds": "Custom Notification Sounds",
"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",
@@ -1336,6 +1337,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",
diff --git a/src/matrix-to.js b/src/matrix-to.js
index a198bb422e..14467cb4c5 100644
--- a/src/matrix-to.js
+++ b/src/matrix-to.js
@@ -70,8 +70,12 @@ const MAX_SERVER_CANDIDATES = 3;
// the list and magically have the link work.
export class RoomPermalinkCreator {
- constructor(room) {
+ // We support being given a roomId as a fallback in the event the `room` object
+ // doesn't exist or is not healthy for us to rely on. For example, loading a
+ // permalink to a room which the MatrixClient doesn't know about.
+ constructor(room, roomId=null) {
this._room = room;
+ this._roomId = room ? room.roomId : roomId;
this._highestPlUserId = null;
this._populationMap = null;
this._bannedHostsRegexps = null;
@@ -79,6 +83,10 @@ export class RoomPermalinkCreator {
this._serverCandidates = null;
this._started = false;
+ if (!this._roomId) {
+ throw new Error("Failed to resolve a roomId for the permalink creator to use");
+ }
+
this.onMembership = this.onMembership.bind(this);
this.onRoomState = this.onRoomState.bind(this);
}
@@ -116,13 +124,13 @@ export class RoomPermalinkCreator {
}
forEvent(eventId) {
- const roomId = this._room.roomId;
+ const roomId = this._roomId;
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
}
forRoom() {
- const roomId = this._room.roomId;
+ const roomId = this._roomId;
const permalinkBase = `${baseUrl}/#/${roomId}`;
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
}
@@ -246,7 +254,6 @@ export class RoomPermalinkCreator {
}
}
-
export function makeUserPermalink(userId) {
return `${baseUrl}/#/${userId}`;
}
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index e5ae504b53..60740bae31 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -125,7 +125,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,
},
diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js
index 45b5f74c5e..33947703ef 100644
--- a/test/matrix-to-test.js
+++ b/test/matrix-to-test.js
@@ -74,7 +74,7 @@ describe('matrix-to', function() {
});
it('should pick no candidate servers when the room has no members', function() {
- const room = mockRoom(null, []);
+ const room = mockRoom("!fake:example.org", []);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator._serverCandidates).toBeTruthy();
@@ -82,7 +82,7 @@ describe('matrix-to', function() {
});
it('should pick a candidate server for the highest power level user in the room', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:pl_50",
powerLevel: 50,
@@ -109,7 +109,7 @@ describe('matrix-to', function() {
userId: "@alice:pl_95",
powerLevel: 95,
};
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:pl_50",
powerLevel: 50,
@@ -132,7 +132,7 @@ describe('matrix-to', function() {
});
it('should pick candidate servers based on user population', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:first",
powerLevel: 0,
@@ -168,7 +168,7 @@ describe('matrix-to', function() {
});
it('should pick prefer candidate servers with higher power levels', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:first",
powerLevel: 100,
@@ -195,7 +195,7 @@ describe('matrix-to', function() {
});
it('should pick a maximum of 3 candidate servers', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:alpha",
powerLevel: 100,
@@ -224,7 +224,7 @@ describe('matrix-to', function() {
});
it('should not consider IPv4 hosts', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:127.0.0.1",
powerLevel: 100,
@@ -237,7 +237,7 @@ describe('matrix-to', function() {
});
it('should not consider IPv6 hosts', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:[::1]",
powerLevel: 100,
@@ -250,7 +250,7 @@ describe('matrix-to', function() {
});
it('should not consider IPv4 hostnames with ports', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:127.0.0.1:8448",
powerLevel: 100,
@@ -263,7 +263,7 @@ describe('matrix-to', function() {
});
it('should not consider IPv6 hostnames with ports', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:[::1]:8448",
powerLevel: 100,
@@ -276,7 +276,7 @@ describe('matrix-to', function() {
});
it('should work with hostnames with ports', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:example.org:8448",
powerLevel: 100,
@@ -291,7 +291,7 @@ describe('matrix-to', function() {
});
it('should not consider servers explicitly denied by ACLs', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:evilcorp.com",
powerLevel: 100,
@@ -311,7 +311,7 @@ describe('matrix-to', function() {
});
it('should not consider servers not allowed by ACLs', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:evilcorp.com",
powerLevel: 100,
@@ -331,7 +331,7 @@ describe('matrix-to', function() {
});
it('should consider servers not explicitly banned by ACLs', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:evilcorp.com",
powerLevel: 100,
@@ -352,7 +352,7 @@ describe('matrix-to', function() {
});
it('should consider servers not disallowed by ACLs', function() {
- const room = mockRoom(null, [
+ const room = mockRoom("!fake:example.org", [
{
userId: "@alice:evilcorp.com",
powerLevel: 100,