diff --git a/res/css/_components.scss b/res/css/_components.scss index 9823b4ac3d..4b8b687146 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -124,6 +124,7 @@ @import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; +@import "./views/messages/_ViewSourceEvent.scss"; @import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index ec6d903753..cc5649a224 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -16,17 +16,22 @@ limitations under the License. .mx_MessageEditor { border-radius: 4px; - background-color: $header-panel-bg-color; - padding: 11px 13px 7px 56px; + padding: 3px; + // this is to try not make the text move but still have some + // padding around and in the editor. + // Actual values from fiddling around in inspector + margin: -7px -10px -5px -10px; .mx_MessageEditor_editor { border-radius: 4px; - border: solid 1px #e9edf1; - background-color: #ffffff; - padding: 10px; + border: solid 1px $primary-hairline-color; + background-color: $primary-bg-color; + padding: 3px 6px; white-space: pre-wrap; word-wrap: break-word; outline: none; + max-height: 200px; + overflow-x: auto; span { display: inline-block; @@ -48,8 +53,15 @@ limitations under the License. .mx_MessageEditor_buttons { display: flex; flex-direction: row; - justify-content: end; - padding: 5px 0; + justify-content: flex-end; + padding: 5px; + position: absolute; + left: 0; + background: $header-panel-bg-color; + z-index: 100; + right: 0; + margin: 0 -110px 0 0; + padding-right: 104px; .mx_AccessibleButton { margin-left: 5px; @@ -62,3 +74,8 @@ limitations under the License. height: 0; } } + +.mx_EventTile_last .mx_MessageEditor_buttons { + position: static; + margin-right: -103px; +} diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss new file mode 100644 index 0000000000..a15924e759 --- /dev/null +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -0,0 +1,50 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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_EventTile_content.mx_ViewSourceEvent { + display: flex; + opacity: 0.6; + font-size: 12px; + + pre, code { + flex: 1; + } + + pre { + line-height: 1.2; + margin: 3.5px 0; + } + + .mx_ViewSourceEvent_toggle { + width: 12px; + mask-repeat: no-repeat; + mask-position: 0 center; + mask-size: auto 12px; + visibility: hidden; + background-color: $accent-color; + mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + } + + &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { + mask-position: 0 bottom; + margin-bottom: 7px; + mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); + } + + &:hover .mx_ViewSourceEvent_toggle { + visibility: visible; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 173b6db536..e67d9d4107 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -43,6 +43,10 @@ limitations under the License. padding-top: 0px ! important; } +.mx_EventTile_isEditing { + background-color: $header-panel-bg-color; +} + .mx_EventTile .mx_SenderProfile { color: $primary-fg-color; font-size: 14px; @@ -72,6 +76,10 @@ limitations under the License. } } +.mx_EventTile_isEditing .mx_MessageTimestamp { + visibility: hidden !important; +} + .mx_EventTile .mx_MessageTimestamp { display: block; visibility: hidden; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index f85ce00171..d06c31682d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -473,8 +473,8 @@ export function bodyToHtml(content, highlights, opts={}) { }); return isDisplayedWithHtml ? - : - { strippedBody }; + : + { strippedBody }; } /** diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index dbaab57adf..9fd3bf508b 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../DateUtils'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; +import SettingsStore from '../../settings/SettingsStore'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -248,6 +249,10 @@ module.exports = React.createClass({ return false; // ignored = no show (only happens if the ignore happens after an event was received) } + if (SettingsStore.getValue("showHiddenEventsInTimeline")) { + return true; + } + const EventTile = sdk.getComponent('rooms.EventTile'); if (!EventTile.haveTileForEvent(mxEv)) { return false; // no tile = no show @@ -450,14 +455,10 @@ module.exports = React.createClass({ _getTilesForEvent: function(prevEvent, mxEv, last) { const EventTile = sdk.getComponent('rooms.EventTile'); - const MessageEditor = sdk.getComponent('elements.MessageEditor'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const ret = []; - if (this.props.editEvent && this.props.editEvent.getId() === mxEv.getId()) { - return []; - } - + const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId(); // is this a continuation of the previous message? let continuation = false; @@ -527,6 +528,7 @@ module.exports = React.createClass({ continuation={continuation} isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} + isEditing={isEditing} onHeightChanged={this._onHeightChanged} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index dcf8aba88c..0065fb208f 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,11 +14,13 @@ 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 PropTypes from 'prop-types'; import sdk from '../../../index'; -const MemberAvatar = require('../avatars/MemberAvatar.js'); +import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; +import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; module.exports = React.createClass({ displayName: 'MemberEventListSummary', @@ -105,7 +108,7 @@ module.exports = React.createClass({ ); }); - const desc = this._renderCommaSeparatedList(descs); + const desc = formatCommaSeparatedList(descs); return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc }); }); @@ -128,7 +131,7 @@ module.exports = React.createClass({ * included before "and [n] others". */ _renderNameList: function(users) { - return this._renderCommaSeparatedList(users, this.props.summaryLength); + return formatCommaSeparatedList(users, this.props.summaryLength); }, /** @@ -279,35 +282,6 @@ module.exports = React.createClass({ return res; }, - /** - * Constructs a written English string representing `items`, with an optional limit on - * the number of items included in the result. If specified and if the length of - *`items` is greater than the limit, the string "and n others" will be appended onto - * the result. - * If `items` is empty, returns the empty string. If there is only one item, return - * it. - * @param {string[]} items the items to construct a string from. - * @param {number?} itemLimit the number by which to limit the list. - * @returns {string} a string constructed by joining `items` with a comma between each - * item, but with the last item appended as " and [lastItem]". - */ - _renderCommaSeparatedList(items, itemLimit) { - const remaining = itemLimit === undefined ? 0 : Math.max( - items.length - itemLimit, 0, - ); - if (items.length === 0) { - return ""; - } else if (items.length === 1) { - return items[0]; - } else if (remaining > 0) { - items = items.slice(0, itemLimit); - return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); - } else { - const lastItem = items.pop(); - return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); - } - }, - _renderAvatars: function(roomMembers) { const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => { return ( diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index c18d1f56fd..0c249d067b 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -107,10 +107,12 @@ export default class MessageEditor extends React.Component { } else if (event.key === "Enter") { this._sendEdit(); event.preventDefault(); + } else if (event.key === "Escape") { + this._cancelEdit(); } } - _onCancelClicked = () => { + _cancelEdit = () => { dis.dispatch({action: "edit_event", event: null}); } @@ -185,7 +187,7 @@ export default class MessageEditor extends React.Component { ref={ref => this._editorRef = ref} >
- {_t("Cancel")} + {_t("Cancel")} {_t("Save")}
; diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 357da1cd10..8c90ec5a46 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ tileShape={this.props.tileShape} maxImageHeight={this.props.maxImageHeight} replacingEventId={this.props.replacingEventId} + isEditing={this.props.isEditing} onHeightChanged={this.props.onHeightChanged} />; }, }); diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index 5a71bbdf84..394812fd38 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -21,6 +21,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import { unicodeToShortcode } from '../../../HtmlUtils'; import { _t } from '../../../languageHandler'; +import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; export default class ReactionsRowButtonTooltip extends React.PureComponent { static propTypes = { @@ -54,7 +55,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { { reactors: () => { return
- {senders.join(", ")} + {formatCommaSeparatedList(senders, 6)}
; }, reactedWith: (sub) => { diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 3d9807878d..5356572bb9 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -89,7 +89,9 @@ module.exports = React.createClass({ componentDidMount: function() { this._unmounted = false; - this._applyFormatting(); + if (!this.props.isEditing) { + this._applyFormatting(); + } }, _applyFormatting() { @@ -128,11 +130,14 @@ module.exports = React.createClass({ }, componentDidUpdate: function(prevProps) { - const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; - if (messageWasEdited) { - this._applyFormatting(); + if (!this.props.isEditing) { + const stoppedEditing = prevProps.isEditing && !this.props.isEditing; + const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; + if (messageWasEdited || stoppedEditing) { + this._applyFormatting(); + } + this.calculateUrlPreview(); } - this.calculateUrlPreview(); }, componentWillUnmount: function() { @@ -148,6 +153,7 @@ module.exports = React.createClass({ nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || nextProps.showUrlPreview !== this.props.showUrlPreview || + nextProps.isEditing !== this.props.isEditing || nextState.links !== this.state.links || nextState.editedMarkerHovered !== this.state.editedMarkerHovered || nextState.widgetHidden !== this.state.widgetHidden); @@ -463,6 +469,10 @@ module.exports = React.createClass({ }, render: function() { + if (this.props.isEditing) { + const MessageEditor = sdk.getComponent('elements.MessageEditor'); + return ; + } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js new file mode 100644 index 0000000000..62cf45fb6e --- /dev/null +++ b/src/components/views/messages/ViewSourceEvent.js @@ -0,0 +1,67 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class ViewSourceEvent extends React.PureComponent { + static propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + expanded: false, + }; + } + + onToggle = (ev) => { + ev.preventDefault(); + const { expanded } = this.state; + this.setState({ + expanded: !expanded, + }); + } + + render() { + const { mxEvent } = this.props; + const { expanded } = this.state; + + let content; + if (expanded) { + content =
{JSON.stringify(mxEvent, null, 4)}
; + } else { + content = {`{ "type": ${mxEvent.getType()} }`}; + } + + const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", { + mx_ViewSourceEvent_expanded: expanded, + }); + + return + {content} + + ; + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 2267c942ba..4b5acf949e 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -520,7 +520,10 @@ module.exports = withMatrixClient(React.createClass({ eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' ); - const tileHandler = getHandlerTile(this.props.mxEvent); + let tileHandler = getHandlerTile(this.props.mxEvent); + if (!tileHandler && SettingsStore.getValue("showHiddenEventsInTimeline")) { + tileHandler = "messages.ViewSourceEvent"; + } // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!tileHandler) { @@ -540,6 +543,7 @@ module.exports = withMatrixClient(React.createClass({ const classes = classNames({ mx_EventTile: true, + mx_EventTile_isEditing: this.props.isEditing, mx_EventTile_info: isInfoMessage, mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting', @@ -617,14 +621,14 @@ module.exports = withMatrixClient(React.createClass({ } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); - const actionBar = ; + /> : undefined; const timestamp = this.props.mxEvent.getTs() ? : null; @@ -779,6 +783,7 @@ module.exports = withMatrixClient(React.createClass({ {flags} + ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 86131645cf..ad5cdd248d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -253,6 +253,9 @@ "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", + "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", + "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -333,6 +336,7 @@ "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show developer tools": "Show developer tools", "Order rooms in the room list by most important first instead of most recent": "Order rooms in the room list by most important first instead of most recent", + "Show hidden events in timeline": "Show hidden events in timeline", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -1045,9 +1049,6 @@ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", - "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", - "%(items)s and %(count)s others|one": "%(items)s and one other", - "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "collapse": "collapse", "expand": "expand", "Power level": "Power level", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 429030d862..116526b63a 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -368,4 +368,9 @@ export const SETTINGS = { displayName: _td('Order rooms in the room list by most important first instead of most recent'), default: true, }, + "showHiddenEventsInTimeline": { + displayName: _td("Show hidden events in timeline"), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, }; diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.js index b461d22079..1fd7d00feb 100644 --- a/src/utils/FormattingUtils.js +++ b/src/utils/FormattingUtils.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _t } from '../languageHandler'; + /** * formats numbers to fit into ~3 characters, suitable for badge counts * e.g: 999, 9.9K, 99K, 0.9M, 9.9M, 99M, 0.9B, 9.9B @@ -63,3 +66,31 @@ export function getUserNameColorClass(userId) { const colorNumber = (hashCode(userId) % 8) + 1; return `mx_Username_color${colorNumber}`; } + +/** + * Constructs a written English string representing `items`, with an optional + * limit on the number of items included in the result. If specified and if the + * length of `items` is greater than the limit, the string "and n others" will + * be appended onto the result. If `items` is empty, returns the empty string. + * If there is only one item, return it. + * @param {string[]} items the items to construct a string from. + * @param {number?} itemLimit the number by which to limit the list. + * @returns {string} a string constructed by joining `items` with a comma + * between each item, but with the last item appended as " and [lastItem]". + */ +export function formatCommaSeparatedList(items, itemLimit) { + const remaining = itemLimit === undefined ? 0 : Math.max( + items.length - itemLimit, 0, + ); + if (items.length === 0) { + return ""; + } else if (items.length === 1) { + return items[0]; + } else if (remaining > 0) { + items = items.slice(0, itemLimit); + return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); + } else { + const lastItem = items.pop(); + return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); + } +}