diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 747e18e679..8c7e397fce 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -118,7 +118,17 @@ export function processHtmlForSending(html: string): string { return contentHTML; } -var sanitizeHtmlParams = { +/* + * Given an untrusted HTML string, return a React node with an sanitized version + * of that HTML. + */ +export function sanitizedHtmlNode(insaneHtml) { + const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); + + return
; +} + +const sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring 'del', // for markdown diff --git a/src/PageTypes.js b/src/PageTypes.js index d87b363a6f..b2346c62c3 100644 --- a/src/PageTypes.js +++ b/src/PageTypes.js @@ -22,4 +22,5 @@ export default { CreateRoom: "create_room", RoomDirectory: "room_directory", UserView: "user_view", + GroupView: "group_view", }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js new file mode 100644 index 0000000000..d3a06b915b --- /dev/null +++ b/src/components/structures/GroupView.js @@ -0,0 +1,131 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; +import MatrixClientPeg from '../../MatrixClientPeg'; +import sdk from '../../index'; +import { sanitizedHtmlNode } from '../../HtmlUtils'; +import { _t } from '../../languageHandler'; + + +module.exports = React.createClass({ + displayName: 'GroupView', + + propTypes: { + groupId: React.PropTypes.string.isRequired, + }, + + getInitialState: function() { + return { + summary: null, + error: null, + }; + }, + + componentWillMount: function() { + this._loadGroupFromServer(this.props.groupId); + }, + + componentWillReceiveProps: function(newProps) { + if (this.props.groupId != newProps.groupId) { + this.setState({ + summary: null, + error: null, + }, () => { + this._loadGroupFromServer(newProps.groupId); + }); + } + }, + + _loadGroupFromServer: function(groupId) { + MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { + this.setState({ + summary: res, + error: null, + }); + }, (err) => { + this.setState({ + summary: null, + error: err, + }); + }); + }, + + render: function() { + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + const Loader = sdk.getComponent("elements.Spinner"); + + if (this.state.summary === null && this.state.error === null) { + return ; + } else if (this.state.summary) { + const summary = this.state.summary; + let avatarUrl = null; + if (summary.profile && summary.profile.avatar_url) { + avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(summary.profile.avatar_url); + } + let description = null; + if (summary.profile && summary.profile.long_description) { + description = sanitizedHtmlNode(summary.profile.long_description); + } + return ( +
+
+
+
+ +
+
+
+ {summary.profile.name} + + ({this.props.groupId}) + +
+
+ {summary.profile.short_description} +
+
+
+
+ {description} +
+ ); + } else if (this.state.error) { + if (this.state.error.httpStatus === 404) { + return ( +
+ Group {this.props.groupId} not found +
+ ); + } else { + let extraText; + if (this.state.error.errcode === 'M_UNRECOGNIZED') { + extraText =
{_t('This Home server does not support groups')}
; + } + return ( +
+ Failed to load {this.props.groupId} + {extraText} +
+ ); + } + } else { + console.error("Invalid state for GroupView"); + return
; + } + }, +}); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 051e49acda..aef7fe9cce 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -210,6 +210,7 @@ export default React.createClass({ const CreateRoom = sdk.getComponent('structures.CreateRoom'); const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const HomePage = sdk.getComponent('structures.HomePage'); + const GroupView = sdk.getComponent('structures.GroupView'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar'); @@ -280,6 +281,11 @@ export default React.createClass({ page_element = null; // deliberately null for now right_panel = ; break; + case PageTypes.GroupView: + page_element = ; + break; } let topBar; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b618caa1c9..025805d921 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -290,6 +290,9 @@ module.exports = React.createClass({ if (this.onUserClick) { linkifyMatrix.onUserClick = this.onUserClick; } + if (this.onGroupClick) { + linkifyMatrix.onGroupClick = this.onGroupClick; + } window.addEventListener('resize', this.handleResize); this.handleResize(); @@ -483,6 +486,14 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_group': + { + const groupId = payload.group_id; + this.setState({currentGroupId: groupId}); + this._setPage(PageTypes.GroupView); + this.notifyNewScreen('group/' + groupId); + } + break; case 'view_home_page': this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); @@ -1199,6 +1210,15 @@ module.exports = React.createClass({ member: member, }); } + } else if (screen.indexOf('group/') == 0) { + const groupId = screen.substring(6); + + // TODO: Check valid group ID + + dis.dispatch({ + action: 'view_group', + group_id: groupId, + }); } else { console.info("Ignoring showScreen for '%s'", screen); } @@ -1227,6 +1247,11 @@ module.exports = React.createClass({ }); }, + onGroupClick: function(event, groupId) { + event.preventDefault(); + dis.dispatch({action: 'view_group', group_id: groupId}); + }, + onLogoutClick: function(event) { dis.dispatch({ action: 'logout', diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index d9b0b78982..01512a771a 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -108,11 +108,53 @@ function matrixLinkify(linkify) { S_AT_NAME_COLON_DOMAIN.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID); + + + var GROUPID = function(value) { + MultiToken.call(this, value); + this.type = 'groupid'; + this.isLink = true; + }; + GROUPID.prototype = new MultiToken(); + + var S_PLUS = new linkify.parser.State(); + var S_PLUS_NAME = new linkify.parser.State(); + var S_PLUS_NAME_COLON = new linkify.parser.State(); + var S_PLUS_NAME_COLON_DOMAIN = new linkify.parser.State(); + var S_PLUS_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); + var S_GROUPID = new linkify.parser.State(GROUPID); + + var groupid_tokens = [ + TT.DOT, + TT.UNDERSCORE, + TT.PLUS, + TT.NUM, + TT.DOMAIN, + TT.TLD, + + // as in roomname_tokens + TT.LOCALHOST, + ]; + + S_START.on(TT.PLUS, S_PLUS); + + S_PLUS.on(groupid_tokens, S_PLUS_NAME); + S_PLUS_NAME.on(groupid_tokens, S_PLUS_NAME); + S_PLUS_NAME.on(TT.DOMAIN, S_PLUS_NAME); + + S_PLUS_NAME.on(TT.COLON, S_PLUS_NAME_COLON); + + S_PLUS_NAME_COLON.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); + S_PLUS_NAME_COLON.on(TT.LOCALHOST, S_GROUPID); // accept +foo:localhost + S_PLUS_NAME_COLON_DOMAIN.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT); + S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); + S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_GROUPID); } // stubs, overwritten in MatrixChat's componentDidMount matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; +matrixLinkify.onGroupClick = function(e, groupId) { e.preventDefault(); }; var escapeRegExp = function(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -143,6 +185,12 @@ matrixLinkify.options = { matrixLinkify.onAliasClick(e, href); } }; + case "groupid": + return { + click: function(e) { + matrixLinkify.onGroupClick(e, href); + } + }; } }, @@ -150,6 +198,7 @@ matrixLinkify.options = { switch (type) { case 'roomalias': case 'userid': + case 'groupid': return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href; default: var m;