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/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 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/TextForEvent.js b/src/TextForEvent.js index 902c10307e..a21eb5c251 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 && !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) { + 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); - } + }, }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4f61bc4647..41e58851ff 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, @@ -379,18 +398,20 @@ export default React.createClass({ saving: false, uploadingAvatar: false, membershipBusy: false, + publicityBusy: false, }; }, componentWillMount: function() { this._changeAvatarComponent = null; - this._loadGroupFromServer(this.props.groupId); + this._initGroupSummaryStore(this.props.groupId); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, componentWillUnmount: function() { MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); + this._groupSummaryStore.removeAllListeners(); }, componentWillReceiveProps: function(newProps) { @@ -399,7 +420,7 @@ export default React.createClass({ summary: null, error: null, }, () => { - this._loadGroupFromServer(newProps.groupId); + this._initGroupSummaryStore(newProps.groupId); }); } }, @@ -410,13 +431,17 @@ export default React.createClass({ this.setState({membershipBusy: false}); }, - _loadGroupFromServer: function(groupId) { - MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { + _initGroupSummaryStore: function(groupId) { + this._groupSummaryStore = new GroupSummaryStore( + MatrixClientPeg.get(), 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, @@ -493,7 +518,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, @@ -560,7 +585,26 @@ export default React.createClass({ }); }, - _getFeaturedRoomsNode() { + _onPubliciseOffClick: function() { + this._setPublicity(false); + }, + + _onPubliciseOnClick: function() { + this._setPublicity(true); + }, + + _setPublicity: function(publicity) { + this.setState({ + publicityBusy: true, + }); + this._groupSummaryStore.setGroupPublicity(publicity).then(() => { + this.setState({ + publicityBusy: false, + }); + }); + }, + + _getFeaturedRoomsNode: function() { const summary = this.state.summary; const defaultCategoryRooms = []; @@ -601,7 +645,7 @@ export default React.createClass({ ; }, - _getFeaturedUsersNode() { + _getFeaturedUsersNode: function() { const summary = this.state.summary; const noRoleUsers = []; @@ -643,12 +687,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 +721,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_publicised) { + if (!this.state.publicityBusy) { + publicisedButton = + {_t("Unpublish")} + ; + } + publicisedSection =
+ {_t("This group is published on your profile")} +
+ {publicisedButton} +
+
; + } else { + if (!this.state.publicityBusy) { + publicisedButton = - {_t("Leave")} - + {_t("Publish")} +
; + } + publicisedSection =
+ {_t("This group is not published on your profile")} +
+ {publicisedButton} +
+
; + } + + return
+
+
+ {youAreAMemberText} +
+
+ + {_t("Leave")} + +
+ {publicisedSection}
; } diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 741ff1fe8c..22cf242fcf 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); @@ -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); @@ -264,6 +264,37 @@ module.exports = React.createClass({ }); }, + _doRoomSearch: function(query) { + 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) { this.setState({ busy: true, diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 1d7e4bd217..97bfa51eb8 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 (this.props.type !== 'jitsi') { + return; + } + if (!event.origin) { + event.origin = event.originalEvent.origin; + } + + if (!this.state.widgetUrl.startsWith(event.origin)) { + 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() { 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]; } diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 8bf63e635a..499fb43ccb 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -152,12 +152,21 @@ module.exports = withMatrixClient(React.createClass({
; } - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const avatar = ( - + const avatarUrl = this.props.matrixClient.mxcUrlToHttp( + this.props.groupMember.avatarUrl, + 36, 36, 'crop', ); - const groupMemberName = this.props.groupMember.userId; + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const avatar = ( + + ); + + const groupMemberName = ( + this.props.groupMember.displayname || this.props.groupMember.userId + ); const EmojiText = sdk.getComponent('elements.EmojiText'); return ( 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/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index 705fa56cae..f40c7ed1c5 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 = ( ); 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/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index babbfcf649..86d44f29d6 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'); @@ -31,30 +26,26 @@ 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', 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() { @@ -207,11 +198,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 +218,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, }); }, 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 f3e4b93e9c..cd17a2c9c0 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?", @@ -876,11 +877,22 @@ "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.", + "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.", "Light theme": "Light theme", "Dark theme": "Dark theme", - "Unknown": "Unknown" + "Unknown": "Unknown", + "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.", + "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:", + "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 new file mode 100644 index 0000000000..aa6e74529b --- /dev/null +++ b/src/stores/GroupSummaryStore.js @@ -0,0 +1,77 @@ +/* +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'; + +/** + * Stores the group summary for a room and provides an API to change it + */ +export default class GroupSummaryStore extends EventEmitter { + constructor(matrixClient, groupId) { + super(); + this._groupId = groupId; + this._matrixClient = matrixClient; + this._summary = {}; + this._fetchSummary(); + } + + _fetchSummary() { + this._matrixClient.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 this._matrixClient + .addRoomToGroupSummary(this._groupId, roomId, categoryId) + .then(this._fetchSummary.bind(this)); + } + + addUserToGroupSummary(userId, roleId) { + return this._matrixClient + .addUserToGroupSummary(this._groupId, userId, roleId) + .then(this._fetchSummary.bind(this)); + } + + removeRoomFromGroupSummary(roomId) { + return this._matrixClient + .removeRoomFromGroupSummary(this._groupId, roomId) + .then(this._fetchSummary.bind(this)); + } + + removeUserFromGroupSummary(userId) { + return this._matrixClient + .removeUserFromGroupSummary(this._groupId, userId) + .then(this._fetchSummary.bind(this)); + } + + setGroupPublicity(isPublished) { + return this._matrixClient + .setGroupPublicity(this._groupId, isPublished) + .then(this._fetchSummary.bind(this)); + } +}