From 53574541c3a480ee3fda37b642d2e8d1e82c5183 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 4 Sep 2017 09:31:25 +0200 Subject: [PATCH 01/23] AppTile: Add Jitsi electron screensharing support --- src/components/views/elements/AppTile.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 7436f84f69..5a5e266df8 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -19,6 +19,7 @@ limitations under the License. import url from 'url'; import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -127,6 +128,30 @@ export default React.createClass({ loading: false, }); }); + window.addEventListener('message', this._onMessage, false); + }, + + componentWillUnmount() { + window.removeEventListener('message', this._onMessage); + }, + + _onMessage(event) { + if (!PlatformPeg.get().isElectron() || this.props.type !== 'jitsi') { + return; + } + if (!event.origin) { + event.origin = event.originalEvent.origin; + } + + if (this.state.widgetUrl.indexOf(event.origin) === -1) { + return; + } + + if (event.data.widgetAction === 'jitsi_iframe_loaded') { + const iframe = this.refs.appFrame.contentWindow + .document.querySelector('iframe[id^="jitsiConferenceFrame"]'); + PlatformPeg.get().setupScreenSharingForIframe(iframe); + } }, _canUserModify: function() { From 25da0f6a7acfa96a933280e0fa68558a5a5f8057 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Sep 2017 17:01:14 +0100 Subject: [PATCH 02/23] MemberList: show 100 more on overflow tile click Not the full list because on HQ that causes your browser to implode. This should really be a decent paginated list at this point, but this is better for now. --- src/components/views/rooms/MemberList.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index babbfcf649..f916c55020 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -31,6 +31,7 @@ var CallHandler = require("../../../CallHandler"); const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; +const SHOW_MORE_INCREMENT = 100; module.exports = React.createClass({ displayName: 'MemberList', @@ -207,11 +208,11 @@ module.exports = React.createClass({ }, _createOverflowTileJoined: function(overflowCount, totalCount) { - return this._createOverflowTile(overflowCount, totalCount, this._showFullJoinedMemberList); + return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList); }, _createOverflowTileInvited: function(overflowCount, totalCount) { - return this._createOverflowTile(overflowCount, totalCount, this._showFullInvitedMemberList); + return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList); }, _createOverflowTile: function(overflowCount, totalCount, onClick) { @@ -227,15 +228,15 @@ module.exports = React.createClass({ ); }, - _showFullJoinedMemberList: function() { + _showMoreJoinedMemberList: function() { this.setState({ - truncateAtJoined: -1 + truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT, }); }, - _showFullInvitedMemberList: function() { + _showMoreInvitedMemberList: function() { this.setState({ - truncateAtInvited: -1 + truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT, }); }, From 791bc5e7ac5c33f6fa72202055cb18f6533c251c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 22 Sep 2017 18:52:06 +0100 Subject: [PATCH 03/23] Create GroupSummaryStore for storing group summary stuff - Acts as a layer between GroupView and the group APIs that modify the summary individually. This allows for abstraction of getting the new summary once a successful API hit has been done. - The plan is to also control the avatar, topic, body of the summary via the same class --- src/components/structures/GroupView.js | 43 ++++++++++++---- src/i18n/strings/en_EN.json | 1 + src/stores/GroupSummaryStore.js | 71 ++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/stores/GroupSummaryStore.js diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4f61bc4647..d3eb97180a 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -27,6 +27,8 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import Modal from '../../Modal'; import classnames from 'classnames'; +import GroupSummaryStore from '../../stores/GroupSummaryStore'; + const RoomSummaryType = PropTypes.shape({ room_id: PropTypes.string.isRequired, profile: PropTypes.shape({ @@ -76,8 +78,8 @@ const CategoryRoomList = React.createClass({ if (!success) return; const errorList = []; Promise.all(addrs.map((addr) => { - return MatrixClientPeg.get() - .addRoomToGroupSummary(this.props.groupId, addr.address) + return this.context.groupSummaryStore + .addRoomToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }) .reflect(); })).then(() => { @@ -153,8 +155,7 @@ const FeaturedRoom = React.createClass({ onDeleteClicked: function(e) { e.preventDefault(); e.stopPropagation(); - MatrixClientPeg.get().removeRoomFromGroupSummary( - this.props.groupId, + this.context.groupSummaryStore.removeRoomFromGroupSummary( this.props.summaryInfo.room_id, ).catch((err) => { console.error('Error whilst removing room from group summary', err); @@ -242,8 +243,8 @@ const RoleUserList = React.createClass({ if (!success) return; const errorList = []; Promise.all(addrs.map((addr) => { - return MatrixClientPeg.get() - .addUserToGroupSummary(this.props.groupId, addr.address) + return this.context.groupSummaryStore + .addUserToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }) .reflect(); })).then(() => { @@ -317,8 +318,7 @@ const FeaturedUser = React.createClass({ onDeleteClicked: function(e) { e.preventDefault(); e.stopPropagation(); - MatrixClientPeg.get().removeUserFromGroupSummary( - this.props.groupId, + this.context.groupSummaryStore.removeUserFromGroupSummary( this.props.summaryInfo.user_id, ).catch((err) => { console.error('Error whilst removing user from group summary', err); @@ -364,6 +364,15 @@ const FeaturedUser = React.createClass({ }, }); +const GroupSummaryContext = { + groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore).isRequired, +}; + +CategoryRoomList.contextTypes = GroupSummaryContext; +FeaturedRoom.contextTypes = GroupSummaryContext; +RoleUserList.contextTypes = GroupSummaryContext; +FeaturedUser.contextTypes = GroupSummaryContext; + export default React.createClass({ displayName: 'GroupView', @@ -371,6 +380,16 @@ export default React.createClass({ groupId: PropTypes.string.isRequired, }, + childContextTypes: { + groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore), + }, + + getChildContext: function() { + return { + groupSummaryStore: this._groupSummaryStore, + }; + }, + getInitialState: function() { return { summary: null, @@ -411,12 +430,14 @@ export default React.createClass({ }, _loadGroupFromServer: function(groupId) { - MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { + this._groupSummaryStore = new GroupSummaryStore(this.props.groupId); + this._groupSummaryStore.on('update', () => { this.setState({ - summary: res, + summary: this._groupSummaryStore.getSummary(), error: null, }); - }, (err) => { + }); + this._groupSummaryStore.on('error', (err) => { this.setState({ summary: null, error: err, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0945d7f50..038d26ba4c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -876,6 +876,7 @@ "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Room name or alias": "Room name or alias", "You are an administrator of this group": "You are an administrator of this group", + "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", "Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s", "The room '%(roomName)' could not be removed from the summary.": "The room '%(roomName)' could not be removed from the summary.", "Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s", diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupSummaryStore.js new file mode 100644 index 0000000000..a26f0f2d61 --- /dev/null +++ b/src/stores/GroupSummaryStore.js @@ -0,0 +1,71 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 EventEmitter from 'events'; +import MatrixClientPeg from '../MatrixClientPeg'; + +/** + * Stores the group summary for a room and provides an API to change it + */ +export default class GroupSummaryStore extends EventEmitter { + constructor(groupId) { + super(); + this._groupId = groupId; + this._summary = {}; + this._fetchSummary(); + } + + _fetchSummary() { + MatrixClientPeg.get().getGroupSummary(this._groupId).then((resp) => { + this._summary = resp; + this._notifyListeners(); + }).catch((err) => { + this.emit('error', err); + }); + } + + _notifyListeners() { + this.emit('update'); + } + + getSummary() { + return this._summary; + } + + addRoomToGroupSummary(roomId, categoryId) { + return MatrixClientPeg.get() + .addRoomToGroupSummary(this._groupId, roomId, categoryId) + .then(this._fetchSummary.bind(this)); + } + + addUserToGroupSummary(userId, roleId) { + return MatrixClientPeg.get() + .addUserToGroupSummary(this._groupId, userId, roleId) + .then(this._fetchSummary.bind(this)); + } + + removeRoomFromGroupSummary(roomId) { + return MatrixClientPeg.get() + .removeRoomFromGroupSummary(this._groupId, roomId) + .then(this._fetchSummary.bind(this)); + } + + removeUserFromGroupSummary(userId) { + return MatrixClientPeg.get() + .removeUserFromGroupSummary(this._groupId, userId) + .then(this._fetchSummary.bind(this)); + } +} From af73f1c3bb41735d67b9e194f23f68149d9c08ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Sep 2017 19:27:02 +0100 Subject: [PATCH 04/23] Add status & toggle for publicity This doesn't work at the moment because it looks like it's broken in synapse as per https://github.com/matrix-org/sytest/pull/391 But if it did work, the client side code might look something like this. --- src/components/structures/GroupView.js | 85 ++++++++++++++++++++++---- src/i18n/strings/en_EN.json | 8 ++- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4f61bc4647..f74e3d60dd 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -379,6 +379,7 @@ export default React.createClass({ saving: false, uploadingAvatar: false, membershipBusy: false, + publicityBusy: false, }; }, @@ -560,7 +561,28 @@ export default React.createClass({ }); }, - _getFeaturedRoomsNode() { + _onPubliciseOffClick: function() { + this._setPublicity(false); + }, + + _onPubliciseOnClick: function() { + this._setPublicity(true); + }, + + _setPublicity: function(publicity) { + this.setState({ + publicityBusy: true, + }); + MatrixClientPeg.get().setGroupPublicity(this.props.groupId, publicity).then(() => { + this._loadGroupFromServer(this.props.groupId); + }).then(() => { + this.setState({ + publicityBusy: false, + }); + }); + }, + + _getFeaturedRoomsNode: function() { const summary = this.state.summary; const defaultCategoryRooms = []; @@ -601,7 +623,7 @@ export default React.createClass({ ; }, - _getFeaturedUsersNode() { + _getFeaturedUsersNode: function() { const summary = this.state.summary; const noRoleUsers = []; @@ -643,11 +665,12 @@ export default React.createClass({ }, _getMembershipSection: function() { + const Spinner = sdk.getComponent("elements.Spinner"); + const group = MatrixClientPeg.get().getGroup(this.props.groupId); if (!group) return null; if (group.myMembership === 'invite') { - const Spinner = sdk.getComponent("elements.Spinner"); if (this.state.membershipBusy) { return
@@ -677,17 +700,57 @@ export default React.createClass({ if (this.state.summary.user && this.state.summary.user.is_privileged) { youAreAMemberText = _t("You are an administrator of this group"); } - return
-
- {youAreAMemberText} -
-
- ; + } + + let publicisedSection; + if (this.state.summary.user && this.state.summary.user.is_public) { + if (!this.state.publicityBusy) { + publicisedButton = + {_t("Make private")} + ; + } + publicisedSection =
+ {_t("Your membership of this group is public")} +
+ {publicisedButton} +
+
; + } else { + if (!this.state.publicityBusy) { + publicisedButton = - {_t("Leave")} + {_t("Make public")} + } + publicisedSection =
+ {_t("Your membership of this group is private")} +
+ {publicisedButton} +
+
; + } + + return
+
+
+ {youAreAMemberText} +
+
+ + {_t("Leave")} + +
+ {publicisedSection}
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0945d7f50..1a775299ea 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -879,5 +879,11 @@ "Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s", "The room '%(roomName)' could not be removed from the summary.": "The room '%(roomName)' could not be removed from the summary.", "Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s", - "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary." + "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.", + "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", + "The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.", + "Your membership of this group is public": "Your membership of this group is public", + "Your membership of this group is private": "Your membership of this group is private", + "Make private": "Make private", + "Make public": "Make public" } From 4da7a368a4f3e9bc6e9e276f926460377d3bb84c Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sat, 23 Sep 2017 10:04:59 +0200 Subject: [PATCH 05/23] Fix incorrect variable in string --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0945d7f50..b6ba5674b1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -877,7 +877,7 @@ "Room name or alias": "Room name or alias", "You are an administrator of this group": "You are an administrator of this group", "Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s", - "The room '%(roomName)' could not be removed from the summary.": "The room '%(roomName)' could not be removed from the summary.", + "The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.", "Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s", "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary." } From c8922ba7f54a76512490016e9ea3aaa91e4426cf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Sep 2017 09:48:00 +0100 Subject: [PATCH 06/23] This file was hurting my eyes. Delint and DRY Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintignore.errorfiles | 1 - src/TextForEvent.js | 213 +++++++++++++++++++++------------------ 2 files changed, 113 insertions(+), 101 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index f9c5e58099..50350f983a 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -123,7 +123,6 @@ src/Roles.js src/Rooms.js src/ScalarAuthClient.js src/ScalarMessaging.js -src/TextForEvent.js src/Tinter.js src/UiEffects.js src/Unread.js diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 902c10307e..400a04f6b3 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -13,56 +13,67 @@ 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 MatrixClientPeg from "./MatrixClientPeg"; -import CallHandler from "./CallHandler"; +import MatrixClientPeg from './MatrixClientPeg'; +import CallHandler from './CallHandler'; import { _t } from './languageHandler'; import * as Roles from './Roles'; function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" - var senderName = ev.sender ? ev.sender.name : ev.getSender(); - var targetName = ev.target ? ev.target.name : ev.getStateKey(); - var ConferenceHandler = CallHandler.getConferenceHandler(); - var reason = ev.getContent().reason ? ( - _t('Reason') + ': ' + ev.getContent().reason - ) : ""; - switch (ev.getContent().membership) { - case 'invite': - var threePidContent = ev.getContent().third_party_invite; + const senderName = ev.sender ? ev.sender.name : ev.getSender(); + const targetName = ev.target ? ev.target.name : ev.getStateKey(); + const prevContent = ev.getPrevContent(); + const content = ev.getContent(); + + const ConferenceHandler = CallHandler.getConferenceHandler(); + const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : ''; + switch (content.membership) { + case 'invite': { + const threePidContent = content.third_party_invite; if (threePidContent) { if (threePidContent.display_name) { - return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name}); + return _t('%(targetName)s accepted the invitation for %(displayName)s.', { + targetName, + displayName: threePidContent.display_name, + }); } else { - return _t('%(targetName)s accepted an invitation.', {targetName: targetName}); + return _t('%(targetName)s accepted an invitation.', {targetName}); } - } - else { + } else { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName}); - } - else { - return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName}); + return _t('%(senderName)s requested a VoIP conference.', {senderName}); + } else { + return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); } } + } case 'ban': - return _t( - '%(senderName)s banned %(targetName)s.', - {senderName: senderName, targetName: targetName} - ) + ' ' + reason; + return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason; case 'join': - if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { - if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { - return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname}); - } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { - return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname}); - } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { - return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname}); - } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { - return _t('%(senderName)s removed their profile picture.', {senderName: senderName}); - } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { - return _t('%(senderName)s changed their profile picture.', {senderName: senderName}); - } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { - return _t('%(senderName)s set a profile picture.', {senderName: senderName}); + if (prevContent && prevContent.membership === 'join') { + if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { + return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', { + senderName, + oldDisplayName: prevContent.displayname, + displayName: content.displayname, + }); + } else if (!prevContent.displayname && content.displayname) { + return _t('%(senderName)s set their display name to %(displayName)s.', { + senderName, + displayName: content.displayname, + }); + } else if (prevContent.displayname && !content.displayname) { + return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { + senderName, + oldDisplayName: prevContent.displayname, + }); + } else if (prevContent.avatar_url && !prevContent.avatar_url) { + return _t('%(senderName)s removed their profile picture.', {senderName}); + } else if (prevContent.avatar_url && content.avatar_url && + prevContent.avatar_url !== content.avatar_url) { + return _t('%(senderName)s changed their profile picture.', {senderName}); + } else if (!prevContent.avatar_url && content.avatar_url) { + return _t('%(senderName)s set a profile picture.', {senderName}); } else { // suppress null rejoins return ''; @@ -71,73 +82,69 @@ function textForMemberEvent(ev) { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { return _t('VoIP conference started.'); - } - else { - return _t('%(targetName)s joined the room.', {targetName: targetName}); + } else { + return _t('%(targetName)s joined the room.', {targetName}); } } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { return _t('VoIP conference finished.'); + } else if (prevContent.membership === "invite") { + return _t('%(targetName)s rejected the invitation.', {targetName}); + } else { + return _t('%(targetName)s left the room.', {targetName}); } - else if (ev.getPrevContent().membership === "invite") { - return _t('%(targetName)s rejected the invitation.', {targetName: targetName}); - } - else { - return _t('%(targetName)s left the room.', {targetName: targetName}); - } - } - else if (ev.getPrevContent().membership === "ban") { - return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName}); - } - else if (ev.getPrevContent().membership === "join") { - return _t( - '%(senderName)s kicked %(targetName)s.', - {senderName: senderName, targetName: targetName} - ) + ' ' + reason; - } - else if (ev.getPrevContent().membership === "invite") { - return _t( - '%(senderName)s withdrew %(targetName)s\'s invitation.', - {senderName: senderName, targetName: targetName} - ) + ' ' + reason; - } - else { - return _t('%(targetName)s left the room.', {targetName: targetName}); + } else if (prevContent.membership === "ban") { + return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); + } else if (prevContent.membership === "join") { + return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason; + } else if (prevContent.membership === "invite") { + return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { + senderName, + targetName, + }) + ' ' + reason; + } else { + return _t('%(targetName)s left the room.', {targetName}); } } } function textForTopicEvent(ev) { - var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic}); + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { + senderDisplayName, + topic: ev.getContent().topic, + }); } function textForRoomNameEvent(ev) { - var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { - return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName}); + return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); } - return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name}); + return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { + senderDisplayName, + roomName: ev.getContent().name, + }); } function textForMessageEvent(ev) { - var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - var message = senderDisplayName + ': ' + ev.getContent().body; + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + let message = senderDisplayName + ': ' + ev.getContent().body; if (ev.getContent().msgtype === "m.emote") { message = "* " + senderDisplayName + " " + message; } else if (ev.getContent().msgtype === "m.image") { - message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName}); + message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); } return message; } function textForCallAnswerEvent(event) { - var senderName = event.sender ? event.sender.name : _t('Someone'); - var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); - return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported; + const senderName = event.sender ? event.sender.name : _t('Someone'); + const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); + return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; } function textForCallHangupEvent(event) { @@ -159,20 +166,23 @@ function textForCallHangupEvent(event) { } function textForCallInviteEvent(event) { - var senderName = event.sender ? event.sender.name : _t('Someone'); + const senderName = event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? - var type = "voice"; + let callType = "voice"; if (event.getContent().offer && event.getContent().offer.sdp && event.getContent().offer.sdp.indexOf('m=video') !== -1) { - type = "video"; + callType = "video"; } - var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); - return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported; + const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported; } function textForThreePidInviteEvent(event) { - var senderName = event.sender ? event.sender.name : event.getSender(); - return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name}); + const senderName = event.sender ? event.sender.name : event.getSender(); + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { + senderName, + targetDisplayName: event.getContent().display_name, + }); } function textForHistoryVisibilityEvent(event) { @@ -197,8 +207,11 @@ function textForHistoryVisibilityEvent(event) { } function textForEncryptionEvent(event) { - var senderName = event.sender ? event.sender.name : event.getSender(); - return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm}); + const senderName = event.sender ? event.sender.name : event.getSender(); + return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', { + senderName, + algorithm: event.getContent().algorithm, + }); } // Currently will only display a change if a user's power level is changed @@ -209,18 +222,18 @@ function textForPowerEvent(event) { } const userDefault = event.getContent().users_default || 0; // Construct set of userIds - let users = []; + const users = []; Object.keys(event.getContent().users).forEach( (userId) => { if (users.indexOf(userId) === -1) users.push(userId); - } + }, ); Object.keys(event.getPrevContent().users).forEach( (userId) => { if (users.indexOf(userId) === -1) users.push(userId); - } + }, ); - let diff = []; + const diff = []; // XXX: This is also surely broken for i18n users.forEach((userId) => { // Previous power level @@ -229,11 +242,11 @@ function textForPowerEvent(event) { const to = event.getContent().users[userId]; if (to !== from) { diff.push( - _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { userId: userId, fromPowerLevel: Roles.textualPowerLevel(from, userDefault), - toPowerLevel: Roles.textualPowerLevel(to, userDefault) - }) + toPowerLevel: Roles.textualPowerLevel(to, userDefault), + }), ); } }); @@ -276,14 +289,14 @@ function textForWidgetEvent(event) { } } -var handlers = { +const handlers = { 'm.room.message': textForMessageEvent, - 'm.room.name': textForRoomNameEvent, - 'm.room.topic': textForTopicEvent, - 'm.room.member': textForMemberEvent, - 'm.call.invite': textForCallInviteEvent, - 'm.call.answer': textForCallAnswerEvent, - 'm.call.hangup': textForCallHangupEvent, + 'm.room.name': textForRoomNameEvent, + 'm.room.topic': textForTopicEvent, + 'm.room.member': textForMemberEvent, + 'm.call.invite': textForCallInviteEvent, + 'm.call.answer': textForCallAnswerEvent, + 'm.call.hangup': textForCallHangupEvent, 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, @@ -294,8 +307,8 @@ var handlers = { module.exports = { textForEvent: function(ev) { - var hdlr = handlers[ev.getType()]; - if (!hdlr) return ""; + const hdlr = handlers[ev.getType()]; + if (!hdlr) return ''; return hdlr(ev); - } + }, }; From b8dca58f4f239edfc987ff472a6ce8cd3ece8872 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 25 Sep 2017 10:02:13 +0100 Subject: [PATCH 07/23] Pass matrixClient as an argument to GSS constructor --- src/components/structures/GroupView.js | 4 +++- src/stores/GroupSummaryStore.js | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d3eb97180a..77034c3594 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -430,7 +430,9 @@ export default React.createClass({ }, _loadGroupFromServer: function(groupId) { - this._groupSummaryStore = new GroupSummaryStore(this.props.groupId); + this._groupSummaryStore = new GroupSummaryStore( + MatrixClientPeg.get(), this.props.groupId, + ); this._groupSummaryStore.on('update', () => { this.setState({ summary: this._groupSummaryStore.getSummary(), diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupSummaryStore.js index a26f0f2d61..170a1ec11e 100644 --- a/src/stores/GroupSummaryStore.js +++ b/src/stores/GroupSummaryStore.js @@ -15,21 +15,21 @@ limitations under the License. */ import EventEmitter from 'events'; -import MatrixClientPeg from '../MatrixClientPeg'; /** * Stores the group summary for a room and provides an API to change it */ export default class GroupSummaryStore extends EventEmitter { - constructor(groupId) { + constructor(matrixClient, groupId) { super(); this._groupId = groupId; + this._matrixClient = matrixClient; this._summary = {}; this._fetchSummary(); } _fetchSummary() { - MatrixClientPeg.get().getGroupSummary(this._groupId).then((resp) => { + this._matrixClient.getGroupSummary(this._groupId).then((resp) => { this._summary = resp; this._notifyListeners(); }).catch((err) => { @@ -46,25 +46,25 @@ export default class GroupSummaryStore extends EventEmitter { } addRoomToGroupSummary(roomId, categoryId) { - return MatrixClientPeg.get() + return this._matrixClient .addRoomToGroupSummary(this._groupId, roomId, categoryId) .then(this._fetchSummary.bind(this)); } addUserToGroupSummary(userId, roleId) { - return MatrixClientPeg.get() + return this._matrixClient .addUserToGroupSummary(this._groupId, userId, roleId) .then(this._fetchSummary.bind(this)); } removeRoomFromGroupSummary(roomId) { - return MatrixClientPeg.get() + return this._matrixClient .removeRoomFromGroupSummary(this._groupId, roomId) .then(this._fetchSummary.bind(this)); } removeUserFromGroupSummary(userId) { - return MatrixClientPeg.get() + return this._matrixClient .removeUserFromGroupSummary(this._groupId, userId) .then(this._fetchSummary.bind(this)); } From e2f8bb2ec02fedeb961670b91986f95e6e9d0368 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 25 Sep 2017 10:25:21 +0100 Subject: [PATCH 08/23] Fix NPE in MemberList _getChildCountInvited would throw an NPE if invoked before the js-sdk had found the room. Make sure we initialise the state correctly. --- src/components/views/rooms/MemberList.js | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index babbfcf649..56933b5d7f 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -18,12 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import classNames from 'classnames'; -import Matrix from 'matrix-js-sdk'; -import Promise from 'bluebird'; var MatrixClientPeg = require("../../../MatrixClientPeg"); -var Modal = require("../../../Modal"); -var Entities = require("../../../Entities"); var sdk = require('../../../index'); var GeminiScrollbar = require('react-gemini-scrollbar'); var rate_limited_func = require('../../../ratelimitedfunc'); @@ -36,25 +31,20 @@ module.exports = React.createClass({ displayName: 'MemberList', getInitialState: function() { - const state = { - members: [], + this.memberDict = this.getMemberDict(); + const members = this.roomMembers(); + + return { + members: members, + filteredJoinedMembers: this._filterMembers(members, 'join'), + filteredInvitedMembers: this._filterMembers(members, 'invite'), + // ideally we'd size this to the page height, but // in practice I find that a little constraining truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS, truncateAtInvited: INITIAL_LOAD_NUM_INVITED, searchQuery: "", }; - if (!this.props.roomId) return state; - var cli = MatrixClientPeg.get(); - var room = cli.getRoom(this.props.roomId); - if (!room) return state; - - this.memberDict = this.getMemberDict(); - - state.members = this.roomMembers(); - state.filteredJoinedMembers = this._filterMembers(state.members, 'join'); - state.filteredInvitedMembers = this._filterMembers(state.members, 'invite'); - return state; }, componentWillMount: function() { From 1a82f121c09f1fc20230f4a60a10fa3f42b72fae Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Sep 2017 13:17:07 +0100 Subject: [PATCH 09/23] lint --- src/components/structures/GroupView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index f74e3d60dd..0a8bfaee38 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -671,7 +671,6 @@ export default React.createClass({ if (!group) return null; if (group.myMembership === 'invite') { - if (this.state.membershipBusy) { return
@@ -727,7 +726,7 @@ export default React.createClass({ onClick={this._onPubliciseOnClick} > {_t("Make public")} - + ; } publicisedSection =
{_t("Your membership of this group is private")} From 5b312ffb03bdeb4465530ec29aba3473c725c113 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Sep 2017 14:38:43 +0100 Subject: [PATCH 10/23] Show displayname / avatar in group member info --- src/components/views/groups/GroupMemberInfo.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 8bf63e635a..3e0e82433c 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -154,10 +154,14 @@ module.exports = withMatrixClient(React.createClass({ const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const avatar = ( - + ); - const groupMemberName = this.props.groupMember.userId; + const groupMemberName = ( + this.props.groupMember.displayname || this.props.groupMember.userId + ); const EmojiText = sdk.getComponent('elements.EmojiText'); return ( From 8e7d58797dbb0462ada9b5209e8e7a6239b8277c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 25 Sep 2017 14:48:49 +0100 Subject: [PATCH 11/23] _loadGroupFromServer -> _initGroupSummaryStore --- src/components/structures/GroupView.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 77034c3594..969a58bf9f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -403,7 +403,7 @@ export default React.createClass({ componentWillMount: function() { this._changeAvatarComponent = null; - this._loadGroupFromServer(this.props.groupId); + this._initGroupSummaryStore(this.props.groupId); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, @@ -418,7 +418,7 @@ export default React.createClass({ summary: null, error: null, }, () => { - this._loadGroupFromServer(newProps.groupId); + this._initGroupSummaryStore(newProps.groupId); }); } }, @@ -429,7 +429,7 @@ export default React.createClass({ this.setState({membershipBusy: false}); }, - _loadGroupFromServer: function(groupId) { + _initGroupSummaryStore: function(groupId) { this._groupSummaryStore = new GroupSummaryStore( MatrixClientPeg.get(), this.props.groupId, ); @@ -516,7 +516,7 @@ export default React.createClass({ editing: false, summary: null, }); - this._loadGroupFromServer(this.props.groupId); + this._initGroupSummaryStore(this.props.groupId); }).catch((e) => { this.setState({ saving: false, From 83e6218930baa64845dafbaa4d25455643af2cba Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 25 Sep 2017 14:49:12 +0100 Subject: [PATCH 12/23] Remove listeners from group summary store on unmount --- src/components/structures/GroupView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 969a58bf9f..21ec733f3f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -410,6 +410,7 @@ export default React.createClass({ componentWillUnmount: function() { MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); + this._groupSummaryStore.removeAllListeners(); }, componentWillReceiveProps: function(newProps) { From 58ec732fb36961dd113a9a47071e772b1adb259d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Sep 2017 14:51:21 +0100 Subject: [PATCH 13/23] Add width/height/resizeMethod params --- src/components/views/groups/GroupMemberInfo.js | 7 ++++++- src/components/views/groups/GroupMemberTile.js | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 3e0e82433c..e66c635f1f 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -152,10 +152,15 @@ module.exports = withMatrixClient(React.createClass({
; } + const avatarUrl = this.props.matrixClient.mxcUrlToHttp( + this.props.groupMember.avatarUrl, + 36, 36, 'crop' + ); + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const avatar = ( ); diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index 705fa56cae..66d45e2f71 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -48,11 +48,15 @@ export default withMatrixClient(React.createClass({ const EntityTile = sdk.getComponent('rooms.EntityTile'); const name = this.props.member.displayname || this.props.member.userId; + const avatarUrl = this.props.matrixClient.mxcUrlToHttp( + this.props.member.avatarUrl, + 36, 36, 'crop' + ); const av = ( ); From 0b96871be4919998bae8a7c13988329455cbe9f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Sep 2017 15:07:02 +0100 Subject: [PATCH 14/23] Lint --- src/components/views/groups/GroupMemberInfo.js | 2 +- src/components/views/groups/GroupMemberTile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index e66c635f1f..499fb43ccb 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -154,7 +154,7 @@ module.exports = withMatrixClient(React.createClass({ const avatarUrl = this.props.matrixClient.mxcUrlToHttp( this.props.groupMember.avatarUrl, - 36, 36, 'crop' + 36, 36, 'crop', ); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index 66d45e2f71..f40c7ed1c5 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -50,7 +50,7 @@ export default withMatrixClient(React.createClass({ const name = this.props.member.displayname || this.props.member.userId; const avatarUrl = this.props.matrixClient.mxcUrlToHttp( this.props.member.avatarUrl, - 36, 36, 'crop' + 36, 36, 'crop', ); const av = ( From 20c731f627fe232f3ea9337578fcc9c937941ba8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 25 Sep 2017 15:21:56 +0100 Subject: [PATCH 15/23] Bust the flair caches after 30mins Group profile data and the groups a user has publicised will be removed from the cache 30mins after retrieval. There may be some benefits to caching the group profiles for longer than the group memberships but for now they're naively busted after the same 30mins. --- src/components/views/elements/Flair.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index cce115dc45..e5860f81e2 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -29,6 +29,9 @@ const BULK_REQUEST_DEBOUNCE_MS = 200; // If true, flair can function and we should keep sending requests for groups and avatars. let groupSupport = true; +const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins +const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins + // TODO: Cache-busting based on time. (The server won't inform us of membership changes.) // This applies to userGroups and groupProfiles. We can provide a slightly better UX by // cache-busting when the current user joins/leaves a group. @@ -69,7 +72,9 @@ function getPublicisedGroupsCached(matrixClient, userId) { usersPending[userId].reject = reject; }).then((groups) => { userGroups[userId] = groups; - // TODO: Reset cache at this point + setTimeout(() => { + delete userGroups[userId]; + }, USER_GROUPS_CACHE_BUST_MS); return userGroups[userId]; }).catch((err) => { throw err; @@ -126,6 +131,9 @@ async function getGroupProfileCached(matrixClient, groupId) { groupId, avatarUrl: profile.avatar_url, }; + setTimeout(() => { + delete groupProfiles[groupId]; + }, GROUP_PROFILES_CACHE_BUST_MS); return groupProfiles[groupId]; } From 796924ef35124dbdc86a753c43a34249f2086111 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@googlemail.com> Date: Mon, 25 Sep 2017 15:49:48 +0100 Subject: [PATCH 16/23] Fix typo as found by Luke --- src/TextForEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 400a04f6b3..a21eb5c251 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -67,7 +67,7 @@ function textForMemberEvent(ev) { senderName, oldDisplayName: prevContent.displayname, }); - } else if (prevContent.avatar_url && !prevContent.avatar_url) { + } else if (prevContent.avatar_url && !content.avatar_url) { return _t('%(senderName)s removed their profile picture.', {senderName}); } else if (prevContent.avatar_url && content.avatar_url && prevContent.avatar_url !== content.avatar_url) { From 7f1d8834a25faed2a691ed0b0fa8d6dd48cd4818 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 25 Sep 2017 17:12:37 +0200 Subject: [PATCH 17/23] Add setupScreenSharingForIframe to BasePlatform --- src/BasePlatform.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 5f8772c7aa..abc9aa0bed 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -107,6 +107,9 @@ export default class BasePlatform { isElectron(): boolean { return false; } + setupScreenSharingForIframe() { + } + /** * Restarts the application, without neccessarily reloading * any application code From 6e49926228368f64d650d641298ca4f54204fc2c Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 25 Sep 2017 17:13:18 +0200 Subject: [PATCH 18/23] AppTile: Do not test for electron platform The method platform method is instead stubbed on all other platforms. --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 5a5e266df8..2e6a4dab2d 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -136,7 +136,7 @@ export default React.createClass({ }, _onMessage(event) { - if (!PlatformPeg.get().isElectron() || this.props.type !== 'jitsi') { + if (this.props.type !== 'jitsi') { return; } if (!event.origin) { From 24de01e21de03d8abdcc112ea67d43a581c72e5a Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 25 Sep 2017 17:14:25 +0200 Subject: [PATCH 19/23] AppTile: Test if widgetUrl startsWith instead of has a substring The event origin should be at the beginning of the URL. --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 2e6a4dab2d..f4e4702989 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -143,7 +143,7 @@ export default React.createClass({ event.origin = event.originalEvent.origin; } - if (this.state.widgetUrl.indexOf(event.origin) === -1) { + if (!this.state.widgetUrl.startsWith(event.origin)) { return; } From 8d0983ab02bd0329c6331f0e5e3a11aa111e0ddd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Sep 2017 14:46:57 +0100 Subject: [PATCH 20/23] Fix group membership publicity * Read the new flag in the summary API (the one we were reading was actually whether the group server listed you as a member to non-members). * Remove call to now-dead _loadGroupFromServer andf use the store instead --- src/components/structures/GroupView.js | 14 ++++++-------- src/i18n/strings/en_EN.json | 8 ++++---- src/stores/GroupSummaryStore.js | 6 ++++++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 6a30c1ce41..41e58851ff 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -597,9 +597,7 @@ export default React.createClass({ this.setState({ publicityBusy: true, }); - MatrixClientPeg.get().setGroupPublicity(this.props.groupId, publicity).then(() => { - this._loadGroupFromServer(this.props.groupId); - }).then(() => { + this._groupSummaryStore.setGroupPublicity(publicity).then(() => { this.setState({ publicityBusy: false, }); @@ -730,16 +728,16 @@ export default React.createClass({ } let publicisedSection; - if (this.state.summary.user && this.state.summary.user.is_public) { + if (this.state.summary.user && this.state.summary.user.is_publicised) { if (!this.state.publicityBusy) { publicisedButton = - {_t("Make private")} + {_t("Unpublish")} ; } publicisedSection =
- {_t("Your membership of this group is public")} + {_t("This group is published on your profile")}
{publicisedButton}
@@ -749,11 +747,11 @@ export default React.createClass({ publicisedButton = - {_t("Make public")} + {_t("Publish")} ; } publicisedSection =
- {_t("Your membership of this group is private")} + {_t("This group is not published on your profile")}
{publicisedButton}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9dd479c789..8584f2eb2f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -883,8 +883,8 @@ "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.", "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", "The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.", - "Your membership of this group is public": "Your membership of this group is public", - "Your membership of this group is private": "Your membership of this group is private", - "Make private": "Make private", - "Make public": "Make public" + "Unpublish": "Unpublish", + "This group is published on your profile": "This group is published on your profile", + "Publish": "Publish", + "This group is not published on your profile": "This group is not published on your profile" } diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupSummaryStore.js index 170a1ec11e..d066f44b40 100644 --- a/src/stores/GroupSummaryStore.js +++ b/src/stores/GroupSummaryStore.js @@ -68,4 +68,10 @@ export default class GroupSummaryStore extends EventEmitter { .removeUserFromGroupSummary(this._groupId, userId) .then(this._fetchSummary.bind(this)); } + + setGroupPublicity(is_published) { + return this._matrixClient + .setGroupPublicity(this._groupId, is_published) + .then(this._fetchSummary.bind(this)); + } } From ddab8d7b5c70d5d3ab2490a656693e08ade3acb8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 26 Sep 2017 14:49:13 +0100 Subject: [PATCH 21/23] Implement "Add room to group" feature --- src/{GroupInvite.js => GroupAddressPicker.js} | 46 +++++- .../views/dialogs/AddressPickerDialog.js | 29 +++- .../views/groups/GroupMemberList.js | 2 +- src/components/views/groups/GroupRoomList.js | 143 ++++++++++++++++++ src/components/views/groups/GroupRoomTile.js | 89 +++++++++++ src/groups.js | 16 ++ src/i18n/strings/en_EN.json | 7 +- 7 files changed, 328 insertions(+), 4 deletions(-) rename src/{GroupInvite.js => GroupAddressPicker.js} (59%) create mode 100644 src/components/views/groups/GroupRoomList.js create mode 100644 src/components/views/groups/GroupRoomTile.js diff --git a/src/GroupInvite.js b/src/GroupAddressPicker.js similarity index 59% rename from src/GroupInvite.js rename to src/GroupAddressPicker.js index e04e90d751..c8cff0ab3e 100644 --- a/src/GroupInvite.js +++ b/src/GroupAddressPicker.js @@ -18,11 +18,12 @@ import Modal from './Modal'; import sdk from './'; import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; +import MatrixClientPeg from './MatrixClientPeg'; export function showGroupInviteDialog(groupId) { const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { - title: _t('Invite new group members'), + title: _t("Invite new group members"), description: _t("Who would you like to add to this group?"), placeholder: _t("Name or matrix ID"), button: _t("Invite to Group"), @@ -35,6 +36,25 @@ export function showGroupInviteDialog(groupId) { }); } +export function showGroupAddRoomDialog(groupId) { + return new Promise((resolve, reject) => { + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); + Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { + title: _t("Add rooms to the group"), + description: _t("Which rooms would you like to add to this group?"), + placeholder: _t("Room name or alias"), + button: _t("Add to group"), + pickerType: 'room', + validAddressTypes: ['mx'], + onFinished: (success, addrs) => { + if (!success) return; + + _onGroupAddRoomFinished(groupId, addrs).then(resolve, reject); + }, + }); + }); +} + function _onGroupInviteFinished(groupId, addrs) { const multiInviter = new MultiInviter(groupId); @@ -65,3 +85,27 @@ function _onGroupInviteFinished(groupId, addrs) { }); } +function _onGroupAddRoomFinished(groupId, addrs) { + const errorList = []; + return Promise.all(addrs.map((addr) => { + return MatrixClientPeg.get() + .addRoomToGroup(groupId, addr.address) + .catch(() => { errorList.push(addr.address); }) + .reflect(); + })).then(() => { + if (errorList.length === 0) { + return; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog( + 'Failed to add the following room to the group', + '', ErrorDialog, + { + title: _t( + "Failed to add the following rooms to %(groupId)s:", + {groupId}, + ), + description: errorList.join(", "), + }); + }); +} diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 741ff1fe8c..523dd9ad0d 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -155,7 +155,7 @@ module.exports = React.createClass({ if (this.props.groupId) { this._doNaiveGroupRoomSearch(query); } else { - console.error('Room searching only implemented for groups'); + this._doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -264,6 +264,33 @@ module.exports = React.createClass({ }); }, + _doRoomSearch: function(query) { + MatrixClientPeg.get().publicRooms({ + filter: { + generic_search_term: query, + }, + }).then((resp) => { + const results = []; + resp.chunk.forEach((r) => { + results.push({ + room_id: r.room_id, + avatar_url: r.avatar_url, + name: r.name, + }); + }); + this._processResults(results, query); + }).catch((err) => { + console.error('Error whilst searching public rooms: ', err); + this.setState({ + searchError: err.errcode ? err.message : _t('Something went wrong!'), + }); + }).done(() => { + this.setState({ + busy: false, + }); + }); + }, + _doUserDirectorySearch: function(query) { this.setState({ busy: true, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 273a04da20..809dac93d0 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -131,7 +131,7 @@ export default withMatrixClient(React.createClass({ const inputBox = (
-
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js new file mode 100644 index 0000000000..0a9dbb4d76 --- /dev/null +++ b/src/components/views/groups/GroupRoomList.js @@ -0,0 +1,143 @@ +/* +Copyright 2017 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import { groupRoomFromApiObject } from '../../../groups'; +import GeminiScrollbar from 'react-gemini-scrollbar'; +import PropTypes from 'prop-types'; +import {MatrixClient} from 'matrix-js-sdk'; + +const INITIAL_LOAD_NUM_ROOMS = 30; + +export default React.createClass({ + contextTypes: { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + }, + + propTypes: { + groupId: PropTypes.string.isRequired, + }, + + getInitialState: function() { + return { + fetching: false, + rooms: null, + truncateAt: INITIAL_LOAD_NUM_ROOMS, + searchQuery: "", + }; + }, + + componentWillMount: function() { + this._unmounted = false; + this._fetchRooms(); + }, + + _fetchRooms: function() { + this.setState({fetching: true}); + this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => { + this.setState({ + rooms: result.chunk.map((apiRoom) => { + return groupRoomFromApiObject(apiRoom); + }), + fetching: false, + }); + }).catch((e) => { + this.setState({fetching: false}); + console.error("Failed to get group room list: ", e); + }); + }, + + _createOverflowTile: function(overflowCount, totalCount) { + // For now we'll pretend this is any entity. It should probably be a separate tile. + const EntityTile = sdk.getComponent("rooms.EntityTile"); + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + const text = _t("and %(count)s others...", { count: overflowCount }); + return ( + + } name={text} presenceState="online" suppressOnHover={true} + onClick={this._showFullRoomList} /> + ); + }, + + _showFullRoomList: function() { + this.setState({ + truncateAt: -1, + }); + }, + + onSearchQueryChanged: function(ev) { + this.setState({ searchQuery: ev.target.value }); + }, + + makeGroupRoomTiles: function(query) { + const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile"); + query = (query || "").toLowerCase(); + + let roomList = this.state.rooms; + if (query) { + roomList = roomList.filter((room) => { + const matchesName = (room.name || "").toLowerCase().include(query); + const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query); + return matchesName || matchesAlias; + }); + } + + roomList = roomList.map((groupRoom, index) => { + return ( + + ); + }); + + return roomList; + }, + + render: function() { + if (this.state.fetching) { + const Spinner = sdk.getComponent("elements.Spinner"); + return (
+ +
); + } else if (this.state.rooms === null) { + return null; + } + + const inputBox = ( +
+ +
+ ); + + const TruncatedList = sdk.getComponent("elements.TruncatedList"); + return ( +
+ { inputBox } + + + {this.makeGroupRoomTiles(this.state.searchQuery)} + + +
+ ); + }, +}); diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js new file mode 100644 index 0000000000..771e3d9c4f --- /dev/null +++ b/src/components/views/groups/GroupRoomTile.js @@ -0,0 +1,89 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 {MatrixClient} from 'matrix-js-sdk'; +import { _t } from '../../../languageHandler'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import { GroupRoomType } from '../../../groups'; + +const GroupRoomTile = React.createClass({ + displayName: 'GroupRoomTile', + + propTypes: { + groupId: PropTypes.string.isRequired, + groupRoom: GroupRoomType.isRequired, + }, + + getInitialState: function() { + return {}; + }, + + onClick: function(e) { + let roomId; + let roomAlias; + if (this.props.groupRoom.canonicalAlias) { + roomAlias = this.props.groupRoom.canonicalAlias; + } else { + roomId = this.props.groupRoom.roomId; + } + dis.dispatch({ + action: 'view_room', + room_id: roomId, + room_alias: roomAlias, + }); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const name = this.props.groupRoom.name || + this.props.groupRoom.canonicalAlias || + _t("Unnamed Room"); + const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + this.props.groupRoom.avatarUrl, + 36, 36, 'crop', + ); + + const av = ( + + ); + + return ( + +
+ {av} +
+
+ {name} +
+
+ ); + }, +}); + +GroupRoomTile.contextTypes = { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, +}; + + +export default GroupRoomTile; diff --git a/src/groups.js b/src/groups.js index 24c0562bde..2ff95f7d65 100644 --- a/src/groups.js +++ b/src/groups.js @@ -22,6 +22,14 @@ export const GroupMemberType = PropTypes.shape({ avatarUrl: PropTypes.string, }); +export const GroupRoomType = PropTypes.shape({ + name: PropTypes.string, + // TODO: API doesn't return this yet + // roomId: PropTypes.string.isRequired, + canonicalAlias: PropTypes.string, + avatarUrl: PropTypes.string, +}); + export function groupMemberFromApiObject(apiObject) { return { userId: apiObject.user_id, @@ -29,3 +37,11 @@ export function groupMemberFromApiObject(apiObject) { avatarUrl: apiObject.avatar_url, }; } + +export function groupRoomFromApiObject(apiObject) { + return { + name: apiObject.name, + canonicalAlias: apiObject.canonical_alias, + avatarUrl: apiObject.avatar_url, + }; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9dd479c789..2e7c6d513c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -846,6 +846,7 @@ "Robot check is currently unavailable on desktop - please use a web browser": "Robot check is currently unavailable on desktop - please use a web browser", "Description": "Description", "Filter group members": "Filter group members", + "Filter group rooms": "Filter group rooms", "Remove from group": "Remove from group", "Invite new group members": "Invite new group members", "Who would you like to add to this group?": "Who would you like to add to this group?", @@ -886,5 +887,9 @@ "Your membership of this group is public": "Your membership of this group is public", "Your membership of this group is private": "Your membership of this group is private", "Make private": "Make private", - "Make public": "Make public" + "Make public": "Make public", + "Add rooms to the group": "Add rooms to the group", + "Which rooms would you like to add to this group?": "Which rooms would you like to add to this group?", + "Add to group": "Add to group", + "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:" } From 8ec1c3ecf410eb266efd1f574016a2c66ea07e78 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Sep 2017 14:58:49 +0100 Subject: [PATCH 22/23] lint --- src/stores/GroupSummaryStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupSummaryStore.js index d066f44b40..aa6e74529b 100644 --- a/src/stores/GroupSummaryStore.js +++ b/src/stores/GroupSummaryStore.js @@ -69,9 +69,9 @@ export default class GroupSummaryStore extends EventEmitter { .then(this._fetchSummary.bind(this)); } - setGroupPublicity(is_published) { + setGroupPublicity(isPublished) { return this._matrixClient - .setGroupPublicity(this._groupId, is_published) + .setGroupPublicity(this._groupId, isPublished) .then(this._fetchSummary.bind(this)); } } From 20d65535c02832d626c993d5f8cbe06b9b32e2db Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 26 Sep 2017 17:11:54 +0100 Subject: [PATCH 23/23] Use all known rooms instead of public rooms --- .../views/dialogs/AddressPickerDialog.js | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 523dd9ad0d..22cf242fcf 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -248,7 +248,7 @@ module.exports = React.createClass({ results.push({ room_id: r.room_id, avatar_url: r.avatar_url, - name: r.name, + name: r.name || r.canonical_alias, }); }); this._processResults(results, query); @@ -265,30 +265,34 @@ module.exports = React.createClass({ }, _doRoomSearch: function(query) { - MatrixClientPeg.get().publicRooms({ - filter: { - generic_search_term: query, - }, - }).then((resp) => { - const results = []; - resp.chunk.forEach((r) => { - results.push({ - room_id: r.room_id, - avatar_url: r.avatar_url, - name: r.name, - }); - }); - this._processResults(results, query); - }).catch((err) => { - console.error('Error whilst searching public rooms: ', err); - this.setState({ - searchError: err.errcode ? err.message : _t('Something went wrong!'), - }); - }).done(() => { - this.setState({ - busy: false, + const lowerCaseQuery = query.toLowerCase(); + const rooms = MatrixClientPeg.get().getRooms(); + const results = []; + rooms.forEach((room) => { + const nameEvent = room.currentState.getStateEvents('m.room.name', ''); + const topicEvent = room.currentState.getStateEvents('m.room.topic', ''); + const name = nameEvent ? nameEvent.getContent().name : ''; + const canonicalAlias = room.getCanonicalAlias(); + const topic = topicEvent ? topicEvent.getContent().topic : ''; + + const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery); + const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery); + const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery); + if (!(nameMatch || topicMatch || aliasMatch)) { + return; + } + const avatarEvent = room.currentState.getStateEvents('m.room.avatar', ''); + const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined; + results.push({ + room_id: room.roomId, + avatar_url: avatarUrl, + name: name || canonicalAlias, }); }); + this._processResults(results, query); + this.setState({ + busy: false, + }); }, _doUserDirectorySearch: function(query) {