From ff39d52431203956cebc82078774504e97c0f180 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 16 Aug 2016 03:31:02 +0530 Subject: [PATCH 01/33] add fancy changelog dialog --- src/components/structures/MatrixChat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8d84cbf7d0..46cad9685b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -94,6 +94,9 @@ module.exports = React.createClass({ width: 10000, sideOpacity: 1.0, middleOpacity: 1.0, + + version: null, + newVersion: null, }; return s; }, @@ -856,6 +859,7 @@ module.exports = React.createClass({ onVersion: function(current, latest) { this.setState({ version: current, + newVersion: latest, hasNewVersion: current !== latest }); }, @@ -992,7 +996,7 @@ module.exports = React.createClass({ var topBar; if (this.state.hasNewVersion) { - topBar = ; + topBar = ; } else if (MatrixClientPeg.get().isGuest()) { topBar = ; From e1739008089fdc97d4c21fb7039382764f2e92b2 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 17 Aug 2016 17:27:19 +0530 Subject: [PATCH 02/33] Update autocomplete design and scroll it correctly --- src/autocomplete/AutocompleteProvider.js | 9 +++ src/autocomplete/CommandProvider.js | 8 ++- src/autocomplete/Components.js | 75 +++++++++++++++++----- src/autocomplete/DuckDuckGoProvider.js | 2 +- src/autocomplete/EmojiProvider.js | 24 ++++--- src/autocomplete/RoomProvider.js | 15 ++++- src/autocomplete/UserProvider.js | 16 ++++- src/components/views/rooms/Autocomplete.js | 55 ++++++++-------- 8 files changed, 146 insertions(+), 58 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 41d5d035d1..e3332d014e 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,4 +1,5 @@ import Q from 'q'; +import React from 'react'; export default class AutocompleteProvider { constructor(commandRegex?: RegExp, fuseOpts?: any) { @@ -51,4 +52,12 @@ export default class AutocompleteProvider { getName(): string { return 'Default Provider'; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return ( +
+ {completions} +
+ ); + } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 19a366ac63..8fb7a75aed 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -74,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider { } getName() { - return 'Commands'; + return '*️⃣ Commands'; } static getInstance(): CommandProvider { @@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider { return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_block', + }); + } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 168da00c1c..f0dbc64d65 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -1,19 +1,62 @@ import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; -export function TextualCompletion({ - title, - subtitle, - description, -}: { - title: ?string, - subtitle: ?string, - description: ?string -}) { - return ( -
- {title} - {subtitle} - {description} -
- ); +/* These were earlier stateless functional components but had to be converted +since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion, +something that is not entirely possible with stateless functional components. One could +presumably wrap them in a
before rendering but I think this is the better way to do it. + */ + +export class TextualCompletion extends React.Component { + render() { + const { + title, + subtitle, + description, + className, + ...restProps, + } = this.props; + return ( +
+ {title} + {subtitle} + {description} +
+ ); + } } +TextualCompletion.propTypes = { + title: React.PropTypes.string, + subtitle: React.PropTypes.string, + description: React.PropTypes.string, + className: React.PropTypes.string, +}; + +export class PillCompletion extends React.Component { + render() { + const { + title, + subtitle, + description, + initialComponent, + className, + ...restProps, + } = this.props; + return ( +
+ {initialComponent} + {title} + {subtitle} + {description} +
+ ); + } +} +PillCompletion.propTypes = { + title: React.PropTypes.string, + subtitle: React.PropTypes.string, + description: React.PropTypes.string, + initialComponent: React.PropTypes.element, + className: React.PropTypes.string, +}; diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 1746ce0aaa..c74ffa0473 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -78,7 +78,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } getName() { - return 'Results from DuckDuckGo'; + return '🔍 Results from DuckDuckGo'; } static getInstance(): DuckDuckGoProvider { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 37a50ee8d8..8763d90749 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -3,6 +3,8 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; +import sdk from '../index'; +import {PillCompletion} from './Components'; const EMOJI_REGEX = /:\w*:?/g; const EMOJI_SHORTNAMES = Object.keys(emojioneList); @@ -16,28 +18,28 @@ export default class EmojiProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const EmojiText = sdk.getComponent('views.elements.EmojiText'); + let completions = []; let {command, range} = this.getCurrentCommand(query, selection); if (command) { completions = this.fuse.search(command[0]).map(result => { - let shortname = EMOJI_SHORTNAMES[result]; - let imageHTML = shortnameToImage(shortname); + const shortname = EMOJI_SHORTNAMES[result]; + const unicode = shortnameToUnicode(shortname); return { - completion: shortnameToUnicode(shortname), + completion: unicode, component: ( -
-   {shortname} -
+ {unicode}} /> ), range, }; - }).slice(0, 4); + }).slice(0, 8); } return Q.when(completions); } getName() { - return 'Emoji'; + return '😃 Emoji'; } static getInstance() { @@ -45,4 +47,10 @@ export default class EmojiProvider extends AutocompleteProvider { instance = new EmojiProvider(); return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b34fdeb59a..f27d450266 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -3,8 +3,9 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; -import {TextualCompletion} from './Components'; +import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../MatrixTools'; +import sdk from '../index'; const ROOM_REGEX = /(?=#)([^\s]*)/g; @@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + let client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection); @@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider { return { completion: displayAlias, component: ( - + } title={room.name} description={displayAlias} /> ), range, }; @@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return 'Rooms'; + return '💬 Rooms'; } static getInstance() { @@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider { return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 8828f8cb70..e772d62b23 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,7 +2,8 @@ import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; -import {TextualCompletion} from './Components'; +import {PillCompletion} from './Components'; +import sdk from '../index'; const USER_REGEX = /@[^\s]*/g; @@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + let completions = []; let {command, range} = this.getCurrentCommand(query, selection); if (command) { @@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider { return { completion: user.userId, component: ( - } title={displayName} description={user.userId} /> ), @@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider { } getName() { - return 'Users'; + return '👥 Users'; } setUserList(users) { @@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider { } return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 32e568e2ba..1f62ce852c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,7 +1,8 @@ import React from 'react'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import ReactDOM from 'react-dom'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; +import sdk from '../../../index'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -100,11 +101,26 @@ export default class Autocomplete extends React.Component { this.setState({selectionOffset}); } + componentDidUpdate() { + // this is the selected completion, so scroll it into view if needed + const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`]; + if (selectedCompletion && this.container) { + const {offsetTop} = ReactDOM.findDOMNode(selectedCompletion); + if (offsetTop > this.container.scrollTop + this.container.offsetHeight || + offsetTop < this.container.scrollTop) { + this.container.scrollTop = offsetTop - this.container.offsetTop; + } + } + } + render() { + const EmojiText = sdk.getComponent('views.elements.EmojiText'); + let position = 0; let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { - let className = classNames('mx_Autocomplete_Completion', { + + const className = classNames('mx_Autocomplete_Completion', { 'selected': position === this.state.selectionOffset, }); let componentPosition = position; @@ -116,40 +132,27 @@ export default class Autocomplete extends React.Component { this.onConfirm(); }; - return ( -
- {completion.component} -
- ); + return React.cloneElement(completion.component, { + key: i, + ref: `completion${i}`, + className, + onMouseOver, + onClick, + }); }); return completions.length > 0 ? (
- {completionResult.provider.getName()} - - {completions} - + {completionResult.provider.getName()} + {completionResult.provider.renderCompletions(completions)}
) : null; }); return ( -
- - {renderedCompletions} - +
this.container = e}> + {renderedCompletions}
); } From 884abbd7e9f48b47f7da0f81b7dc4c37b51cb251 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 23 Aug 2016 00:36:31 +0530 Subject: [PATCH 03/33] Cleanup autocomplete --- src/autocomplete/AutocompleteProvider.js | 7 ++----- src/autocomplete/CommandProvider.js | 6 +++--- src/autocomplete/Components.js | 12 ++++++------ src/autocomplete/DuckDuckGoProvider.js | 6 ++++++ src/autocomplete/EmojiProvider.js | 6 +++--- src/autocomplete/RoomProvider.js | 6 +++--- src/autocomplete/UserProvider.js | 6 +++--- src/components/views/rooms/Autocomplete.js | 3 ++- 8 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index e3332d014e..87d7987856 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -54,10 +54,7 @@ export default class AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return ( -
- {completions} -
- ); + console.error('stub; should be implemented in subclasses'); + return null; } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 8fb7a75aed..4652e69ddf 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -85,8 +85,8 @@ export default class CommandProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return React.cloneElement(super.renderCompletions(completions), { - className: 'mx_Autocomplete_Completion_container_block', - }); + return
+ {completions} +
; } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index f0dbc64d65..4595f7456d 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -19,9 +19,9 @@ export class TextualCompletion extends React.Component { } = this.props; return (
- {title} - {subtitle} - {description} + {title} + {subtitle} + {description}
); } @@ -46,9 +46,9 @@ export class PillCompletion extends React.Component { return (
{initialComponent} - {title} - {subtitle} - {description} + {title} + {subtitle} + {description}
); } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index c74ffa0473..c85eb8a10b 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -87,4 +87,10 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return
+ {completions} +
; + } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 8763d90749..e292808787 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -49,8 +49,8 @@ export default class EmojiProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return React.cloneElement(super.renderCompletions(completions), { - className: 'mx_Autocomplete_Completion_container_pill', - }); + return
+ {completions} +
; } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index f27d450266..39cf1179d7 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -64,8 +64,8 @@ export default class RoomProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return React.cloneElement(super.renderCompletions(completions), { - className: 'mx_Autocomplete_Completion_container_pill', - }); + return
+ {completions} +
; } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index e772d62b23..7485d76484 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -60,8 +60,8 @@ export default class UserProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return React.cloneElement(super.renderCompletions(completions), { - className: 'mx_Autocomplete_Completion_container_pill', - }); + return
+ {completions} +
; } } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 1f62ce852c..4b2e23a8b8 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -105,7 +105,8 @@ export default class Autocomplete extends React.Component { // this is the selected completion, so scroll it into view if needed const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`]; if (selectedCompletion && this.container) { - const {offsetTop} = ReactDOM.findDOMNode(selectedCompletion); + const domNode = ReactDOM.findDOMNode(selectedCompletion); + const offsetTop = domNode && domNode.offsetTop; if (offsetTop > this.container.scrollTop + this.container.offsetHeight || offsetTop < this.container.scrollTop) { this.container.scrollTop = offsetTop - this.container.offsetTop; From 9162a0ff012bd01e131b4ce47650f747e7307e20 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Aug 2016 12:00:11 +0100 Subject: [PATCH 04/33] Add postMessage API required for integration provisioning Supports querying member state and creating invites only. --- src/ScalarMessaging.js | 171 +++++++++++++++++++++ src/components/views/rooms/RoomSettings.js | 4 + 2 files changed, 175 insertions(+) create mode 100644 src/ScalarMessaging.js diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js new file mode 100644 index 0000000000..7033e6b6a5 --- /dev/null +++ b/src/ScalarMessaging.js @@ -0,0 +1,171 @@ +/* +Copyright 2016 OpenMarket Ltd + +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. +*/ + +/* +Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed: +{ + action: "invite" | "membership_state", + room_id: $ROOM_ID, + user_id: $USER_ID +} + +The complete request object is returned to the caller with an additional "response" key like so: +{ + action: "invite" | "membership_state", + room_id: $ROOM_ID, + user_id: $USER_ID, + response: { ... } +} + +"response" objects can consist of a sole "error" key to indicate an error. These look like: +{ + error: { + message: "Unable to invite user into room.", + _error: + } +} +The "message" key should be a human-friendly string. + +The response object for "membership_state" looks like: +{ + membership_state: "join" | "leave" | "invite" | "ban" +} + +The response object for "invite" looks like: +{ + invite: true +} + +*/ + +const SdkConfig = require('./SdkConfig'); +const MatrixClientPeg = require("./MatrixClientPeg"); + +const onMessage = function(event) { + if (!event.origin) { // stupid chrome + event.origin = event.originalEvent.origin; + } + + // check it is from the integrations UI URL (remove trailing spaces) + let url = SdkConfig.get().integrations_ui_url; + if (url.endsWith("/")) { + url = url.substr(0, url.length - 1); + } + if (url !== event.origin) { + console.warn("Unauthorised postMessage received. Source URL: " + event.origin); + return; + } + + switch (event.data.action) { + case "membership_state": { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + return sendError(event, "Missing user_id in request"); + } + if (!roomId) { + return sendError(event, "Missing room_id in request"); + } + console.log(`membership_state of ${userId} in room ${roomId} requested.`); + const client = MatrixClientPeg.get(); + if (!client) { + return sendError(event, "You need to be logged in."); + } + const room = client.getRoom(roomId); + if (!room) { + return sendError(event, "This room is not recognised."); + } + let membershipState = "leave"; + const member = room.getMember(userId); + if (member) { + membershipState = member.membership; + } + sendResponse(event, { + membership_state: membershipState + }); + } + break; + case "invite": { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + return sendError(event, "Missing user_id in request"); + } + if (!roomId) { + return sendError(event, "Missing room_id in request"); + } + console.log(`Received request to invite ${userId} into room ${roomId}`); + const client = MatrixClientPeg.get(); + if (!client) { + return sendError(event, "You need to be logged in."); + } + const room = client.getRoom(roomId); + if (room) { + // if they are already invited we can resolve immediately. + const member = room.getMember(userId); + if (member && member.membership === "invite") { + sendResponse(event, { + invite: true + }); + return; + } + } + + client.invite(roomId, userId).then(function() { + sendResponse(event, { + invite: true + }); + }, function(err) { + sendError(event, "You need to be able to invite users to do that.", err); + }); + } + break; + default: + console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); + break; + } +}; + +function sendResponse(event, res) { + console.log("Action:" + event.data.action + " succeeded with response: " + JSON.stringify(res)); + const data = event.data; + data.response = res; + event.source.postMessage(data, event.origin) +} + +function sendError(event, msg, nestedError) { + console.error("Action:" + event.data.action + " failed with message: " + msg); + const data = event.data; + data.response = { + error: { + message: msg, + } + }; + if (nestedError) { + data.response.error._error = nestedError; + } + event.source.postMessage(data, event.origin); +} + +module.exports = { + startListening: function() { + window.addEventListener("message", onMessage, false); + }, + + stopListening: function() { + window.removeEventListener("message", onMessage); + } +}; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index abc90ae486..39d67f5b48 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -23,6 +23,7 @@ var Modal = require('../../../Modal'); var ObjectUtils = require("../../../ObjectUtils"); var dis = require("../../../dispatcher"); var ScalarAuthClient = require("../../../ScalarAuthClient"); +var ScalarMessaging = require('../../../ScalarMessaging'); var UserSettingsStore = require('../../../UserSettingsStore'); // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -81,6 +82,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { + ScalarMessaging.startListening(); MatrixClientPeg.get().getRoomDirectoryVisibility( this.props.room.roomId ).done((result) => { @@ -104,6 +106,8 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { + ScalarMessaging.stopListening(); + dis.dispatch({ action: 'ui_opacity', sideOpacity: 1.0, From f6b008350dcbcf8576e1172f855459735b5ac00c Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Aug 2016 13:31:55 +0100 Subject: [PATCH 05/33] Spaces not tabs --- src/ScalarMessaging.js | 226 ++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 7033e6b6a5..f2983acaeb 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -17,36 +17,36 @@ limitations under the License. /* Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed: { - action: "invite" | "membership_state", - room_id: $ROOM_ID, - user_id: $USER_ID + action: "invite" | "membership_state", + room_id: $ROOM_ID, + user_id: $USER_ID } The complete request object is returned to the caller with an additional "response" key like so: { - action: "invite" | "membership_state", - room_id: $ROOM_ID, - user_id: $USER_ID, - response: { ... } + action: "invite" | "membership_state", + room_id: $ROOM_ID, + user_id: $USER_ID, + response: { ... } } "response" objects can consist of a sole "error" key to indicate an error. These look like: { - error: { - message: "Unable to invite user into room.", - _error: - } + error: { + message: "Unable to invite user into room.", + _error: + } } The "message" key should be a human-friendly string. The response object for "membership_state" looks like: { - membership_state: "join" | "leave" | "invite" | "ban" + membership_state: "join" | "leave" | "invite" | "ban" } The response object for "invite" looks like: { - invite: true + invite: true } */ @@ -55,117 +55,117 @@ const SdkConfig = require('./SdkConfig'); const MatrixClientPeg = require("./MatrixClientPeg"); const onMessage = function(event) { - if (!event.origin) { // stupid chrome - event.origin = event.originalEvent.origin; - } + if (!event.origin) { // stupid chrome + event.origin = event.originalEvent.origin; + } - // check it is from the integrations UI URL (remove trailing spaces) - let url = SdkConfig.get().integrations_ui_url; - if (url.endsWith("/")) { - url = url.substr(0, url.length - 1); - } - if (url !== event.origin) { - console.warn("Unauthorised postMessage received. Source URL: " + event.origin); - return; - } + // check it is from the integrations UI URL (remove trailing spaces) + let url = SdkConfig.get().integrations_ui_url; + if (url.endsWith("/")) { + url = url.substr(0, url.length - 1); + } + if (url !== event.origin) { + console.warn("Unauthorised postMessage received. Source URL: " + event.origin); + return; + } - switch (event.data.action) { - case "membership_state": { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - return sendError(event, "Missing user_id in request"); - } - if (!roomId) { - return sendError(event, "Missing room_id in request"); - } - console.log(`membership_state of ${userId} in room ${roomId} requested.`); - const client = MatrixClientPeg.get(); - if (!client) { - return sendError(event, "You need to be logged in."); - } - const room = client.getRoom(roomId); - if (!room) { - return sendError(event, "This room is not recognised."); - } - let membershipState = "leave"; - const member = room.getMember(userId); - if (member) { - membershipState = member.membership; - } - sendResponse(event, { - membership_state: membershipState - }); - } - break; - case "invite": { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - return sendError(event, "Missing user_id in request"); - } - if (!roomId) { - return sendError(event, "Missing room_id in request"); - } - console.log(`Received request to invite ${userId} into room ${roomId}`); - const client = MatrixClientPeg.get(); - if (!client) { - return sendError(event, "You need to be logged in."); - } - const room = client.getRoom(roomId); - if (room) { - // if they are already invited we can resolve immediately. - const member = room.getMember(userId); - if (member && member.membership === "invite") { - sendResponse(event, { - invite: true - }); - return; - } - } + switch (event.data.action) { + case "membership_state": { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + return sendError(event, "Missing user_id in request"); + } + if (!roomId) { + return sendError(event, "Missing room_id in request"); + } + console.log(`membership_state of ${userId} in room ${roomId} requested.`); + const client = MatrixClientPeg.get(); + if (!client) { + return sendError(event, "You need to be logged in."); + } + const room = client.getRoom(roomId); + if (!room) { + return sendError(event, "This room is not recognised."); + } + let membershipState = "leave"; + const member = room.getMember(userId); + if (member) { + membershipState = member.membership; + } + sendResponse(event, { + membership_state: membershipState + }); + } + break; + case "invite": { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + return sendError(event, "Missing user_id in request"); + } + if (!roomId) { + return sendError(event, "Missing room_id in request"); + } + console.log(`Received request to invite ${userId} into room ${roomId}`); + const client = MatrixClientPeg.get(); + if (!client) { + return sendError(event, "You need to be logged in."); + } + const room = client.getRoom(roomId); + if (room) { + // if they are already invited we can resolve immediately. + const member = room.getMember(userId); + if (member && member.membership === "invite") { + sendResponse(event, { + invite: true + }); + return; + } + } - client.invite(roomId, userId).then(function() { - sendResponse(event, { - invite: true - }); - }, function(err) { - sendError(event, "You need to be able to invite users to do that.", err); - }); - } - break; - default: - console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); - break; - } + client.invite(roomId, userId).then(function() { + sendResponse(event, { + invite: true + }); + }, function(err) { + sendError(event, "You need to be able to invite users to do that.", err); + }); + } + break; + default: + console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); + break; + } }; function sendResponse(event, res) { - console.log("Action:" + event.data.action + " succeeded with response: " + JSON.stringify(res)); - const data = event.data; - data.response = res; - event.source.postMessage(data, event.origin) + console.log("Action:" + event.data.action + " succeeded with response: " + JSON.stringify(res)); + const data = event.data; + data.response = res; + event.source.postMessage(data, event.origin) } function sendError(event, msg, nestedError) { - console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = event.data; - data.response = { - error: { - message: msg, - } - }; - if (nestedError) { - data.response.error._error = nestedError; - } - event.source.postMessage(data, event.origin); + console.error("Action:" + event.data.action + " failed with message: " + msg); + const data = event.data; + data.response = { + error: { + message: msg, + } + }; + if (nestedError) { + data.response.error._error = nestedError; + } + event.source.postMessage(data, event.origin); } module.exports = { - startListening: function() { - window.addEventListener("message", onMessage, false); - }, + startListening: function() { + window.addEventListener("message", onMessage, false); + }, - stopListening: function() { - window.removeEventListener("message", onMessage); - } + stopListening: function() { + window.removeEventListener("message", onMessage); + } }; From 70c0290aece2eced622effe72c1a7e2a256df604 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 23 Aug 2016 14:39:44 +0100 Subject: [PATCH 06/33] Fix TimelinePanel test https://github.com/matrix-org/matrix-react-sdk/pull/422 made events take up less vertical space, so we now need more of them to allow us to scroll back. --- test/components/structures/TimelinePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index 7a603d138f..6acda0e6ac 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -87,7 +87,7 @@ describe('TimelinePanel', function() { // this is https://github.com/vector-im/vector-web/issues/1367 // enough events to allow us to scroll back - var N_EVENTS = 20; + var N_EVENTS = 30; for (var i = 0; i < N_EVENTS; i++) { timeline.addEvent(mkMessage()); } From e96a40004b7634003d43a57c7e797baee4e46ed6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Aug 2016 14:41:47 +0100 Subject: [PATCH 07/33] Review comments --- src/ScalarMessaging.js | 178 ++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 83 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index f2983acaeb..3f31cad19e 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -54,6 +54,96 @@ The response object for "invite" looks like: const SdkConfig = require('./SdkConfig'); const MatrixClientPeg = require("./MatrixClientPeg"); +function sendResponse(event, res) { + const data = JSON.parse(JSON.stringify(event.data)); + data.response = res; + event.source.postMessage(data, event.origin); +} + +function sendError(event, msg, nestedError) { + console.error("Action:" + event.data.action + " failed with message: " + msg); + const data = event.data; + data.response = { + error: { + message: msg, + }, + }; + if (nestedError) { + data.response.error._error = nestedError; + } + event.source.postMessage(data, event.origin); +} + +function inviteUser(event) { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + sendError(event, "Missing user_id in request"); + return; + } + if (!roomId) { + sendError(event, "Missing room_id in request"); + return; + } + console.log(`Received request to invite ${userId} into room ${roomId}`); + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, "You need to be logged in."); + return; + } + const room = client.getRoom(roomId); + if (room) { + // if they are already invited we can resolve immediately. + const member = room.getMember(userId); + if (member && member.membership === "invite") { + sendResponse(event, { + invite: true, + }); + return; + } + } + + client.invite(roomId, userId).then(function() { + sendResponse(event, { + invite: true, + }); + }, function(err) { + sendError(event, "You need to be able to invite users to do that.", err); + }); +} + +function getMembershipState(event) { + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + sendError(event, "Missing user_id in request"); + return; + } + if (!roomId) { + sendError(event, "Missing room_id in request"); + return; + } + console.log(`membership_state of ${userId} in room ${roomId} requested.`); + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, "You need to be logged in."); + return; + } + const room = client.getRoom(roomId); + if (!room) { + sendError(event, "This room is not recognised."); + return; + } + let membershipState = "leave"; + const member = room.getMember(userId); + if (member) { + membershipState = member.membership; + } + sendResponse(event, { + membership_state: membershipState, + }); +} + const onMessage = function(event) { if (!event.origin) { // stupid chrome event.origin = event.originalEvent.origin; @@ -70,68 +160,11 @@ const onMessage = function(event) { } switch (event.data.action) { - case "membership_state": { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - return sendError(event, "Missing user_id in request"); - } - if (!roomId) { - return sendError(event, "Missing room_id in request"); - } - console.log(`membership_state of ${userId} in room ${roomId} requested.`); - const client = MatrixClientPeg.get(); - if (!client) { - return sendError(event, "You need to be logged in."); - } - const room = client.getRoom(roomId); - if (!room) { - return sendError(event, "This room is not recognised."); - } - let membershipState = "leave"; - const member = room.getMember(userId); - if (member) { - membershipState = member.membership; - } - sendResponse(event, { - membership_state: membershipState - }); - } + case "membership_state": + getMembershipState(event); break; - case "invite": { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - return sendError(event, "Missing user_id in request"); - } - if (!roomId) { - return sendError(event, "Missing room_id in request"); - } - console.log(`Received request to invite ${userId} into room ${roomId}`); - const client = MatrixClientPeg.get(); - if (!client) { - return sendError(event, "You need to be logged in."); - } - const room = client.getRoom(roomId); - if (room) { - // if they are already invited we can resolve immediately. - const member = room.getMember(userId); - if (member && member.membership === "invite") { - sendResponse(event, { - invite: true - }); - return; - } - } - - client.invite(roomId, userId).then(function() { - sendResponse(event, { - invite: true - }); - }, function(err) { - sendError(event, "You need to be able to invite users to do that.", err); - }); - } + case "invite": + inviteUser(event); break; default: console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); @@ -139,27 +172,6 @@ const onMessage = function(event) { } }; -function sendResponse(event, res) { - console.log("Action:" + event.data.action + " succeeded with response: " + JSON.stringify(res)); - const data = event.data; - data.response = res; - event.source.postMessage(data, event.origin) -} - -function sendError(event, msg, nestedError) { - console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = event.data; - data.response = { - error: { - message: msg, - } - }; - if (nestedError) { - data.response.error._error = nestedError; - } - event.source.postMessage(data, event.origin); -} - module.exports = { startListening: function() { window.addEventListener("message", onMessage, false); @@ -167,5 +179,5 @@ module.exports = { stopListening: function() { window.removeEventListener("message", onMessage); - } + }, }; From 1c649303e347ae31c1b6ad720007f2fb288f2d42 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Aug 2016 14:50:29 +0100 Subject: [PATCH 08/33] Consistency with sendResponse --- src/ScalarMessaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 3f31cad19e..22b2b9a6d1 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -62,7 +62,7 @@ function sendResponse(event, res) { function sendError(event, msg, nestedError) { console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = event.data; + const data = JSON.parse(JSON.stringify(event.data); data.response = { error: { message: msg, From ecc7850e1372789d618376701ce946cdbbde9d21 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 23 Aug 2016 14:50:52 +0100 Subject: [PATCH 09/33] Trailing ) --- src/ScalarMessaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 22b2b9a6d1..da1ef7edfc 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -62,7 +62,7 @@ function sendResponse(event, res) { function sendError(event, msg, nestedError) { console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = JSON.parse(JSON.stringify(event.data); + const data = JSON.parse(JSON.stringify(event.data)); data.response = { error: { message: msg, From 763d3bf9e7bf917a5f1d05308a5b6d8728d8491b Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 23 Aug 2016 15:05:50 +0100 Subject: [PATCH 10/33] Add .travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..9d6a114391 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - node # Latest stable version of nodejs. From ae8234ffe3e9c9f9d79c4fe476eec9562b9aa378 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 23 Aug 2016 15:58:27 +0100 Subject: [PATCH 11/33] Added event/info message avatars back in --- src/components/views/rooms/EventTile.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index bc0317addd..b33860b7b3 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -403,7 +403,13 @@ module.exports = React.createClass({ var readAvatars = this.getReadAvatars(); var avatar, sender; - if (!this.props.continuation && !isInfoMessage) { + if (isInfoMessage) { + avatar = ( +
+ +
+ ); + } else if (!this.props.continuation) { if (this.props.mxEvent.sender) { avatar = (
From dbff5f671e679d9094c5c3ac7330d6367a052eae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 24 Aug 2016 13:23:06 +0100 Subject: [PATCH 12/33] Add ability to query and set bot options - Also standardise on 'success' as the key name for successes. - Also return the entire `content` on get state event requests. --- src/ScalarMessaging.js | 165 +++++++++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 38 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index da1ef7edfc..afe52a34cc 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -17,20 +17,24 @@ limitations under the License. /* Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed: { - action: "invite" | "membership_state", + action: "invite" | "membership_state" | "bot_options" | "set_bot_options", room_id: $ROOM_ID, user_id: $USER_ID + // additional request fields } The complete request object is returned to the caller with an additional "response" key like so: { - action: "invite" | "membership_state", + action: "invite" | "membership_state" | "bot_options" | "set_bot_options", room_id: $ROOM_ID, user_id: $USER_ID, + // additional request fields response: { ... } } -"response" objects can consist of a sole "error" key to indicate an error. These look like: +The "action" determines the format of the request and response. All actions can return an error response. +An error response is a "response" object which consists of a sole "error" key to indicate an error. +They look like: { error: { message: "Unable to invite user into room.", @@ -39,16 +43,82 @@ The complete request object is returned to the caller with an additional "respon } The "message" key should be a human-friendly string. -The response object for "membership_state" looks like: +ACTIONS +======= +All actions can return an error response instead of the response outlined below. + +invite +------ +Invites a user into a room. + +Request: + - room_id is the room to invite the user into. + - user_id is the user ID to invite. + - No additional fields. +Response: { - membership_state: "join" | "leave" | "invite" | "ban" + success: true +} +Example: +{ + action: "invite", + room_id: "!foo:bar", + user_id: "@invitee:bar", + response: { + success: true + } } -The response object for "invite" looks like: +set_bot_options +--------------- +Set the m.room.bot.options state event for a bot user. + +Request: + - room_id is the room to send the state event into. + - user_id is the user ID of the bot who you're setting options for. + - "content" is an object consisting of the content you wish to set. +Response: { - invite: true + success: true +} +Example: +{ + action: "set_bot_options", + room_id: "!foo:bar", + user_id: "@bot:bar", + content: { + default_option: "alpha" + }, + response: { + success: true + } } +membership_state AND bot_options +-------------------------------- +Get the content of the "m.room.member" or "m.room.bot.options" state event respectively. + +NB: Whilst this API is basically equivalent to getStateEvent, we specifically do not + want external entities to be able to query any state event for any room, hence the + restrictive API outlined here. + +Request: + - room_id is the room which has the state event. + - user_id is the state_key parameter which in both cases is a user ID (the member or the bot). + - No additional fields. +Response: + - The event content. If there is no state event, the "response" key should be null. +Example: +{ + action: "membership_state", + room_id: "!foo:bar", + user_id: "@somemember:bar", + response: { + membership: "join", + displayname: "Bob", + avatar_url: null + } +} */ const SdkConfig = require('./SdkConfig'); @@ -74,17 +144,7 @@ function sendError(event, msg, nestedError) { event.source.postMessage(data, event.origin); } -function inviteUser(event) { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - sendError(event, "Missing user_id in request"); - return; - } - if (!roomId) { - sendError(event, "Missing room_id in request"); - return; - } +function inviteUser(event, roomId, userId) { console.log(`Received request to invite ${userId} into room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { @@ -97,7 +157,7 @@ function inviteUser(event) { const member = room.getMember(userId); if (member && member.membership === "invite") { sendResponse(event, { - invite: true, + success: true, }); return; } @@ -105,25 +165,41 @@ function inviteUser(event) { client.invite(roomId, userId).then(function() { sendResponse(event, { - invite: true, + success: true, }); }, function(err) { sendError(event, "You need to be able to invite users to do that.", err); }); } -function getMembershipState(event) { - const roomId = event.data.room_id; - const userId = event.data.user_id; - if (!userId) { - sendError(event, "Missing user_id in request"); - return; - } - if (!roomId) { - sendError(event, "Missing room_id in request"); +function setBotOptions(event, roomId, userId) { + console.log(`Received request to set options for bot ${userId} in room ${roomId}`); + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, "You need to be logged in."); return; } + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, userId).then(() => { + sendResponse(event, { + success: true, + }); + }, (err) => { + sendError(event, "Failed to send request.", err); + }); +} + +function getMembershipState(event, roomId, userId) { console.log(`membership_state of ${userId} in room ${roomId} requested.`); + returnStateEvent(event, roomId, "m.room.member", userId); +} + +function botOptions(event, roomId, userId) { + console.log(`bot_options of ${userId} in room ${roomId} requested.`); + returnStateEvent(event, roomId, "m.room.bot.options", userId); +} + + +function returnStateEvent(event, roomId, eventType, stateKey) { const client = MatrixClientPeg.get(); if (!client) { sendError(event, "You need to be logged in."); @@ -134,14 +210,11 @@ function getMembershipState(event) { sendError(event, "This room is not recognised."); return; } - let membershipState = "leave"; - const member = room.getMember(userId); - if (member) { - membershipState = member.membership; + const stateEvent = room.currentState.getStateEvents(eventType, stateKey); + if (!stateEvent) { + sendResponse(event, null); } - sendResponse(event, { - membership_state: membershipState, - }); + sendResponse(event, stateEvent.getContent()); } const onMessage = function(event) { @@ -159,12 +232,28 @@ const onMessage = function(event) { return; } + const roomId = event.data.room_id; + const userId = event.data.user_id; + if (!userId) { + sendError(event, "Missing user_id in request"); + return; + } + if (!roomId) { + sendError(event, "Missing room_id in request"); + return; + } switch (event.data.action) { case "membership_state": - getMembershipState(event); + getMembershipState(event, roomId, userId); break; case "invite": - inviteUser(event); + inviteUser(event, roomId, userId); + break; + case "bot_options": + botOptions(event, roomId, userId); + break; + case "set_bot_options": + setBotOptions(event, roomId, userId); break; default: console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); From 73ba55abadde2893ed2986275821603864cb0fe0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 24 Aug 2016 13:47:35 +0100 Subject: [PATCH 13/33] s/then/done/ to throw uncaught exceptions if the error function throws --- src/ScalarMessaging.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index afe52a34cc..04686cff9c 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -163,7 +163,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).then(function() { + client.invite(roomId, userId).done(function() { sendResponse(event, { success: true, }); @@ -179,7 +179,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, "You need to be logged in."); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, userId).then(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, userId).done(() => { sendResponse(event, { success: true, }); From 6235d88b553057d9ec44ea697eeb7486ae1cf58c Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 24 Aug 2016 14:10:21 +0100 Subject: [PATCH 14/33] Missing return --- src/ScalarMessaging.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 04686cff9c..3e370b546f 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -213,6 +213,7 @@ function returnStateEvent(event, roomId, eventType, stateKey) { const stateEvent = room.currentState.getStateEvents(eventType, stateKey); if (!stateEvent) { sendResponse(event, null); + return; } sendResponse(event, stateEvent.getContent()); } From c292312115ea580553f46977e3b8570afcfea6cb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 24 Aug 2016 14:54:44 +0100 Subject: [PATCH 15/33] Send bot options with leading underscore on the state key This is in order to get around the synapse rule which states that if the state key is a user ID, then only that user can send the event. We want to set the event for another user (the bot), hence we can't send just the plain user ID. --- src/ScalarMessaging.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 3e370b546f..cc552ba898 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -179,12 +179,12 @@ function setBotOptions(event, roomId, userId) { sendError(event, "You need to be logged in."); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { sendResponse(event, { success: true, }); }, (err) => { - sendError(event, "Failed to send request.", err); + sendError(event, err.message ? err.message : "Failed to send request.", err); }); } @@ -195,7 +195,7 @@ function getMembershipState(event, roomId, userId) { function botOptions(event, roomId, userId) { console.log(`bot_options of ${userId} in room ${roomId} requested.`); - returnStateEvent(event, roomId, "m.room.bot.options", userId); + returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId); } From cdd2902b013ba3027761b87ffb0ee69417231151 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 24 Aug 2016 15:48:19 +0100 Subject: [PATCH 16/33] Update annoying TimelinePanel test * Actually test what we were supposed to be testing (viz, that we can paginate back down after we hit the top of the room) * Make the cap on the number of events we show in the timeline a configurable property, so that we can set it in the test * Use a smaller cap in the test, so that we have to do less paginating to hit the cap, to make the test run quicker. * add some more logging so that we can see how far it's got if it gets stuck. --- src/components/structures/TimelinePanel.js | 12 ++++- .../structures/TimelinePanel-test.js | 47 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 798ee03e73..c80a8c89d7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -31,7 +31,6 @@ var KeyCode = require('../../KeyCode'); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 20; -var TIMELINE_CAP = 250; // the most events to show in a timeline var DEBUG = false; @@ -82,6 +81,9 @@ var TimelinePanel = React.createClass({ // opacity for dynamic UI fading effects opacity: React.PropTypes.number, + + // maximum number of events to show in a timeline + timelineCap: React.PropTypes.number, }, statics: { @@ -92,6 +94,12 @@ var TimelinePanel = React.createClass({ roomReadMarkerTsMap: {}, }, + getDefaultProps: function() { + return { + timelineCap: 250, + }; + }, + getInitialState: function() { var initialReadMarker = TimelinePanel.roomReadMarkerMap[this.props.room.roomId] @@ -684,7 +692,7 @@ var TimelinePanel = React.createClass({ _loadTimeline: function(eventId, pixelOffset, offsetBase) { this._timelineWindow = new Matrix.TimelineWindow( MatrixClientPeg.get(), this.props.room, - {windowLimit: TIMELINE_CAP}); + {windowLimit: this.props.timelineCap}); var onLoaded = () => { this._reloadEvents(); diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index 6acda0e6ac..fb82404616 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -40,11 +40,12 @@ describe('TimelinePanel', function() { var timeline; var parentDiv; - function mkMessage() { + function mkMessage(opts) { return test_utils.mkMessage( { event: true, room: ROOM_ID, user: USER_ID, ts: Date.now(), + ... opts, }); } @@ -207,10 +208,11 @@ describe('TimelinePanel', function() { }); it("should let you scroll down again after you've scrolled up", function(done) { - var N_EVENTS = 600; + var TIMELINE_CAP = 100; // needs to be more than we can fit in the div + var N_EVENTS = 120; // needs to be more than TIMELINE_CAP // sadly, loading all those events takes a while - this.timeout(N_EVENTS * 30); + this.timeout(N_EVENTS * 50); // client.getRoom is called a /lot/ in this test, so replace // sinon's spy with a fast noop. @@ -218,13 +220,15 @@ describe('TimelinePanel', function() { // fill the timeline with lots of events for (var i = 0; i < N_EVENTS; i++) { - timeline.addEvent(mkMessage()); + timeline.addEvent(mkMessage({msg: ""+i})); } console.log("added events to timeline"); var scrollDefer; var panel = ReactDOM.render( - {scrollDefer.resolve()}} />, + {scrollDefer.resolve()}} + timelineCap={TIMELINE_CAP} + />, parentDiv ); console.log("TimelinePanel rendered"); @@ -256,14 +260,18 @@ describe('TimelinePanel', function() { console.log("back paginating..."); setScrollTop(0); return awaitScroll().then(() => { + let eventTiles = scryEventTiles(panel); + let firstEvent = eventTiles[0].props.mxEvent; + + console.log("TimelinePanel contains " + eventTiles.length + + " events; first is " + + firstEvent.getContent().body); + if(scrollingDiv.scrollTop > 0) { // need to go further return backPaginate(); } console.log("paginated to start."); - - // hopefully, we got to the start of the timeline - expect(messagePanel.props.backPaginating).toBe(false); }); } @@ -276,9 +284,13 @@ describe('TimelinePanel', function() { // back-paginate until we hit the start return backPaginate(); }).then(() => { + // hopefully, we got to the start of the timeline + expect(messagePanel.props.backPaginating).toBe(false); + expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); var events = scryEventTiles(panel); - expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]) + expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]); + expect(events.length).toEqual(TIMELINE_CAP); // we should now be able to scroll down, and paginate in the other // direction. @@ -286,6 +298,23 @@ describe('TimelinePanel', function() { scrollingDiv.scrollTop = scrollingDiv.scrollHeight; return awaitScroll(); }).then(() => { + expect(messagePanel.props.backPaginating).toBe(false); + expect(messagePanel.props.forwardPaginating).toBe(true); + return awaitScroll(); + }).then(() => { + expect(messagePanel.props.backPaginating).toBe(false); + expect(messagePanel.props.forwardPaginating).toBe(false); + expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); + + var events = scryEventTiles(panel); + expect(events.length).toEqual(TIMELINE_CAP); + + // we don't really know what the first event tile will be, since that + // depends on how much the timelinepanel decides to paginate. + // + // just check that the first tile isn't event 0. + expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]); + console.log("done"); }).done(done, done); }); From 6bcf9cc9db20c21f5ac31f7f6f76a4d441f89c68 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 24 Aug 2016 17:30:53 +0100 Subject: [PATCH 17/33] TimelinePanel-test: Hackery to make it pass --- test/components/structures/TimelinePanel-test.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index fb82404616..e817923ec7 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -296,11 +296,12 @@ describe('TimelinePanel', function() { // direction. setScrollTop(scrollingDiv.scrollHeight); scrollingDiv.scrollTop = scrollingDiv.scrollHeight; - return awaitScroll(); - }).then(() => { - expect(messagePanel.props.backPaginating).toBe(false); - expect(messagePanel.props.forwardPaginating).toBe(true); - return awaitScroll(); + + // the delay() below is a heinous hack to deal with the fact that, + // without it, we may or may not get control back before the + // forward pagination completes. The delay means that it should + // have completed. + return awaitScroll().delay(0); }).then(() => { expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.forwardPaginating).toBe(false); From dd77cd74d1f614eb5a4a2aa2437be33c3d79f1d2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 25 Aug 2016 15:13:28 +0100 Subject: [PATCH 18/33] Add text prefix to test event content --- test/components/structures/TimelinePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index e817923ec7..027d888d2d 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -220,7 +220,7 @@ describe('TimelinePanel', function() { // fill the timeline with lots of events for (var i = 0; i < N_EVENTS; i++) { - timeline.addEvent(mkMessage({msg: ""+i})); + timeline.addEvent(mkMessage({msg: "Event "+i})); } console.log("added events to timeline"); From 36d8fa9786ffcd90b370bd4bfb0d76135867d9c1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 25 Aug 2016 16:20:31 +0100 Subject: [PATCH 19/33] fix avatar clicking in memberinfo --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 59e186da06..c02b009c39 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -531,7 +531,7 @@ module.exports = React.createClass({ }, onMemberAvatarClick: function () { - var avatarUrl = this.props.member.user.avatarUrl; + var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url; if(!avatarUrl) return; var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl); From 96567dad0d93ffcc1b4962350e6b7b0ae04d791b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 25 Aug 2016 16:55:09 +0100 Subject: [PATCH 20/33] Fix NPE when we don't know the sender of an event Fixes a bug introduced in https://github.com/matrix-org/matrix-react-sdk/pull/426. Particularly when we are showing search results, we may not recognise the sender of an event; attempting to create a MemberAvatar for it will lead to null-reference errors. Also a bit of untangling of the logic of needsSenderProfile. Since https://github.com/matrix-org/matrix-react-sdk/pull/422, EventTileType.needsSenderProfile was only being called on MessageEvents, and therefore only returned true. It's a shame to see all this logic going into EventTile rather than the individual EventTileTypes, but since it's there, let's not leave the unused logic lying around in the EventTileType implementations. --- src/components/views/messages/MessageEvent.js | 6 --- src/components/views/messages/TextualEvent.js | 7 --- src/components/views/rooms/EventTile.js | 53 ++++++++++++------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 0863fe0842..26658c3005 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -22,12 +22,6 @@ var sdk = require('../../../index'); module.exports = React.createClass({ displayName: 'MessageEvent', - statics: { - needsSenderProfile: function() { - return true; - } - }, - propTypes: { /* the MatrixEvent to show */ mxEvent: React.PropTypes.object.isRequired, diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js index 7cab98ea84..8319dbd434 100644 --- a/src/components/views/messages/TextualEvent.js +++ b/src/components/views/messages/TextualEvent.js @@ -24,12 +24,6 @@ import sdk from '../../../index'; module.exports = React.createClass({ displayName: 'TextualEvent', - statics: { - needsSenderProfile: function() { - return false; - } - }, - render: function() { const EmojiText = sdk.getComponent('elements.EmojiText'); var text = TextForEvent.textForEvent(this.props.mxEvent); @@ -39,4 +33,3 @@ module.exports = React.createClass({ ); }, }); - diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index b33860b7b3..b1df3f3267 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -62,7 +62,7 @@ var MAX_READ_AVATARS = 5; // '----------------------------------------------------------' module.exports = React.createClass({ - displayName: 'Event', + displayName: 'EventTile', statics: { haveTileForEvent: function(e) { @@ -368,7 +368,7 @@ module.exports = React.createClass({ // room, or emote messages var isInfoMessage = (msgtype === 'm.emote' || eventType !== 'm.room.message'); - var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]); + var EventTileType = sdk.getComponent(eventTileTypes[eventType]); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!EventTileType) { @@ -395,31 +395,44 @@ module.exports = React.createClass({ - var aux = null; - if (msgtype === 'm.image') aux = "sent an image"; - else if (msgtype === 'm.video') aux = "sent a video"; - else if (msgtype === 'm.file') aux = "uploaded a file"; - var readAvatars = this.getReadAvatars(); var avatar, sender; + let avatarSize; + let needsSenderProfile; + if (isInfoMessage) { + // a small avatar, with no sender profile, for emotes and + // joins/parts/etc + avatarSize = 14; + needsSenderProfile = false; + } else if (this.props.continuation) { + // no avatar or sender profile for continuation messages + avatarSize = 0; + needsSenderProfile = false; + } else { + avatarSize = 30; + needsSenderProfile = true; + } + + if (this.props.mxEvent.sender && avatarSize) { avatar = ( -
- -
- ); - } else if (!this.props.continuation) { - if (this.props.mxEvent.sender) { - avatar = (
- +
- ); - } - if (EventTileType.needsSenderProfile()) { - sender = ; - } + ); + } + + if (needsSenderProfile) { + let aux = null; + if (msgtype === 'm.image') aux = "sent an image"; + else if (msgtype === 'm.video') aux = "sent a video"; + else if (msgtype === 'm.file') aux = "uploaded a file"; + + sender = ; } var editButton = ( From 6786539550a88bf03cb20808e5328222e4cc330c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 25 Aug 2016 18:30:48 +0100 Subject: [PATCH 21/33] pin linkifyjs to precisely 2.0.0-beta4, as 2.0.0-beta9 breaks links as per https://github.com/vector-im/vector-web/issues/2010 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6ece7e20d..d1b9122a62 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", - "linkifyjs": "^2.0.0-beta.4", + "linkifyjs": "2.0.0-beta.4", "lodash": "^4.13.1", "marked": "^0.3.5", "matrix-js-sdk": "0.5.5", From 5df98d70f4ed65777c6d65e551d7e333453f2d9c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 27 Aug 2016 01:18:48 +0100 Subject: [PATCH 22/33] add debugging to MemberAvatar to try to kick jenkins... --- src/components/views/avatars/MemberAvatar.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 654f801afc..6e1670604e 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -47,6 +47,9 @@ module.exports = React.createClass({ }, _getState: function(props) { + if (!props.member) { + console.error("MemberAvatar called somehow with null member"); + } return { name: props.member.name, title: props.member.userId, From 5b0d13c1fcc8de83b7fc6fa8d5be02297e9f2fb6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 27 Aug 2016 23:59:55 +0100 Subject: [PATCH 23/33] switch to namespaced CSS --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 6a8d903df8..d309ce1950 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -49,7 +49,7 @@ export function unicodeToImage(str) { alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode]; const title = mappedUnicode[unicode]; - replaceWith = `${alt}`; + replaceWith = `${alt}`; return replaceWith; } }); From fb5f8294c848b89605e11f4e0588ad8d3e3a3780 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 00:00:10 +0100 Subject: [PATCH 24/33] switch to namespaced CSS --- src/components/views/settings/DevicesPanelEntry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index cc416ace2f..8e8351de43 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -100,7 +100,7 @@ export default class DevicesPanelEntry extends React.Component { deleteButton =
{this.state.deleteError}
} else { deleteButton = ( -
Delete
From 50d09f73f5d7a6a3a494d65f5b788bc283151140 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 00:00:22 +0100 Subject: [PATCH 25/33] add leave/forget button to RoomSettings --- src/components/views/rooms/RoomSettings.js | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 894012d74e..145b8559b7 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -426,6 +426,27 @@ module.exports = React.createClass({ }, ""); }, + onLeaveClick() { + dis.dispatch({ + action: 'leave_room', + room_id: this.props.room.roomId, + }); + }, + + onForgetClick() { + // FIXME: duplicated with RoomTagContextualMenu (and dead code in RoomView) + MatrixClientPeg.get().forget(this.props.room.roomId).done(function() { + dis.dispatch({ action: 'view_next_room' }); + }, function(err) { + var errCode = err.errcode || "unknown error code"; + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Error", + description: `Failed to forget room (${errCode})` + }); + }); + }, + _renderEncryptionSection: function() { if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { return null; @@ -544,6 +565,25 @@ module.exports = React.createClass({ ); } + var leaveButton = null; + var myMember = this.props.room.getMember(user_id); + if (myMember) { + if (myMember.membership === "join") { + leaveButton = ( +
+ Leave room +
+ ); + } + else if (myMember.membership === "leave") { + leaveButton = ( +
+ Forget room +
+ ); + } + } + // TODO: support editing custom events_levels // TODO: support editing custom user_levels @@ -631,6 +671,8 @@ module.exports = React.createClass({ return (
+ { leaveButton } + { tagsSection }
From ad873c2b60c1b7fead2b20975a66c6dc6cec06cc Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 01:55:42 +0100 Subject: [PATCH 26/33] handle matrix.to links correctly. add partial support for #/user URLs --- src/HtmlUtils.js | 28 ++++++++--- src/components/structures/MatrixChat.js | 64 ++++++++++++++----------- src/linkify-matrix.js | 44 +++++++++++++---- 3 files changed, 92 insertions(+), 44 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d309ce1950..d4a999a5ac 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -85,12 +85,28 @@ var sanitizeHtmlParams = { transformTags: { // custom to matrix // add blank targets to all hyperlinks except vector URLs 'a': function(tagName, attribs) { - var m = attribs.href ? attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN) : null; - if (m) { - delete attribs.target; - } - else { - attribs.target = '_blank'; + if (attribs.href) { + attribs.target = '_blank'; // by default + + var m; + // FIXME: horrible duplication with linkify-matrix + m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN); + if (m) { + attribs.href = m[1]; + delete attribs.target; + } + + m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); + if (m) { + var entity = m[1]; + if (entity[0] === '@') { + attribs.href = '#'; // TODO + } + else if (entity[0] === '#') { + attribs.href = '#/room/' + entity; + } + delete attribs.target; + } } attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 46cad9685b..743f30aa15 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -176,12 +176,12 @@ module.exports = React.createClass({ // this can technically be done anywhere but doing this here keeps all // the routing url path logic together. - if (this.onAliasClick) { - linkifyMatrix.onAliasClick = this.onAliasClick; - } - if (this.onUserClick) { - linkifyMatrix.onUserClick = this.onUserClick; - } + // if (this.onAliasClick) { + // linkifyMatrix.onAliasClick = this.onAliasClick; + // } + // if (this.onUserClick) { + // linkifyMatrix.onUserClick = this.onUserClick; + // } window.addEventListener('resize', this.handleResize); this.handleResize(); @@ -739,6 +739,16 @@ module.exports = React.createClass({ } else { dis.dispatch(payload); } + } else if (screen.indexOf('user/') == 0) { + var userId = screen.substring(5); + var member = new Matrix.RoomMember(null, userId); + if (member) { + // FIXME: this doesn't work yet + dis.dispatch({ + action: 'view_user', + member: member, + }); + } } else { console.info("Ignoring showScreen for '%s'", screen); @@ -751,31 +761,29 @@ module.exports = React.createClass({ } }, - onAliasClick: function(event, alias) { - event.preventDefault(); - dis.dispatch({action: 'view_room', room_alias: alias}); - }, + // onAliasClick: function(event, alias) { + // event.preventDefault(); + // dis.dispatch({action: 'view_room', room_alias: alias}); + // }, - onUserClick: function(event, userId) { - event.preventDefault(); + // onUserClick: function(event, userId) { + // event.preventDefault(); - /* - var MemberInfo = sdk.getComponent('rooms.MemberInfo'); - var member = new Matrix.RoomMember(null, userId); - ContextualMenu.createMenu(MemberInfo, { - member: member, - right: window.innerWidth - event.pageX, - top: event.pageY - }); - */ + // // var MemberInfo = sdk.getComponent('rooms.MemberInfo'); + // // var member = new Matrix.RoomMember(null, userId); + // // ContextualMenu.createMenu(MemberInfo, { + // // member: member, + // // right: window.innerWidth - event.pageX, + // // top: event.pageY + // // }); - var member = new Matrix.RoomMember(null, userId); - if (!member) { return; } - dis.dispatch({ - action: 'view_user', - member: member, - }); - }, + // var member = new Matrix.RoomMember(null, userId); + // if (!member) { return; } + // dis.dispatch({ + // action: 'view_user', + // member: member, + // }); + // }, onLogoutClick: function(event) { dis.dispatch({ diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 99b7ee5c33..eaddbffebc 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -95,20 +95,25 @@ function matrixLinkify(linkify) { S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID); } -matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; -matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; +// stubs, overwritten in MatrixChat's componentDidMount +// matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; +// matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; var escapeRegExp = function(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }; -// we only recognise URLs which match our current URL as being the same app -// as if someone explicitly links to vector.im/develop and we're on vector.im/beta -// they may well be trying to get us to explicitly go to develop. -// FIXME: intercept matrix.to URLs as well. -matrixLinkify.VECTOR_URL_PATTERN = "^(https?:\/\/)?" + escapeRegExp(window.location.host + window.location.pathname); +// Recognise URLs from both our local vector and official vector as vector. +// anyone else really should be using matrix.to. +matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + + escapeRegExp(window.location.host + window.location.pathname) + "|" + + "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/" + + ")(#.*)"; + +matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@).*)"; matrixLinkify.options = { +/* events: function (href, type) { switch (type) { case "userid": @@ -125,14 +130,31 @@ matrixLinkify.options = { }; } }, - +*/ formatHref: function (href, type) { switch (type) { case 'roomalias': return '#/room/' + href; case 'userid': - return '#'; + return '#/user/' + href; default: + var m; + // FIXME: horrible duplication with HtmlUtils' transform tags + m = href.match(matrixLinkify.VECTOR_URL_PATTERN); + if (m) { + return m[1]; + } + m = href.match(matrixLinkify.MATRIXTO_URL_PATTERN); + if (m) { + var entity = m[1]; + if (entity[0] === '@') { + return '#'; // TODO + } + else if (entity[0] === '#') { + return '#/room/' + entity; + } + } + return href; } }, @@ -143,7 +165,9 @@ matrixLinkify.options = { target: function(href, type) { if (type === 'url') { - if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) { + if (href.match(matrixLinkify.VECTOR_URL_PATTERN) || + href.match(matrixLinkify.MATRIXTO_URL_PATTERN)) + { return null; } else { From de82ac3bc07d82978cd04be4f9d20fa38b794661 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 02:05:23 +0100 Subject: [PATCH 27/33] don't change URL bar when clicking on linkified rooms or users. be aware of /user paths. --- src/HtmlUtils.js | 4 +- src/components/structures/MatrixChat.js | 52 ++++++++++++------------- src/linkify-matrix.js | 11 +++--- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d4a999a5ac..c0e814e49c 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -100,9 +100,9 @@ var sanitizeHtmlParams = { if (m) { var entity = m[1]; if (entity[0] === '@') { - attribs.href = '#'; // TODO + attribs.href = '#/user/' + entity; } - else if (entity[0] === '#') { + else if (entity[0] === '#' || entity[0] === '!') { attribs.href = '#/room/' + entity; } delete attribs.target; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 743f30aa15..839d59e2d9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -176,12 +176,12 @@ module.exports = React.createClass({ // this can technically be done anywhere but doing this here keeps all // the routing url path logic together. - // if (this.onAliasClick) { - // linkifyMatrix.onAliasClick = this.onAliasClick; - // } - // if (this.onUserClick) { - // linkifyMatrix.onUserClick = this.onUserClick; - // } + if (this.onAliasClick) { + linkifyMatrix.onAliasClick = this.onAliasClick; + } + if (this.onUserClick) { + linkifyMatrix.onUserClick = this.onUserClick; + } window.addEventListener('resize', this.handleResize); this.handleResize(); @@ -761,29 +761,29 @@ module.exports = React.createClass({ } }, - // onAliasClick: function(event, alias) { - // event.preventDefault(); - // dis.dispatch({action: 'view_room', room_alias: alias}); - // }, + onAliasClick: function(event, alias) { + event.preventDefault(); + dis.dispatch({action: 'view_room', room_alias: alias}); + }, - // onUserClick: function(event, userId) { - // event.preventDefault(); + onUserClick: function(event, userId) { + event.preventDefault(); - // // var MemberInfo = sdk.getComponent('rooms.MemberInfo'); - // // var member = new Matrix.RoomMember(null, userId); - // // ContextualMenu.createMenu(MemberInfo, { - // // member: member, - // // right: window.innerWidth - event.pageX, - // // top: event.pageY - // // }); + // var MemberInfo = sdk.getComponent('rooms.MemberInfo'); + // var member = new Matrix.RoomMember(null, userId); + // ContextualMenu.createMenu(MemberInfo, { + // member: member, + // right: window.innerWidth - event.pageX, + // top: event.pageY + // }); - // var member = new Matrix.RoomMember(null, userId); - // if (!member) { return; } - // dis.dispatch({ - // action: 'view_user', - // member: member, - // }); - // }, + var member = new Matrix.RoomMember(null, userId); + if (!member) { return; } + dis.dispatch({ + action: 'view_user', + member: member, + }); + }, onLogoutClick: function(event) { dis.dispatch({ diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index eaddbffebc..594c0c6a22 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -96,8 +96,8 @@ function matrixLinkify(linkify) { } // stubs, overwritten in MatrixChat's componentDidMount -// matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; -// matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; +matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; +matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; var escapeRegExp = function(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -113,7 +113,6 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@).*)"; matrixLinkify.options = { -/* events: function (href, type) { switch (type) { case "userid": @@ -130,7 +129,7 @@ matrixLinkify.options = { }; } }, -*/ + formatHref: function (href, type) { switch (type) { case 'roomalias': @@ -148,9 +147,9 @@ matrixLinkify.options = { if (m) { var entity = m[1]; if (entity[0] === '@') { - return '#'; // TODO + return '#/user/' + entity; } - else if (entity[0] === '#') { + else if (entity[0] === '#' || entity[0] === '!') { return '#/room/' + entity; } } From 18436e1257da62725300acff5203b3b1efd30297 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 02:12:18 +0100 Subject: [PATCH 28/33] pick up ! matrix.to URLs --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 594c0c6a22..99e4898182 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -110,7 +110,7 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/" + ")(#.*)"; -matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@).*)"; +matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; matrixLinkify.options = { events: function (href, type) { From 7c6b1703f324a7c567ca8a37ffe1a5891ea1679c Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 28 Aug 2016 14:50:22 +0530 Subject: [PATCH 29/33] fix emojione sizing --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c0e814e49c..2bb0ef4c04 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -287,7 +287,7 @@ module.exports = { emojifyText: function(text) { return { - __html: emojione.unicodeToImage(escape(text)), + __html: unicodeToImage(escape(text)), }; }, }; From 5e105e1be6747340839dae0ac2d59013e17d5d1b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 14:04:02 +0100 Subject: [PATCH 30/33] make /user URLs work --- src/components/structures/MatrixChat.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 839d59e2d9..84575b9e4f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -69,6 +69,7 @@ module.exports = React.createClass({ UserSettings: "user_settings", CreateRoom: "create_room", RoomDirectory: "room_directory", + UserView: "user_view", }, AuxPanel: { @@ -87,6 +88,10 @@ module.exports = React.createClass({ // in the case where we view a room by ID or by RoomView when it resolves // what ID an alias points at. currentRoomId: null, + + // If we're trying to just view a user ID (i.e. /user URL), this is it + viewUserId: null, + logged_in: false, collapse_lhs: false, collapse_rhs: false, @@ -741,9 +746,11 @@ module.exports = React.createClass({ } } else if (screen.indexOf('user/') == 0) { var userId = screen.substring(5); + this.setState({ viewUserId: userId }); + this._setPage(this.PageTypes.UserView); + this.notifyNewScreen('user/' + userId); var member = new Matrix.RoomMember(null, userId); if (member) { - // FIXME: this doesn't work yet dis.dispatch({ action: 'view_user', member: member, @@ -1000,6 +1007,10 @@ module.exports = React.createClass({ page_element = right_panel = break; + case this.PageTypes.UserView: + page_element = null; // deliberately null for now + right_panel = + break; } var topBar; From 3dd337b553783443150ec89a65be1b51ebfe93d6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 16:51:51 +0100 Subject: [PATCH 31/33] dep on 0.5.6 of matrix-js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1b9122a62..9b2ad756c7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "linkifyjs": "2.0.0-beta.4", "lodash": "^4.13.1", "marked": "^0.3.5", - "matrix-js-sdk": "0.5.5", + "matrix-js-sdk": "0.5.6", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.2.1", From 4e169f8e002919aec484a58f2818098c777019e0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 16:55:04 +0100 Subject: [PATCH 32/33] Prepare changelog for v0.6.5 --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99944185e..c18ffa24d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +Changes in [0.6.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5) (2016-08-28) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4-r1...v0.6.5) + + * re-add leave button in RoomSettings + * add /user URLs + * recognise matrix.to links and other vector links + * fix linkify dependency + * fix avatar clicking in MemberInfo + * fix emojione sizing + [\#431](https://github.com/matrix-org/matrix-react-sdk/pull/431) + * Fix NPE when we don't know the sender of an event + [\#430](https://github.com/matrix-org/matrix-react-sdk/pull/430) + * Update annoying TimelinePanel test + [\#429](https://github.com/matrix-org/matrix-react-sdk/pull/429) + * add fancy changelog dialog + [\#416](https://github.com/matrix-org/matrix-react-sdk/pull/416) + * Send bot options with leading underscore on the state key + [\#428](https://github.com/matrix-org/matrix-react-sdk/pull/428) + * Update autocomplete design and scroll it correctly + [\#419](https://github.com/matrix-org/matrix-react-sdk/pull/419) + * Add ability to query and set bot options + [\#427](https://github.com/matrix-org/matrix-react-sdk/pull/427) + * Add .travis.yml + [\#425](https://github.com/matrix-org/matrix-react-sdk/pull/425) + * Added event/info message avatars back in + [\#426](https://github.com/matrix-org/matrix-react-sdk/pull/426) + * Add postMessage API required for integration provisioning + [\#423](https://github.com/matrix-org/matrix-react-sdk/pull/423) + * Fix TimelinePanel test + [\#424](https://github.com/matrix-org/matrix-react-sdk/pull/424) + * Wmwragg/chat message presentation + [\#422](https://github.com/matrix-org/matrix-react-sdk/pull/422) + * Only try to delete room rule if it exists + [\#421](https://github.com/matrix-org/matrix-react-sdk/pull/421) + * Make the notification slider work + [\#420](https://github.com/matrix-org/matrix-react-sdk/pull/420) + * Don't download E2E devices if feature disabled + [\#418](https://github.com/matrix-org/matrix-react-sdk/pull/418) + * strip (IRC) suffix from tabcomplete entries + [\#417](https://github.com/matrix-org/matrix-react-sdk/pull/417) + * ignore local busy + [\#415](https://github.com/matrix-org/matrix-react-sdk/pull/415) + * defaultDeviceDisplayName should be a prop + [\#414](https://github.com/matrix-org/matrix-react-sdk/pull/414) + * Use server-generated deviceId + [\#410](https://github.com/matrix-org/matrix-react-sdk/pull/410) + * Set initial_device_display_name on login and register + [\#413](https://github.com/matrix-org/matrix-react-sdk/pull/413) + * Add device_id to devices display + [\#409](https://github.com/matrix-org/matrix-react-sdk/pull/409) + * Don't use MatrixClientPeg for temporary clients + [\#408](https://github.com/matrix-org/matrix-react-sdk/pull/408) + Changes in [0.6.4-r1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.4-r1) (2016-08-12) ========================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4...v0.6.4-r1) From 90cfb06ba3a2efa7063a5e500146fe9ee482ef6e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 Aug 2016 16:55:04 +0100 Subject: [PATCH 33/33] 0.6.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b2ad756c7..569ce798f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.6.4-r1", + "version": "0.6.5", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": {