diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index d6d9302f..a07d1162 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -43,26 +43,40 @@ module.exports = React.createClass({ getInitialState: function() { return { canRedact: false, + canPin: false, }; }, componentWillMount: function() { - MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkCanRedact); - this._checkCanRedact(); + MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); + this._checkPermissions(); }, componentWillUnmount: function() { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener('RoomMember.powerLevel', this._checkCanRedact); + cli.removeListener('RoomMember.powerLevel', this._checkPermissions); } }, - _checkCanRedact: function() { + _checkPermissions: function() { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId); - this.setState({canRedact}); + let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); + + // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality + if (!UserSettingsStore.isFeatureEnabled("feature_pinning")) canPin = false; + + this.setState({canRedact, canPin}); + }, + + _isPinned: function() { + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); + if (!pinnedEvent) return false; + return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId()); }, onResendClick: function() { @@ -122,6 +136,28 @@ module.exports = React.createClass({ this.closeMenu(); }, + onPinClick: function() { + MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '') + .catch(e => { + // Intercept the Event Not Found error and fall through the promise chain with no event. + if (e.errcode === "M_NOT_FOUND") return null; + throw e; + }) + .then(event => { + const eventIds = (event ? event.pinned : []) || []; + if (!eventIds.includes(this.props.mxEvent.getId())) { + // Not pinned - add + eventIds.push(this.props.mxEvent.getId()); + } else { + // Pinned - remove + eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1); + } + + MatrixClientPeg.get().sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); + }); + this.closeMenu(); + }, + closeMenu: function() { if (this.props.onFinished) this.props.onFinished(); }, @@ -147,6 +183,7 @@ module.exports = React.createClass({ let redactButton; let cancelButton; let forwardButton; + let pinButton; let viewSourceButton; let viewClearSourceButton; let unhidePreviewButton; @@ -186,6 +223,14 @@ module.exports = React.createClass({ { _t('Forward Message') } ); + + if (this.state.canPin) { + pinButton = ( +
+ {this._isPinned() ? _t('Unpin Message') : _t('Pin Message')} +
+ ); + } } } @@ -246,6 +291,7 @@ module.exports = React.createClass({ {redactButton} {cancelButton} {forwardButton} + {pinButton} {viewSourceButton} {viewClearSourceButton} {unhidePreviewButton} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 74e59f5f..16d47d10 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -200,6 +200,11 @@ "You have successfully set a password!": "You have successfully set a password!", "You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.", "Continue": "Continue", + "Pin Message": "Pin Message", + "Unpin Message": "Unpin Message", + "Jump to message": "Jump to message", + "No pinned messages.": "No pinned messages.", + "Loading...": "Loading...", "Please set a password!": "Please set a password!", "This will allow you to return to your account after signing out, and sign in on other devices.": "This will allow you to return to your account after signing out, and sign in on other devices.", "You have successfully set a password and an email address!": "You have successfully set a password and an email address!", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 8d7eb15d..ef3eff35 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -185,6 +185,11 @@ "You have successfully set a password and an email address!": "You have successfully set a password and an email address!", "Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.", "Warning": "Warning", + "Pin Message": "Pin Message", + "Unpin Message": "Unpin Message", + "Jump to message": "Jump to message", + "No pinned messages.": "No pinned messages.", + "Loading...": "Loading...", "Checking for an update...": "Checking for an update...", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "No update available.": "No update available.", diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss index 809c6384..76ac3945 100644 --- a/src/skins/vector/css/_components.scss +++ b/src/skins/vector/css/_components.scss @@ -69,6 +69,8 @@ @import "./matrix-react-sdk/views/voip/_CallView.scss"; @import "./matrix-react-sdk/views/voip/_IncomingCallbox.scss"; @import "./matrix-react-sdk/views/voip/_VideoView.scss"; +@import "./matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss"; +@import "./matrix-react-sdk/views/rooms/_PinnedEventTile.scss"; @import "./vector-web/_fonts.scss"; @import "./vector-web/structures/_CompatibilityPage.scss"; @import "./vector-web/structures/_HomePage.scss"; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventTile.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventTile.scss new file mode 100644 index 00000000..ca790ef8 --- /dev/null +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventTile.scss @@ -0,0 +1,67 @@ +/* +Copyright 2017 Travis Ralston + +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. +*/ + +.mx_PinnedEventTile { + min-height: 40px; + margin-bottom: 5px; + width: 100%; + border-radius: 5px; // for the hover +} + +.mx_PinnedEventTile:hover { + background-color: $event-selected-color; +} + +.mx_PinnedEventTile .mx_PinnedEventTile_sender { + color: #868686; + font-size: 0.8em; + vertical-align: top; + display: block; + padding-bottom: 3px; +} + +.mx_PinnedEventTile .mx_EventTile_content { + margin-left: 50px; + position: relative; + top: 0; + left: 0; +} + +.mx_PinnedEventTile .mx_BaseAvatar { + float: left; + margin-right: 10px; +} + +.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions { + display: block; +} + +.mx_PinnedEventTile_actions { + float: right; + margin-right: 10px; + display: none; +} + +.mx_PinnedEventTile_unpinButton { + display: inline-block; + cursor: pointer; + margin-left: 10px; +} + +.mx_PinnedEventTile_gotoButton { + display: inline-block; + font-size: 0.8em; +} diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss new file mode 100644 index 00000000..663d5bdf --- /dev/null +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss @@ -0,0 +1,37 @@ +/* +Copyright 2017 Travis Ralston + +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. +*/ + +.mx_PinnedEventsPanel { + border-top: 1px solid $primary-hairline-color; +} + +.mx_PinnedEventsPanel_body { + max-height: 300px; + overflow-y: auto; + padding-bottom: 15px; +} + +.mx_PinnedEventsPanel_header { + margin: 0; + padding-top: 8px; + padding-bottom: 15px; +} + +.mx_PinnedEventsPanel_cancel { + margin: 12px; + float: right; + display: inline-block; +} diff --git a/src/skins/vector/img/icons-pin.svg b/src/skins/vector/img/icons-pin.svg new file mode 100644 index 00000000..a6fbf13b --- /dev/null +++ b/src/skins/vector/img/icons-pin.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file