diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 427f549eb0..1979c6d111 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -21,6 +21,8 @@ import Modal from './Modal'; import { getAddressType } from './UserAddress'; import createRoom from './createRoom'; import sdk from './'; +import dis from './dispatcher'; +import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; export function inviteToRoom(roomId, addr) { @@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); if (_isDmChat(addrTexts)) { - // Start a new DM chat - createRoom({dmUserId: addrTexts[0]}).catch((err) => { - console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { - title: _t("Failed to invite user"), - description: ((err && err.message) ? err.message : _t("Operation failed")), + const rooms = _getDirectMessageRooms(addrTexts[0]); + if (rooms.length > 0) { + // A Direct Message room already exists for this user, so select a + // room from a list that is similar to the one in MemberInfo panel + const ChatCreateOrReuseDialog = sdk.getComponent( + "views.dialogs.ChatCreateOrReuseDialog", + ); + const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { + userId: addrTexts[0], + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: addrTexts[0], + }); + close(true); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + close(true); + }, + }).close; + } else { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); }); - }); + } } else { // Start multi user chat let room; @@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) { return addrs; } +function _getDirectMessageRooms(addr) { + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); + const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); + const rooms = []; + dmRooms.forEach((dmRoom) => { + const room = MatrixClientPeg.get().getRoom(dmRoom); + if (room) { + const me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (me.membership == 'join') { + rooms.push(room); + } + } + }); + return rooms; +} + diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 314a36e486..1564efd8d3 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -447,7 +447,7 @@ export default React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { const summary = this._groupStore.getSummary(); if (summary.profile) { // Default profile fields should be "" for later sending to the server (which @@ -464,7 +464,6 @@ export default React.createClass({ }); }); this._groupStore.on('error', (err) => { - console.error(err); this.setState({ summary: null, error: err, @@ -964,13 +963,15 @@ export default React.createClass({ , ); } else { - rightButtons.push( - - - , - ); + if (summary.user && summary.user.membership === 'join') { + rightButtons.push( + + + , + ); + } if (this.props.collapsedRhs) { rightButtons.push(
- {_t("Remove + {_t("Remove
- +
{ avatar } diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index a5ab22eb0e..8658ac19a5 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -50,12 +50,9 @@ export default withMatrixClient(React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { this._fetchMembers(); }); - this._groupStore.on('error', (err) => { - console.error(err); - }); }, _fetchMembers: function() { diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index 3fcfedd486..aeded2dfb0 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -47,16 +47,14 @@ export default React.createClass({ _initGroupStore: function(groupId) { this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - this._groupStore.on('update', () => { + this._groupStore.registerListener(() => { this._fetchRooms(); }); this._groupStore.on('error', (err) => { - console.error('Error in group store (listened to by GroupRoomList)', err); this.setState({ rooms: null, }); }); - this._fetchRooms(); }, _fetchRooms: function() { diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 23e53996e3..94dc8e593f 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -120,8 +120,11 @@ const GroupRoomTile = React.createClass({
{ this.state.name }
- - + + ); diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 083d7ac12e..9a172baf7c 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -25,7 +25,10 @@ module.exports = React.createClass({ render: function() { let tooltip = _t("Removed or unknown message type"); if (this.props.mxEvent.isRedacted()) { - tooltip = _t("Message removed by %(userId)s", {userId: this.props.mxEvent.getSender()}); + const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender; + tooltip = redactedBecauseUserId ? + _t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) : + _t("Message removed"); } const text = this.props.mxEvent.getContent().body; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 10c809e0f1..d69a8fbcf1 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -133,8 +133,9 @@ module.exports = React.createClass({ { p["og:description"] }
- + ); }, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index ae36f4e021..c043b3714d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -39,6 +39,7 @@ import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import GeminiScrollbar from 'react-gemini-scrollbar'; +import RoomViewStore from '../../../stores/RoomViewStore'; module.exports = withMatrixClient(React.createClass({ @@ -81,6 +82,7 @@ module.exports = withMatrixClient(React.createClass({ cli.on("Room.receipt", this.onRoomReceipt); cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); + cli.on("RoomMember.membership", this.onRoomMemberMembership); cli.on("accountData", this.onAccountData); this._checkIgnoreState(); @@ -107,6 +109,7 @@ module.exports = withMatrixClient(React.createClass({ client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("RoomState.events", this.onRoomStateEvents); client.removeListener("RoomMember.name", this.onRoomMemberName); + client.removeListener("RoomMember.membership", this.onRoomMemberMembership); client.removeListener("accountData", this.onAccountData); } if (this._cancelDeviceList) { @@ -186,6 +189,10 @@ module.exports = withMatrixClient(React.createClass({ this.forceUpdate(); }, + onRoomMemberMembership: function(ev, member) { + if (this.props.member.userId === member.userId) this.forceUpdate(); + }, + onAccountData: function(ev) { if (ev.getType() === 'm.direct') { this.forceUpdate(); @@ -615,6 +622,8 @@ module.exports = withMatrixClient(React.createClass({ const member = this.props.member; let ignoreButton = null; + let insertPillButton = null; + let inviteUserButton = null; let readReceiptButton = null; // Only allow the user to ignore the user if its not ourselves @@ -639,22 +648,58 @@ module.exports = withMatrixClient(React.createClass({ }); }; + const onInsertPillButton = function() { + dis.dispatch({ + action: 'insert_mention', + user_id: member.userId, + }); + }; + readReceiptButton = ( { _t('Jump to read receipt') } ); + + insertPillButton = ( + + { _t('Mention') } + + ); + } + + if (!member || !member.membership || member.membership === 'leave') { + const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); + const onInviteUserButton = async () => { + try { + await cli.invite(roomId, member.userId); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t('Failed to invite'), + description: ((err && err.message) ? err.message : "Operation failed"), + }); + } + }; + + inviteUserButton = ( + + { _t('Invite') } + + ); } } - if (!ignoreButton && !readReceiptButton) return null; + if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null; return (

{ _t("User Options") }

{ readReceiptButton } + { insertPillButton } { ignoreButton } + { inviteUserButton }
); @@ -760,9 +805,6 @@ module.exports = withMatrixClient(React.createClass({
; } - // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet - // e.g. clicking on a linkified userid in a room - let adminTools; if (kickButton || banButton || muteButton || giveModButton) { adminTools = @@ -790,9 +832,29 @@ module.exports = withMatrixClient(React.createClass({ presenceCurrentlyActive = this.props.member.user.currentlyActive; } + let roomMemberDetails = null; + + if (this.props.member.roomId) { // is in room + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); + roomMemberDetails =
+
+ { _t("Level:") } + + +
+
+ +
+
; + } + const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const EmojiText = sdk.getComponent('elements.EmojiText'); return (
@@ -808,16 +870,7 @@ module.exports = withMatrixClient(React.createClass({
{ this.props.member.userId }
-
- { _t("Level:") } - - -
-
- -
+ { roomMemberDetails }
{ this._renderUserOptions() } diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js index deea03f030..5b1b8a4590 100644 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -95,7 +95,9 @@ module.exports = React.createClass({ return (
- + + +

{ _t("Pinned Messages") }

{ tiles }
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4df0ff738c..4dfbdb3644 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -281,8 +281,11 @@ module.exports = React.createClass({
- {_t("Remove + {_t("Remove
); diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index 8f062d27ae..748673f1a5 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -18,10 +18,13 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; +import classNames from 'classnames'; import sdk from '../../../index'; import dis from '../../../dispatcher'; +import UserSettingsStore from '../../../UserSettingsStore'; + module.exports = React.createClass({ displayName: 'VideoView', @@ -108,14 +111,18 @@ module.exports = React.createClass({ document.mozFullScreenElement || document.webkitFullscreenElement); const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight; - + const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed", + { "mx_VideoView_localVideoFeed_flipped": + UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false), + }, + ); return (
-
+
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 94b9e7bfc3..6fd82757f0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -152,6 +152,7 @@ "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "Communities": "Communities", "Message Pinning": "Message Pinning", + "Mention": "Mention", "%(displayName)s is typing": "%(displayName)s is typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", @@ -200,8 +201,6 @@ "Authentication": "Authentication", "Failed to delete device": "Failed to delete device", "Delete": "Delete", - "Delete Widget": "Delete Widget", - "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", "Cannot add any more widgets": "Cannot add any more widgets", @@ -245,6 +244,7 @@ "Unignore": "Unignore", "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", + "Invite": "Invite", "User Options": "User Options", "Direct chats": "Direct chats", "Unmute": "Unmute", @@ -456,6 +456,7 @@ "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", + "Message removed": "Message removed", "Robot check is currently unavailable on desktop - please use a web browser": "Robot check is currently unavailable on desktop - please use a web browser", "This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot", "Sign in with CAS": "Sign in with CAS", @@ -500,10 +501,13 @@ "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Remove": "Remove", + "Remove this room from the community": "Remove this room from the community", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Do you want to load widget from URL:": "Do you want to load widget from URL:", "Allow": "Allow", + "Delete Widget": "Delete Widget", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", "Revoke widget access": "Revoke widget access", "Edit": "Edit", @@ -688,8 +692,8 @@ "Featured Rooms:": "Featured Rooms:", "Featured Users:": "Featured Users:", "%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community", - "You are a member of this community": "You are a member of this community", "You are an administrator of this community": "You are an administrator of this community", + "You are a member of this community": "You are a member of this community", "Community Member Settings": "Community Member Settings", "Publish this community on your profile": "Publish this community on your profile", "Long Description (HTML)": "Long Description (HTML)", @@ -759,6 +763,7 @@ "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Hide avatars in user and room mentions": "Hide avatars in user and room mentions", "Disable big emoji in chat": "Disable big emoji in chat", + "Mirror local video feed": "Mirror local video feed", "Opt out of analytics": "Opt out of analytics", "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 1da1c35a2b..66bc293b44 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -29,9 +29,10 @@ export default class GroupStore extends EventEmitter { this._matrixClient = matrixClient; this._summary = {}; this._rooms = []; - this._fetchSummary(); - this._fetchRooms(); - this._fetchMembers(); + + this.on('error', (err) => { + console.error(`GroupStore for ${this.groupId} encountered error`, err); + }); } _fetchMembers() { @@ -51,6 +52,10 @@ export default class GroupStore extends EventEmitter { }); this._notifyListeners(); }).catch((err) => { + // Invited users not visible to non-members + if (err.httpStatus === 403) { + return; + } console.error("Failed to get group invited member list: " + err); this.emit('error', err); }); @@ -80,6 +85,17 @@ export default class GroupStore extends EventEmitter { this.emit('update'); } + registerListener(fn) { + this.on('update', fn); + this._fetchSummary(); + this._fetchRooms(); + this._fetchMembers(); + } + + unregisterListener(fn) { + this.removeListener('update', fn); + } + getSummary() { return this._summary; } diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 1618fe4cfe..436133c717 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -88,6 +88,9 @@ describe('MemberEventListSummary', function() { sandbox = testUtils.stubClient(); languageHandler.setLanguage('en').done(done); + languageHandler.setMissingEntryGenerator(function(key) { + return key.split('|', 2)[1]; + }); }); afterEach(function() {