diff --git a/res/css/_common.scss b/res/css/_common.scss index d3cf1921e0..d46f38bddb 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -557,4 +557,3 @@ textarea { .mx_Username_color8 { color: $username-variant8-color; } - diff --git a/res/css/_components.scss b/res/css/_components.scss index 6321c46ffc..2a91f08ee4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -120,10 +120,12 @@ @import "./views/messages/_ReactionDimension.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; +@import "./views/messages/_ReactionsRowButtonTooltip.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @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/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 2f01f3ecc6..1f5d36b57a 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -82,8 +82,13 @@ limitations under the License. display: inline-block; } -.mx_DevTools_content .mx_Field_input + .mx_Field_input { - margin-left: 42px; +.mx_DevTools_eventTypeStateKeyGroup { + display: flex; + flex-wrap: wrap; +} + +.mx_DevTools_content .mx_Field_input:first-of-type { + margin-right: 42px; } .mx_DevTools_tgl { 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/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 43ddf6dde5..3a6b6fb936 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -74,3 +74,19 @@ limitations under the License. animation: mx_fadeout 0.1s forwards; } } + +.mx_Tooltip_timeline { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + text-align: center; + border: none; + border-radius: 3px; + font-size: 14px; + line-height: 1.2; + padding: 6px 8px; + + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss new file mode 100644 index 0000000000..cf4219fcec --- /dev/null +++ b/res/css/views/messages/_ReactionsRowButtonTooltip.scss @@ -0,0 +1,19 @@ +/* +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_ReactionsRowButtonTooltip_reactedWith { + opacity: 0.7; +} 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 f4c12bb734..4442ccd1e4 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; @@ -377,6 +385,14 @@ limitations under the License. left: 41px; } +.mx_EventTile_content .mx_EventTile_edited { + user-select: none; + font-size: 12px; + color: $roomtopic-color; + display: inline-block; + margin-left: 9px; +} + /* Various markdown overrides */ .mx_EventTile_content .markdown-body { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 592b1a1887..bdccf71540 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -157,6 +157,9 @@ $reaction-row-button-hover-border-color: $header-panel-text-primary-color; $reaction-row-button-selected-bg-color: #1f6954; $reaction-row-button-selected-border-color: $accent-color; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fc15170b87..d11dfebda3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -265,6 +265,9 @@ $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/CallHandler.js b/src/CallHandler.js index acdc3e5122..e47209eebe 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -361,7 +361,7 @@ async function _startCallApp(roomId, type) { Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { title: _t('Could not connect to the integration server'), - description: _t('A conference call could not be started because the intgrations server is not available'), + description: _t('A conference call could not be started because the integrations server is not available'), }); return; } diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 532ee23c25..cd5ecc790d 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -38,7 +38,7 @@ export function showGroupInviteDialog(groupId) { Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { title: _t("Invite new community members"), description: description, - placeholder: _t("Name or matrix ID"), + placeholder: _t("Name or Matrix ID"), button: _t("Invite to Community"), validAddressTypes: ['mx-user-id'], onFinished: (success, addrs) => { diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d9d8bac93b..97f547ceb4 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -107,6 +107,17 @@ function unicodeToImage(str, addAlt) { return str; } +/** + * Returns the shortcode for an emoji character. + * + * @param {String} char The emoji character + * @return {String} The shortcode (such as :thumbup:) + */ +export function unicodeToShort(char) { + const unicode = emojione.jsEscapeMap[char]; + return emojione.mapUnicodeToShort()[unicode]; +} + /** * Given one or more unicode characters (represented by unicode * character number), return an image node with the corresponding @@ -530,8 +541,8 @@ export function bodyToHtml(content, highlights, opts={}) { }); return isDisplayedWithHtml ? - : - { strippedBody }; + : + { strippedBody }; } export function emojifyText(text, addAlt) { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index b808b935a6..34b9635780 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -45,7 +45,7 @@ export function showStartChatInviteDialog() { Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { title: _t('Start a chat'), description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or matrix ID"), + placeholder: _t("Email, name or Matrix ID"), validAddressTypes: ['mx-user-id', 'email'], button: _t("Start Chat"), onFinished: _onStartChatFinished, @@ -58,7 +58,7 @@ export function showRoomInviteDialog(roomId) { title: _t('Invite new room members'), description: _t('Who would you like to add to this room?'), button: _t('Send Invites'), - placeholder: _t("Email, name or matrix ID"), + placeholder: _t("Email, name or Matrix ID"), onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); }, diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 55107db899..f25bc9af07 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -518,7 +518,7 @@ export const CommandMap = { unban: new Command({ name: 'unban', args: '', - description: _td('Unbans user with given id'), + description: _td('Unbans user with given ID'), runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+)$/); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index dcbe212267..cdfbe26fea 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -265,7 +265,7 @@ const RoleUserList = React.createClass({ Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, { title: _t('Add users to the community summary'), description: _t("Who would you like to add to this summary?"), - placeholder: _t("Name or matrix ID"), + placeholder: _t("Name or Matrix ID"), button: _t("Add to summary"), validAddressTypes: ['mx-user-id'], groupId: this.props.groupId, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index dbaab57adf..d61092c051 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} @@ -714,7 +716,7 @@ module.exports = React.createClass({ ); let whoIsTyping; - if (this.props.room) { + if (this.props.room && !this.props.tileShape) { whoIsTyping = (
- { this.textInput('eventType', _t('Event Type')) } - { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) } +
+ { this.textInput('eventType', _t('Event Type')) } + { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) } +

diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js index 13d8e99258..bd6746a1e5 100644 --- a/src/components/views/dialogs/ShareDialog.js +++ b/src/components/views/dialogs/ShareDialog.js @@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component { top: y, message: successful ? _t('Copied!') : _t('Failed to copy'), }, false); - e.target.onmouseleave = close; + // Drop a reference to this close handler for componentWillUnmount + this.closeCopiedTooltip = e.target.onmouseleave = close; } onLinkSpecificEventCheckboxClick() { @@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component { } } + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape or clicking X really quickly + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + render() { let title; let matrixToUrl; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 3c58f90a2b..dc9c72df6e 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 }); }); @@ -132,7 +135,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); }, /** @@ -283,35 +286,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 b42923954b..0c249d067b 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -77,35 +77,46 @@ export default class MessageEditor extends React.Component { } _onKeyDown = (event) => { + // insert newline on Shift+Enter + if (event.shiftKey && event.key === "Enter") { + event.preventDefault(); // just in case the browser does support this + document.execCommand("insertHTML", undefined, "\n"); + return; + } + // autocomplete or enter to send below shouldn't have any modifier keys pressed. if (event.metaKey || event.altKey || event.shiftKey) { return; } - if (!this.model.autoComplete) { - return; + if (this.model.autoComplete) { + const autoComplete = this.model.autoComplete; + switch (event.key) { + case "Enter": + autoComplete.onEnter(event); break; + case "ArrowUp": + autoComplete.onUpArrow(event); break; + case "ArrowDown": + autoComplete.onDownArrow(event); break; + case "Tab": + autoComplete.onTab(event); break; + case "Escape": + autoComplete.onEscape(event); break; + default: + return; // don't preventDefault on anything else + } + event.preventDefault(); + } else if (event.key === "Enter") { + this._sendEdit(); + event.preventDefault(); + } else if (event.key === "Escape") { + this._cancelEdit(); } - const autoComplete = this.model.autoComplete; - switch (event.key) { - case "Enter": - autoComplete.onEnter(event); break; - case "ArrowUp": - autoComplete.onUpArrow(event); break; - case "ArrowDown": - autoComplete.onDownArrow(event); break; - case "Tab": - autoComplete.onTab(event); break; - case "Escape": - autoComplete.onEscape(event); break; - default: - return; // don't preventDefault on anything else - } - event.preventDefault(); } - _onCancelClicked = () => { + _cancelEdit = () => { dis.dispatch({action: "edit_event", event: null}); } - _onSaveClicked = () => { + _sendEdit = () => { const newContent = { "msgtype": "m.text", "body": textSerialize(this.model), @@ -144,12 +155,7 @@ export default class MessageEditor extends React.Component { componentDidMount() { this._updateEditorState(); - const sel = document.getSelection(); - const range = document.createRange(); - range.selectNodeContents(this._editorRef); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); + setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd()); this._editorRef.focus(); } @@ -181,8 +187,8 @@ export default class MessageEditor extends React.Component { ref={ref => this._editorRef = ref} >
- {_t("Cancel")} - {_t("Save")} + {_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/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js index 843254ade3..de33ad1a57 100644 --- a/src/components/views/messages/ReactionDimension.js +++ b/src/components/views/messages/ReactionDimension.js @@ -168,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent { return {items} ; diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index d55ecd6578..d3bf6a2035 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -116,8 +116,8 @@ export default class ReactionsRow extends React.PureComponent { return ; }); diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index 721147cdb8..19cae27b87 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -19,17 +19,28 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; export default class ReactionsRowButton extends React.PureComponent { static propTypes = { // The event we're displaying reactions for mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji content: PropTypes.string.isRequired, - count: PropTypes.number.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, // A possible Matrix event if the current user has voted for this type myReactionEvent: PropTypes.object, } + constructor(props) { + super(props); + + this.state = { + tooltipVisible: false, + }; + } + onClick = (ev) => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { @@ -48,18 +59,53 @@ export default class ReactionsRowButton extends React.PureComponent { } }; + onMouseOver = () => { + this.setState({ + // To avoid littering the DOM with a tooltip for every reaction, + // only render it on first use. + tooltipRendered: true, + tooltipVisible: true, + }); + } + + onMouseOut = () => { + this.setState({ + tooltipVisible: false, + }); + } + render() { - const { content, count, myReactionEvent } = this.props; + const ReactionsRowButtonTooltip = + sdk.getComponent('messages.ReactionsRowButtonTooltip'); + const { content, reactionEvents, myReactionEvent } = this.props; + + const count = reactionEvents.size; + if (!count) { + return null; + } const classes = classNames({ mx_ReactionsRowButton: true, mx_ReactionsRowButton_selected: !!myReactionEvent, }); + let tooltip; + if (this.state.tooltipRendered) { + tooltip = ; + } + return {content} {count} + {tooltip} ; } } diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js new file mode 100644 index 0000000000..e9ec58e8d0 --- /dev/null +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -0,0 +1,81 @@ +/* +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 MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import { unicodeToShort } from '../../../HtmlUtils'; +import { _t } from '../../../languageHandler'; +import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; + +export default class ReactionsRowButtonTooltip extends React.PureComponent { + static propTypes = { + // The event we're displaying reactions for + mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji + content: PropTypes.string.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, + visible: PropTypes.bool.isRequired, + } + + render() { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const { content, reactionEvents, mxEvent, visible } = this.props; + + const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + let tooltipLabel; + if (room) { + const senders = []; + for (const reactionEvent of reactionEvents) { + const { name } = room.getMember(reactionEvent.getSender()); + senders.push(name); + } + const shortName = unicodeToShort(content) || content; + tooltipLabel =
{_t( + "reacted with %(shortName)s", + { + shortName, + }, + { + reactors: () => { + return
+ {formatCommaSeparatedList(senders, 6)} +
; + }, + reactedWith: (sub) => { + return
+ {sub} +
; + }, + }, + )}
; + } + + let tooltip; + if (tooltipLabel) { + tooltip = ; + } + + return tooltip; + } +} diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 34769c060f..c59ee8be08 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -22,6 +22,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; +import {formatDate} from '../../../DateUtils'; import sdk from '../../../index'; import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from '../../../Modal'; @@ -88,7 +89,9 @@ module.exports = React.createClass({ componentDidMount: function() { this._unmounted = false; - this._applyFormatting(); + if (!this.props.isEditing) { + this._applyFormatting(); + } }, _applyFormatting() { @@ -127,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() { @@ -147,7 +153,9 @@ 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); }, @@ -432,7 +440,39 @@ module.exports = React.createClass({ }); }, + _onMouseEnterEditedMarker: function() { + this.setState({editedMarkerHovered: true}); + }, + + _onMouseLeaveEditedMarker: function() { + this.setState({editedMarkerHovered: false}); + }, + + _renderEditedMarker: function() { + let editedTooltip; + if (this.state.editedMarkerHovered) { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const editEvent = this.props.mxEvent.replacingEvent(); + const date = editEvent && formatDate(editEvent.getDate()); + editedTooltip = ; + } + return ( +
{editedTooltip}{`(${_t("edited")})`}
+ ); + }, + render: function() { + if (this.props.isEditing) { + const MessageEditor = sdk.getComponent('elements.MessageEditor'); + return ; + } const EmojiText = sdk.getComponent('elements.EmojiText'); const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); @@ -443,6 +483,9 @@ module.exports = React.createClass({ // Part of Replies fallback support stripReplyFallback: stripReply, }); + if (this.props.replacingEventId) { + body = [body, this._renderEditedMarker()]; + } if (this.props.highlightLink) { body = { body }; 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 f38e3c3946..4c0f0e628e 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; @@ -780,6 +784,7 @@ module.exports = withMatrixClient(React.createClass({ {flags} + ); diff --git a/src/editor/model.js b/src/editor/model.js index 85dd425b0e..13066897b9 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -61,6 +61,16 @@ export default class EditorModel { return null; } + getPositionAtEnd() { + if (this._parts.length) { + const index = this._parts.length - 1; + const part = this._parts[index]; + return new DocumentPosition(index, part.text.length); + } else { + return new DocumentPosition(0, 0); + } + } + serializeParts() { return this._parts.map(({type, text}) => {return {type, text};}); } @@ -88,7 +98,8 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - const newPosition = this._positionForOffset(caretOffset, true); + let newPosition = this._positionForOffset(caretOffset, true); + newPosition = newPosition.skipUneditableParts(this._parts); this._setActivePart(newPosition); this._updateCallback(newPosition); } @@ -172,21 +183,26 @@ export default class EditorModel { // part might be undefined here let part = this._parts[index]; const amount = Math.min(len, part.text.length - offset); - if (part.canEdit) { - const replaceWith = part.remove(offset, amount); - if (typeof replaceWith === "string") { - this._replacePart(index, this._partCreator.createDefaultPart(replaceWith)); - } - part = this._parts[index]; - // remove empty part - if (!part.text.length) { - this._removePart(index); + // don't allow 0 amount deletions + if (amount) { + if (part.canEdit) { + const replaceWith = part.remove(offset, amount); + if (typeof replaceWith === "string") { + this._replacePart(index, this._partCreator.createDefaultPart(replaceWith)); + } + part = this._parts[index]; + // remove empty part + if (!part.text.length) { + this._removePart(index); + } else { + index += 1; + } } else { - index += 1; + removedOffsetDecrease += offset; + this._removePart(index); } } else { - removedOffsetDecrease += offset; - this._removePart(index); + index += 1; } len -= amount; offset = 0; @@ -261,4 +277,13 @@ class DocumentPosition { get offset() { return this._offset; } + + skipUneditableParts(parts) { + const part = parts[this.index]; + if (part && !part.canEdit) { + return new DocumentPosition(this.index + 1, 0); + } else { + return this; + } + } } diff --git a/src/editor/parts.js b/src/editor/parts.js index a20b857fee..bf792b1ab9 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -57,7 +57,7 @@ class BasePart { appendUntilRejected(str) { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); - if (!this.acceptsInsertion(chr)) { + if (!this.acceptsInsertion(chr, i)) { this._text = this._text + str.substr(0, i); return str.substr(i); } @@ -180,8 +180,8 @@ class PillPart extends BasePart { } export class NewlinePart extends BasePart { - acceptsInsertion(chr) { - return this.text.length === 0 && chr === "\n"; + acceptsInsertion(chr, i) { + return (this.text.length + i) === 0 && chr === "\n"; } acceptsRemoval(position, chr) { @@ -205,6 +205,14 @@ export class NewlinePart extends BasePart { get type() { return "newline"; } + + // this makes the cursor skip this part when it is inserted + // rather than trying to append to it, which is what we want. + // As a newline can also be only one character, it makes sense + // as it can only be one character long. This caused #9741. + get canEdit() { + return false; + } } export class RoomPillPart extends PillPart { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f584291edb..0a79117310 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -35,7 +35,7 @@ "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Could not connect to the integration server": "Could not connect to the integration server", - "A conference call could not be started because the intgrations server is not available": "A conference call could not be started because the intgrations server is not available", + "A conference call could not be started because the integrations server is not available": "A conference call could not be started because the integrations server is not available", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "A call is already in progress!": "A call is already in progress!", @@ -80,7 +80,7 @@ "Who would you like to add to this community?": "Who would you like to add to this community?", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID", "Invite new community members": "Invite new community members", - "Name or matrix ID": "Name or matrix ID", + "Name or Matrix ID": "Name or Matrix ID", "Invite to Community": "Invite to Community", "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?", "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?", @@ -109,7 +109,7 @@ "Admin": "Admin", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", - "Email, name or matrix ID": "Email, name or matrix ID", + "Email, name or Matrix ID": "Email, name or Matrix ID", "Start Chat": "Start Chat", "Invite new room members": "Invite new room members", "Who would you like to add to this room?": "Who would you like to add to this room?", @@ -157,7 +157,7 @@ "Unrecognised room alias:": "Unrecognised room alias:", "Kicks user with given id": "Kicks user with given id", "Bans user with given id": "Bans user with given id", - "Unbans user with given id": "Unbans user with given id", + "Unbans user with given ID": "Unbans user with given ID", "Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you", "Ignored user": "Ignored user", "You are now ignoring %(userId)s": "You are now ignoring %(userId)s", @@ -255,6 +255,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?", @@ -335,6 +338,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", @@ -909,6 +913,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Error decrypting video": "Error decrypting video", + "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", @@ -918,6 +923,8 @@ "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", "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?", + "Edited at %(date)s": "Edited at %(date)s", + "edited": "edited", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", "Message removed": "Message removed", @@ -1044,9 +1051,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/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 1c6f782db4..8829b1a421 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -887,5 +887,8 @@ "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.", "Spanner": "Wrench", "Aeroplane": "Airplane", - "Cat": "Cat" + "Cat": "Cat", + "Sends the given message coloured as a rainbow": "Sends the given message colored as a rainbow", + "Sends the given emote coloured as a rainbow": "Sends the given emote colored as a rainbow", + "Unrecognised address": "Unrecognized address" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 76448ddcb2..515ff416c1 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -426,7 +426,7 @@ "Start new chat": "Aloita uusi keskustelu", "Failed to invite": "Kutsu epäonnistui", "Failed to invite user": "Käyttäjän kutsuminen epäonnistui", - "Failed to invite the following users to the %(roomName)s room:": "Seuraavian käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui:", + "Failed to invite the following users to the %(roomName)s room:": "Seuraavien käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui:", "Confirm Removal": "Varmista poistaminen", "Unknown error": "Tuntematon virhe", "Incorrect password": "Virheellinen salasana", @@ -469,7 +469,7 @@ "Riot does not have permission to send you notifications - please check your browser settings": "Riotilla ei ole oikeuksia lähettää sinulle ilmoituksia. Ole hyvä ja tarkista selaimen asetukset", "Riot was not given permission to send notifications - please try again": "Riot ei saannut lupaa lähettää ilmoituksia. Ole hyvä ja yritä uudelleen", "Room %(roomId)s not visible": "Huone %(roomId)s ei ole näkyvissä", - "%(roomName)s does not exist.": "%(roomName)s ei ole olemassa.", + "%(roomName)s does not exist.": "Huonetta %(roomName)s ei ole olemassa.", "%(roomName)s is not accessible at this time.": "%(roomName)s ei ole saatavilla tällä hetkellä.", "Seen by %(userName)s at %(dateTime)s": "Käyttäjän %(userName)s näkemä %(dateTime)s", "Send Reset Email": "Lähetä salasanan palautusviesti", @@ -943,7 +943,7 @@ "Send Custom Event": "Lähetä mukautettu tapahtuma", "Advanced notification settings": "Lisäasetukset ilmoituksille", "delete the alias.": "poista alias.", - "To return to your account in future you need to set a password": "Voidaksesi tulevaisuudessa palata tilillesi sinun pitää asettaa salasana", + "To return to your account in future you need to set a password": "Jotta voit jatkossa palata tilillesi, sinun pitää asettaa salasana", "Forget": "Unohda", "#example": "#esimerkki", "Hide panel": "Piilota paneeli", @@ -1346,11 +1346,11 @@ "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Jos olet aikaisemmin käyttänyt uudempaa versiota Riotista, istuntosi voi olla epäyhteensopiva tämän version kanssa. Sulje tämä ikkuna ja yritä uudemman version kanssa.", "The platform you're on": "Alusta, jolla olet", "Whether or not you're logged in (we don't record your username)": "Riippumatta siitä oletko kirjautunut sisään (emme tallenna käyttäjätunnustasi)", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Riippumatta siitä, että käytätkö muotoillun tekstin tilaa muotoilueditorissa", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Riippumatta siitä, käytätkö muotoillun tekstin tilaa muotoilueditorissa", "Your User Agent": "Selaintunnisteesi", "The information being sent to us to help make Riot.im better includes:": "Tietoihin, jota lähetetään Riot.im:ään palvelun parantamiseksi, sisältyy:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Niissä kohdissa, missä tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän ID:n, kyseinen tieto poistetaan ennen tiedon lähetystä palvelimelle.", - "A conference call could not be started because the intgrations server is not available": "Konferenssipuhelua ei pystytty aloittamaan, koska integraatiopalvelin ei ole käytettävissä", + "A conference call could not be started because the intgrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä", "A call is currently being placed!": "Puhelua ollaan aloittamassa!", "A call is already in progress!": "Puhelu on jo meneillään!", "Permission Required": "Lisäoikeuksia tarvitaan", @@ -1760,5 +1760,82 @@ "Recovery Method Removed": "Palautustapa poistettu", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Tämä laite on huomannut, että palautuksen salalauseesi ja avaimesi salatuille viesteille on poistettu.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "Jos teit tämän vahingossa, voit ottaa käyttöön salatut viestit tälle laitteelle, joka uudelleensalaa tämän laitteen keskusteluhistorian uudella palautustavalla.", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi." + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Riippumatta siitä, käytätkö 'leivänmuruja' (kuvia huonelistan yläpuolella)", + "Replying With Files": "Tiedostoilla vastaaminen", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Tiedostolla vastaaminen ei onnistu tällä kertaa. Haluatko ladata tiedoston vastaamatta?", + "The file '%(fileName)s' failed to upload.": "Tiedoston '%(fileName)s' lataaminen ei onnistunut.", + "The server does not support the room version specified.": "Palvelin ei tue määritettyä huoneversiota.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Vahvista, että haluat päivittää huoneen versiosta versioon .", + "Changes your avatar in this current room only": "Vaihtaa kuvasi vain nykyisessä huoneessa", + "Sends the given message coloured as a rainbow": "Lähettää viestin sateenkaaren väreissä", + "Sends the given emote coloured as a rainbow": "Lähettää emoten sateenkaaren väreissä", + "The user's homeserver does not support the version of the room.": "Käyttäjän kotipalvelin ei tue huoneen versiota.", + "Show recent room avatars above the room list": "Näytä viimeaikaiset huoneen kuvat huoneluettelon yläpuolella", + "Edit messages after they have been sent (refresh to apply changes)": "Muokkaa viestejä niiden lähettämisen jälkeen (päivitä saattaaksesi muutokset voimaan)", + "React to messages with emoji (refresh to apply changes)": "Reagoi viesteihin emojeilla (päivitä saattaaksesi muutokset voimaan)", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Tämä laite ei varmuuskopioi avaimiasi, mutta sinulla on olemassa varmuuskopio palauttamista ja lisäämistä varten.", + "Backup has an invalid signature from this device": "Varmuuskopiossa on epäkelpo allekirjoitus tältä laitteelta", + "this room": "tämä huone", + "View older messages in %(roomName)s.": "Näytä vanhemmat viestit huoneessa %(roomName)s.", + "Joining room …": "Liitytään huoneeseen …", + "Loading …": "Latataan …", + "Join the conversation with an account": "Liity keskusteluun tilin avulla", + "Sign Up": "Rekisteröidy", + "Sign In": "Kirjaudu", + "Reason: %(reason)s": "Syy: %(reason)s", + "Forget this room": "Unohda tämä huone", + "Re-join": "Liity uudelleen", + "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s antoi sinulle porttikiellon huoneeseen %(roomName)s", + "Something went wrong with your invite to %(roomName)s": "Jotain meni vikaan kutsussasi huoneeseen %(roomName)s", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "Kutsusi validointi palautti virhekoodin %(errcode)s. Voit koettaa välittää tiedon huoneen ylläpitäjälle.", + "You can only join it with a working invite.": "Voit liittyä siihen vain toimivalla kutsulla.", + "You can still join it because this is a public room.": "Voit silti liittyä siihen, koska huone on julkinen.", + "Join the discussion": "Liity keskusteluun", + "Try to join anyway": "Yritä silti liittyä", + "This invite to %(roomName)s wasn't sent to your account": "Kutsua huoneeseen %(roomName)s ei lähetetty tilillesi", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Kirjaudu eri tilillä, pyydä uutta kutsua tai lisää sähköpostiosoite %(email)s tähän tiliin.", + "Do you want to chat with %(user)s?": "Haluatko keskustella käyttäjän %(user)s kanssa?", + "Do you want to join %(roomName)s?": "Haluatko liittyä huoneeseen %(roomName)s?", + " invited you": " kutsui sinut", + "You're previewing %(roomName)s. Want to join it?": "Esikatselet huonetta %(roomName)s. Haluatko liittyä siihen?", + "%(roomName)s can't be previewed. Do you want to join it?": "Huonetta %(roomName)s ei voi esikatsella. Haluatko liittyä siihen?", + "This room doesn't exist. Are you sure you're at the right place?": "Tätä huonetta ei ole olemassa. Oletko varma, että olet oikeassa paikassa?", + "This room has already been upgraded.": "Tämä huone on jo päivitetty.", + "Rotate Left": "Kierrä vasempaan", + "Rotate counter-clockwise": "Kierrä vastapäivään", + "Rotate Right": "Kierrä oikeaan", + "Rotate clockwise": "Kierrä myötäpäivään", + "View Servers in Room": "Näytä huoneessa olevat palvelimet", + "Sign out and remove encryption keys?": "Kirjaudu ulos ja poista salausavaimet?", + "Missing session data": "Istunnon dataa puuttuu", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Istunnon dataa, mukaanlukien salausavaimia, puuttuu. Kirjaudu ulos ja sisään, jolloin avaimet palautetaan varmuuskopiosta.", + "Your browser likely removed this data when running low on disk space.": "Selaimesi luultavasti poisti tämän datan, kun levytila oli vähissä.", + "Upload files (%(current)s of %(total)s)": "Lataa tiedostot (%(current)s / %(total)s)", + "Upload files": "Lataa tiedostot", + "These files are too large to upload. The file size limit is %(limit)s.": "Tiedostot ovat liian isoja ladattaviksi. Tiedoston kokoraja on %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Osa tiedostoista on liian isoja ladattaviksi. Tiedoston kokoraja on %(limit)s.", + "Upload %(count)s other files|other": "Lataa %(count)s muuta tiedostoa", + "Upload %(count)s other files|one": "Lataa %(count)s muu tiedosto", + "Cancel All": "Peruuta kaikki", + "Upload Error": "Latausvirhe", + "Use an email address to recover your account": "Palauta tilisi sähköpostiosoitteen avulla", + "Enter email address (required on this homeserver)": "Syötä sähköpostiosoite (vaaditaan tällä kotipalvelimella)", + "Doesn't look like a valid email address": "Ei näytä kelvolliselta sähköpostiosoitteelta", + "Enter password": "Syötä salasana", + "Password is allowed, but unsafe": "Salasana on sallittu, mutta turvaton", + "Nice, strong password!": "Hyvä, vahva salasana!", + "Passwords don't match": "Salasanat eivät täsmää", + "Other users can invite you to rooms using your contact details": "Muut voivat kutsua sinut huoneisiin yhteystietojesi avulla", + "Enter phone number (required on this homeserver)": "Syötä puhelinnumero (vaaditaan tällä kotipalvelimella)", + "Doesn't look like a valid phone number": "Ei näytä kelvolliselta puhelinnumerolta", + "Use letters, numbers, dashes and underscores only": "Käytä vain kirjaimia, numeroita, viivoja ja alaviivoja", + "Enter username": "Syötä käyttäjänimi", + "Some characters not allowed": "Osaa merkeistä ei sallita", + "Use an email address to recover your account.": "Palauta tilisi sähköpostiosoitteen avulla.", + "Other users can invite you to rooms using your contact details.": "Muut käyttäjät voivat kutsua sinut huoneisiin yhteystietojesi avulla.", + "Error loading Riot": "Virhe Riotin lataamisessa", + "If this is unexpected, please contact your system administrator or technical support representative.": "Jos et odottanut tätä, ota yhteyttä järjestelmänvalvojaan tai tekniseen tukeen.", + "Homeserver URL does not appear to be a valid Matrix homeserver": "Kotipalvelimen osoite ei näytä olevan kelvollinen Matrix-kotipalvelin", + "Identity server URL does not appear to be a valid identity server": "Identiteettipalvelimen osoite ei näytä olevan kelvollinen identiteettipalvelin" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index d0a4560dc0..c758badebb 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1892,5 +1892,75 @@ "Upload %(count)s other files|other": "Feltölt %(count)s másik fájlt", "Upload %(count)s other files|one": "Feltölt %(count)s másik fájlt", "Cancel All": "Mindent megszakít", - "Upload Error": "Feltöltési hiba" + "Upload Error": "Feltöltési hiba", + "The server does not support the room version specified.": "A szerver nem támogatja a megadott szoba verziót.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Kérlek erősítsd meg, hogy a szobát frissíted a verzióról verzióra.", + "Changes your avatar in this current room only": "A profilképedet csak ebben a szobában változtatja meg", + "Sends the given message coloured as a rainbow": "A megadott üzenetet szivárvány színben küldi el", + "Sends the given emote coloured as a rainbow": "A megadott hangulatjelet szivárvány színben küldi el", + "The user's homeserver does not support the version of the room.": "A felhasználó matrix szervere nem támogatja a megadott szoba verziót.", + "Edit messages after they have been sent (refresh to apply changes)": "Üzenet szerkesztése küldés után (újratöltés szükséges)", + "React to messages with emoji (refresh to apply changes)": "Reagálj az üzenetre emoji-val (újratöltés szükséges)", + "When rooms are upgraded": "Ha a szobák frissültek", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Ez az eszköz nem menti el a kulcsaidat, de létezik mentés amit visszaállíthatsz és folytathatod.", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Csatlakozz ezzel az eszközzel a kulcs mentéshez kilépés előtt, hogy ne veszíts el kulcsot ami esetleg csak ezen az eszközön van meg.", + "Connect this device to Key Backup": "Csatlakozz ezzel az eszközzel a Kulcs Mentéshez", + "Backup has an invalid signature from this device": "A mentés érvénytelen aláírással rendelkezik erről az eszközről", + "this room": "ez a szoba", + "View older messages in %(roomName)s.": "Régebbi üzenetek megjelenítése itt: %(roomName)s.", + "Joining room …": "Szobához csatlakozás …", + "Loading …": "Betöltés …", + "Rejecting invite …": "Meghívó elutasítása …", + "Join the conversation with an account": "Beszélgetéshez csatlakozás felhasználói fiókkal", + "Sign Up": "Fiók készítés", + "Sign In": "Bejelentkezés", + "You were kicked from %(roomName)s by %(memberName)s": "Téged kirúgott %(memberName)s ebből a szobából: %(roomName)s", + "Reason: %(reason)s": "Ok: %(reason)s", + "Forget this room": "Szoba elfelejtése", + "Re-join": "Újra-csatlakozás", + "You were banned from %(roomName)s by %(memberName)s": "Téged kitiltott %(memberName)s ebből a szobából: %(roomName)s", + "Something went wrong with your invite to %(roomName)s": "A meghívóddal ebbe a szobába: %(roomName)s valami baj történt", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "A meghívód ellenőrzése során az alábbi hibakódot kaptuk: %(errcode)s. Megpróbálhatod ezt az információt átadni a szoba adminisztrátorának.", + "You can only join it with a working invite.": "Csak érvényes meghívóval tudsz csatlakozni.", + "You can still join it because this is a public room.": "Mivel a szoba nyilvános megpróbálhatsz csatlakozni.", + "Join the discussion": "Beszélgetéshez csatlakozás", + "Try to join anyway": "Mindennek ellenére próbálj csatlakozni", + "This invite to %(roomName)s wasn't sent to your account": "Ezt a meghívót ide: %(roomName)s nem a te fiókodnak küldték", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Jelentkezz be más fiókkal, kérj másik meghívót vagy add hozzá a fiókodhoz ezt az e-mail címet: %(email)s.", + "Do you want to chat with %(user)s?": "%(user)s felhasználóval szeretnél beszélgetni?", + "Do you want to join %(roomName)s?": "%(roomName)s szobába szeretnél belépni?", + " invited you": " meghívott", + "You're previewing %(roomName)s. Want to join it?": "%(roomName)s szoba előnézetét látod. Belépsz?", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s szobának nincs előnézete. Be szeretnél lépni?", + "This room doesn't exist. Are you sure you're at the right place?": "Ez a szoba nem létezik. Biztos, hogy jó helyen vagy?", + "Try again later, or ask a room admin to check if you have access.": "Próbálkozz később vagy kérd meg a szoba adminisztrátorát, hogy nézze meg van-e hozzáférésed.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "Amikor a szobát próbáltuk elérni ezt a hibaüzenetet kaptuk: %(errcode)s. Ha úgy gondolod, hogy ez egy hiba légy szívesnyiss egy hibajegyet.", + "This room has already been upgraded.": "Ez a szoba már frissült.", + "Agree or Disagree": "Egyetért vagy Ellentmond", + "Like or Dislike": "Kedveli vagy Nem kedveli", + "Rotate Left": "Balra forgat", + "Rotate Right": "Jobbra forgat", + "View Servers in Room": "Szerverek megjelenítése a szobában", + "Use an email address to recover your account": "A felhasználói fiók visszaszerzése e-mail címmel", + "Enter email address (required on this homeserver)": "E-mail cím megadása (ezen a matrix szerveren kötelező)", + "Doesn't look like a valid email address": "Az e-mail cím nem tűnik érvényesnek", + "Enter password": "Jelszó megadása", + "Password is allowed, but unsafe": "A jelszó engedélyezett, de nem biztonságos", + "Nice, strong password!": "Szép, erős jelszó!", + "Passwords don't match": "A jelszavak nem egyeznek meg", + "Other users can invite you to rooms using your contact details": "Mások meghívhatnak a szobákba a kapcsolatoknál megadott adataiddal", + "Enter phone number (required on this homeserver)": "Telefonszám megadása (ennél a matrix szervernél kötelező)", + "Doesn't look like a valid phone number": "Ez a telefonszám nem tűnik érvényesnek", + "Use letters, numbers, dashes and underscores only": "Csak betűket, számokat, kötőjelet és aláhúzást használj", + "Enter username": "Felhasználói név megadása", + "Some characters not allowed": "Néhány karakter nem engedélyezett", + "Use an email address to recover your account.": "A felhasználói fiókod visszaszerzéséhez használd az e-mail címet.", + "Other users can invite you to rooms using your contact details.": "Mások meghívhatnak a szobákba a kapcsolatoknál megadott adataid alapján.", + "Error loading Riot": "A Riot betöltésénél hiba", + "If this is unexpected, please contact your system administrator or technical support representative.": "Ha ez váratlanul ért, kérlek vedd fel a kapcsolatot a rendszer adminisztrátorával vagy a technikai segítséggel.", + "Failed to get autodiscovery configuration from server": "A szerverről nem sikerült beszerezni az automatikus felderítés beállításait", + "Invalid base_url for m.homeserver": "Hibás base_url az m.homeserver -hez", + "Homeserver URL does not appear to be a valid Matrix homeserver": "A matrix URL nem tűnik érvényesnek", + "Invalid base_url for m.identity_server": "Érvénytelen base_url az m.identity_server -hez", + "Identity server URL does not appear to be a valid identity server": "Az Azonosító szerver URL nem tűnik érvényesnek" } diff --git a/src/i18n/strings/sl.json b/src/i18n/strings/sl.json index 0967ef424b..39297c8ae9 100644 --- a/src/i18n/strings/sl.json +++ b/src/i18n/strings/sl.json @@ -1 +1,13 @@ -{} +{ + "This email address is already in use": "Ta e-poštni naslov je že v uporabi", + "This phone number is already in use": "Ta telefonska številka je že v uporabi", + "Failed to verify email address: make sure you clicked the link in the email": "E-poštnega naslova ni bilo mogoče preveriti: preverite, ali ste kliknili povezavo v e-poštnem sporočilu", + "The platform you're on": "Vaša platforma", + "The version of Riot.im": "Različica Riot.im", + "Dismiss": "Opusti", + "Chat with Riot Bot": "Klepetajte z Riot Botom", + "Sign In": "Prijava", + "powered by Matrix": "poganja Matrix", + "Custom Server Options": "Možnosti strežnika po meri", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Nastavite lahko tudi strežnik za identiteto po meri, vendar ne boste mogli povabiti uporabnikov prek e-pošte, prav tako pa vas ne bodo mogli povabiti drugi." +} diff --git a/src/languageHandler.js b/src/languageHandler.js index bd3a8df721..267d62a7bb 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -137,20 +137,25 @@ export function _t(text, variables, tags) { * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function substitute(text, variables, tags) { - const regexpMapping = {}; + let result = text; if (variables !== undefined) { + const regexpMapping = {}; for (const variable in variables) { regexpMapping[`%\\(${variable}\\)s`] = variables[variable]; } + result = replaceByRegexes(result, regexpMapping); } if (tags !== undefined) { + const regexpMapping = {}; for (const tag in tags) { regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag]; } + result = replaceByRegexes(result, regexpMapping); } - return replaceByRegexes(text, regexpMapping); + + return result; } /* 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/EventUtils.js b/src/utils/EventUtils.js index ac415ca6de..68f6d4b14e 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -47,5 +47,6 @@ export function isContentActionable(mxEvent) { export function canEditContent(mxEvent) { return isContentActionable(mxEvent) && + mxEvent.getOriginalContent().msgtype === "m.text" && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } 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 }); + } +}