Merge branch 'develop' into travis/feature/wellknown2

This commit is contained in:
Travis Ralston 2019-05-20 19:50:02 -06:00
commit 3476be3327
41 changed files with 750 additions and 132 deletions

View file

@ -557,4 +557,3 @@ textarea {
.mx_Username_color8 {
color: $username-variant8-color;
}

View file

@ -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";

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}

View file

@ -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) => {

View file

@ -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 ?
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span className={className} dir="auto">{ strippedBody }</span>;
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
}
export function emojifyText(text, addAlt) {

View file

@ -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);
},

View file

@ -518,7 +518,7 @@ export const CommandMap = {
unban: new Command({
name: 'unban',
args: '<user-id>',
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+)$/);

View file

@ -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,

View file

@ -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 [<MessageEditor key={mxEv.getId()} event={mxEv} />];
}
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 = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}

View file

@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
return <div>
<div className="mx_DevTools_content">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
<div className="mx_DevTools_eventTypeStateKeyGroup">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
</div>
<br />

View file

@ -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;

View file

@ -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 (

View file

@ -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}
></div>
<div className="mx_MessageEditor_buttons">
<AccessibleButton kind="secondary" onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._onSaveClicked}>{_t("Save")}</AccessibleButton>
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
</div>
</div>;
}

View file

@ -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} />;
},
});

View file

@ -168,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent {
return <span className="mx_ReactionDimension"
title={this.props.title}
aria-hidden={true}
>
{items}
</span>;

View file

@ -116,8 +116,8 @@ export default class ReactionsRow extends React.PureComponent {
return <ReactionsRowButton
key={content}
content={content}
count={count}
mxEvent={mxEvent}
reactionEvents={events}
myReactionEvent={myReactionEvent}
/>;
});

View file

@ -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 = <ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
/>;
}
return <span className={classes}
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
{content} {count}
{tooltip}
</span>;
}
}

View file

@ -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 = <div>{_t(
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
{
shortName,
},
{
reactors: () => {
return <div className="mx_ReactionsRowButtonTooltip_senders">
{formatCommaSeparatedList(senders, 6)}
</div>;
},
reactedWith: (sub) => {
return <div className="mx_ReactionsRowButtonTooltip_reactedWith">
{sub}
</div>;
},
},
)}</div>;
}
let tooltip;
if (tooltipLabel) {
tooltip = <Tooltip
tooltipClassName="mx_Tooltip_timeline"
visible={visible}
label={tooltipLabel}
/>;
}
return tooltip;
}
}

View file

@ -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 = <Tooltip
tooltipClassName="mx_Tooltip_timeline"
label={_t("Edited at %(date)s", {date})}
/>;
}
return (
<div
key="editedMarker" className="mx_EventTile_edited"
onMouseEnter={this._onMouseEnterEditedMarker}
onMouseLeave={this._onMouseLeaveEditedMarker}
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
);
},
render: function() {
if (this.props.isEditing) {
const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} />;
}
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 = <a href={this.props.highlightLink}>{ body }</a>;

View file

@ -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 = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
} else {
content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
}
const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
mx_ViewSourceEvent_expanded: expanded,
});
return <span className={classes}>
{content}
<a
className="mx_ViewSourceEvent_toggle"
href="#"
onClick={this.onToggle}
/>
</span>;
}
}

View file

@ -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 = <MessageActionBar
const actionBar = !this.props.isEditing ? <MessageActionBar
mxEvent={this.props.mxEvent}
reactions={this.state.reactions}
permalinkCreator={this.props.permalinkCreator}
getTile={this.getTile}
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
/>;
/> : undefined;
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -780,6 +784,7 @@ module.exports = withMatrixClient(React.createClass({
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}

View file

@ -52,6 +52,7 @@ export default class LabsUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
</div>
</div>
);

View file

@ -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;
}
}
}

View file

@ -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 {

View file

@ -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 <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> 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",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
"%(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 <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
@ -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",

View file

@ -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"
}

View file

@ -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 <u>set a password</u>": "Voidaksesi tulevaisuudessa palata tilillesi sinun pitää <u>asettaa salasana</u>",
"To return to your account in future you need to <u>set a password</u>": "Jotta voit jatkossa palata tilillesi, sinun pitää <u>asettaa salasana</u>",
"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 <oldVersion /> to <newVersion />.": "Vahvista, että haluat päivittää huoneen versiosta <oldVersion /> versioon <newVersion />.",
"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 <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Tämä laite <b>ei varmuuskopioi avaimiasi</b>, mutta sinulla on olemassa varmuuskopio palauttamista ja lisäämistä varten.",
"Backup has an <validity>invalid</validity> signature from this device": "Varmuuskopiossa on <validity>epäkelpo</validity> 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?",
"<userName/> invited you": "<userName/> 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 <b>too large</b> to upload. The file size limit is %(limit)s.": "Tiedostot ovat <b>liian isoja</b> ladattaviksi. Tiedoston kokoraja on %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Osa tiedostoista on <b>liian isoja</b> 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"
}

View file

@ -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 <oldVersion /> to <newVersion />.": "Kérlek erősítsd meg, hogy a szobát frissíted a <oldVersion /> verzióról <newVersion /> 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 <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Ez az eszköz <b>nem menti el a kulcsaidat</b>, 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 <validity>invalid</validity> signature from this device": "A mentés <validity>érvénytelen</validity> 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?",
"<userName/> invited you": "<userName/> 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 <issueLink>submit a bug report</issueLink>.": "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íves<issueLink>nyiss egy hibajegyet</issueLink>.",
"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"
}

View file

@ -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."
}

View file

@ -137,20 +137,25 @@ export function _t(text, variables, tags) {
* @return a React <span> 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;
}
/*

View file

@ -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,
},
};

View file

@ -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();
}

View file

@ -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 });
}
}