Merge remote-tracking branch 'upstream/develop' into hs/custom-notif-sounds

This commit is contained in:
Will Hunt 2019-05-31 10:44:30 +01:00
commit 9369e964fa
143 changed files with 3498 additions and 1773 deletions

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import {ContentRepo} from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
@ -58,4 +59,71 @@ module.exports = {
}
return require('../res/img/' + images[total % images.length] + '.png');
},
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
getInitialLetter(name) {
if (name.length < 1) {
return undefined;
}
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
let chars = 1;
const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
const second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
avatarUrlForRoom(room, width, height, resizeMethod) {
const explicitRoomAvatar = room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
resizeMethod,
false,
);
if (explicitRoomAvatar) {
return explicitRoomAvatar;
}
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
resizeMethod,
false,
);
}
return null;
},
};

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

@ -1,86 +0,0 @@
//@flow
/*
Copyright 2017 Aviral Dasgupta
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 { Value } from 'slate';
import _clamp from 'lodash/clamp';
type MessageFormat = 'rich' | 'markdown';
class HistoryItem {
// We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format.
value: Value;
format: MessageFormat = 'rich';
constructor(value: ?Value, format: ?MessageFormat) {
this.value = value;
this.format = format;
}
static fromJSON(obj: Object): HistoryItem {
return new HistoryItem(
Value.fromJSON(obj.value),
obj.format,
);
}
toJSON(): Object {
return {
value: this.value.toJSON(),
format: this.format,
};
}
}
export default class ComposerHistoryManager {
history: Array<HistoryItem> = [];
prefix: string;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
this.prefix = prefix + roomId;
// TODO: Performance issues?
let item;
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
try {
this.history.push(
HistoryItem.fromJSON(JSON.parse(item)),
);
} catch (e) {
console.warn("Throwing away unserialisable history", e);
}
}
this.lastIndex = this.currentIndex;
// reset currentIndex to account for any unserialisable history
this.currentIndex = this.history.length;
}
save(value: Value, format: MessageFormat) {
const item = new HistoryItem(value, format);
this.history.push(item);
this.currentIndex = this.history.length;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
}
getItem(offset: number): ?HistoryItem {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
}

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

@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string';
import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';
import url from 'url';
linkifyMatrix(linkify);
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOJIBASE_REGEX from 'emojibase-regex';
emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
emojione.imageType = 'svg';
linkifyMatrix(linkify);
// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
// And there a bunch more symbol characters that emojione has within the
// And there a bunch more symbol characters that emojibase has within the
// BMP, so this includes the ranges from 'letterlike symbols' to
// 'miscellaneous symbols and arrows' which should catch all of them
// (with plenty of false positives, but that's OK)
@ -54,15 +50,15 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
// Regex pattern for whitespace characters
const WHITESPACE_REGEX = new RegExp("\\s", "g");
// And this is emojione's complete regex
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
/*
* Return true if the given string contains emoji
* Uses a much, much simpler regex than emojione's so will give false
* Uses a much, much simpler regex than emojibase's so will give false
* positives, but useful for fast-path testing strings to see if they
* need emojification.
* unicodeToImage uses this function.
@ -71,62 +67,27 @@ export function containsEmoji(str) {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
}
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
* because we want to include emoji shortnames in title text
/**
* Returns the shortcode for an emoji character.
*
* @param {String} char The emoji character
* @return {String} The shortcode (such as :thumbup:)
*/
function unicodeToImage(str, addAlt) {
if (addAlt === undefined) addAlt = true;
let replaceWith; let unicode; let short; let fname;
const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) {
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar;
} else {
// get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar];
short = mappedUnicode[unicode];
fname = emojione.emojioneList[short].fname;
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
const title = mappedUnicode[unicode];
if (addAlt) {
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
} else {
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
}
return replaceWith;
}
});
return str;
export function unicodeToShortcode(char) {
const data = EMOJIBASE.find(e => e.unicode === char);
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
}
/**
* Given one or more unicode characters (represented by unicode
* character number), return an image node with the corresponding
* emoji.
* Returns the unicode character for an emoji shortcode
*
* @param alt {string} String to use for the image alt text
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
* @param unicode {integer} One or more integers representing unicode characters
* @returns A img node with the corresponding emoji
* @param {String} shortcode The shortcode (such as :thumbup:)
* @return {String} The emoji character; null if none exists
*/
export function charactersToImageNode(alt, useSvg, ...unicode) {
const fileName = unicode.map((u) => {
return u.toString(16);
}).join('-');
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
const fileType = useSvg ? 'svg' : 'png';
return <img
alt={alt}
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
/>;
export function shortcodeToUnicode(shortcode) {
shortcode = shortcode.slice(1, shortcode.length - 1);
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
return data ? data.unicode : null;
}
export function processHtmlForSending(html: string): string {
@ -433,13 +394,10 @@ class TextHighlighter extends BaseHighlighter {
* opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
* opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
*/
export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false;
let sanitizeParams = sanitizeHtmlParams;
@ -470,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) {
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
if (doEmojiOne) {
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
}
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
// Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
} else {
// ... or if there are emoji, which we insert as HTML alongside the
// escaped plaintext body.
if (bodyHasEmoji) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
}
}
// An HTML message with emoji
// or a plaintext message with emoji that was escaped and sanitized into
// HTML.
if (bodyHasEmoji) {
safeBody = unicodeToImage(safeBody);
}
} finally {
delete sanitizeParams.textFilter;
@ -503,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) {
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0;
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
// Ignore spaces in body text. Emojis with spaces in between should
@ -515,29 +456,25 @@ export function bodyToHtml(content, highlights, opts={}) {
// presented as large.
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username
&& (content.formatted_body == undefined
|| !content.formatted_body.includes("https://matrix.to/"));
(
content.formatted_body == undefined ||
!content.formatted_body.includes("https://matrix.to/")
);
}
const className = classNames({
'mx_EventTile_body': true,
'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtmlMessage,
'markdown-body': isHtmlMessage && !emojiBody,
});
return isDisplayedWithHtml ?
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span className={className} dir="auto">{ strippedBody }</span>;
}
export function emojifyText(text, addAlt) {
return {
__html: unicodeToImage(escape(text), addAlt),
};
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
}
/**

View file

@ -33,6 +33,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -499,7 +500,9 @@ async function startMatrixClient() {
Notifier.start();
UserActivity.sharedInstance().start();
Presence.start();
if (!SettingsStore.getValue("lowBandwidth")) {
Presence.start();
}
DMRoomMap.makeShared().start();
ActiveWidgetStore.start();

View file

@ -119,7 +119,7 @@ class MatrixClientPeg {
// try to initialise e2e on the new client
try {
// check that we have a version of the js-sdk which includes initCrypto
if (this.matrixClient.initCrypto) {
if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
StorageManager.setCryptoInitialised(true);
}
@ -188,8 +188,7 @@ class MatrixClientPeg {
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
verificationMethods: [verificationMethods.SAS],
unstableClientRelationAggregation: aggregateRelations,
unstableClientRelationReplacements: enableEdits,
unstableClientRelationAggregation: aggregateRelations || enableEdits,
};
this.matrixClient = createMatrixClient(opts);

View file

@ -85,7 +85,11 @@ const Notifier = {
msg = '';
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
let avatarUrl = null;
if (ev.sender && !SettingsStore.getValue("lowBandwidth")) {
avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop');
}
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
// if displayNotification returns non-null, the platform supports

View file

@ -1,40 +0,0 @@
/*
Copyright 2015 - 2017 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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 * as emojione from 'emojione';
export function unicodeToEmojiUri(str) {
const mappedUnicode = emojione.mapUnicodeToShort();
// remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them
return str.replace(emojione.regUnicode, function(unicodeChar) {
if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) {
// if the unicodeChar doesn't exist just return the entire match
return unicodeChar;
} else {
// get the unicode codepoint from the actual char
const unicode = emojione.jsEscapeMap[unicodeChar];
const short = mappedUnicode[unicode];
const fname = emojione.emojioneList[short].fname;
return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam;
}
});
}

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

@ -19,47 +19,31 @@ limitations under the License.
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import QueryMatcher from './QueryMatcher';
import sdk from '../index';
import {PillCompletion} from './Components';
import type {Completion, SelectionRange} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import EmojiData from '../stripped-emoji.json';
const LIMIT = 20;
const CATEGORY_ORDER = [
'people',
'food',
'objects',
'activity',
'nature',
'travel',
'flags',
'regional',
'symbols',
'modifier',
];
// Match for ":wink:" or ascii-style ";-)" provided by emojione
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
// that we need to support inputting multiple emoji with no space between them.
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g');
// We also need to match the non-zero-length prefixes to remove them from the final match,
// and update the range so that we don't replace the whitespace or the previous emoji.
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
// XXX: it's very unclear why we bother with this generated emojidata file.
// all it means is that we end up bloating the bundle with precomputed stuff
// which would be trivial to calculate and cache on demand.
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
(a, b) => {
if (a.category === b.category) {
return a.emoji_order - b.emoji_order;
}
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
return a.category - b.category;
},
).map((a, index) => {
return {
@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider {
return []; // don't give any suggestions if the user doesn't want them
}
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
let matchedString = command[0];
// Remove prefix of any length (single whitespace or unicode emoji)
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
if (prefixMatch) {
matchedString = matchedString.slice(prefixMatch[0].length);
range.start += prefixMatch[0].length;
}
const matchedString = command[0];
completions = this.matcher.match(matchedString);
// Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString));
const sorters = [];
// First, sort by score (Infinity if matchedString not in shortname)
// make sure that emoticons come first
sorters.push((c) => score(matchedString, c.aliases_ascii));
// then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
// If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark"
@ -133,12 +111,12 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = _sortBy(_uniq(completions), sorters);
completions = completions.map((result) => {
const {shortname} = result;
const unicode = shortnameToUnicode(shortname);
const { shortname } = result;
const unicode = shortcodeToUnicode(shortname);
return {
completion: unicode,
component: (
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
),
range,
};

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

@ -129,7 +129,7 @@ export default class IndicatorScrollbar extends React.Component {
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
const yRetention = 1.0;
if (Math.abs(e.deltaX) < xyThreshold) {
if (Math.abs(e.deltaX) <= xyThreshold) {
// noinspection JSSuspiciousNameCombination
this._scrollElement.scrollLeft += e.deltaY * yRetention;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations 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.
@ -60,7 +61,7 @@ export default React.createClass({
inputs: PropTypes.object,
// As js-sdk interactive-auth
makeRegistrationUrl: PropTypes.func,
requestEmailToken: PropTypes.func,
sessionId: PropTypes.string,
clientSecret: PropTypes.string,
emailSid: PropTypes.string,
@ -96,6 +97,7 @@ export default React.createClass({
sessionId: this.props.sessionId,
clientSecret: this.props.clientSecret,
emailSid: this.props.emailSid,
requestEmailToken: this.props.requestEmailToken,
});
this._authLogic.attemptAuth().then((result) => {
@ -202,7 +204,6 @@ export default React.createClass({
stageState={this.state.stageState}
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
makeRegistrationUrl={this.props.makeRegistrationUrl}
showContinue={!this.props.continueIsManaged}
/>
);

View file

@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
@ -118,6 +119,8 @@ const LoggedInView = React.createClass({
this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
fixupColorFonts();
},
componentDidUpdate(prevProps) {
@ -322,6 +325,18 @@ const LoggedInView = React.createClass({
handled = true;
}
break;
case KeyCode.KEY_I:
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"
// will have to do.
if (ctrlCmdOnly) {
dis.dispatch({
action: 'toggle_top_left_menu',
});
handled = true;
}
break;
}
if (handled) {

View file

@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
const AutoDiscovery = Matrix.AutoDiscovery;
import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
@ -109,6 +108,7 @@ export default React.createClass({
propTypes: {
config: PropTypes.object,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig),
ConferenceHandler: PropTypes.any,
onNewScreen: PropTypes.func,
registrationUrl: PropTypes.string,
@ -181,16 +181,8 @@ export default React.createClass({
// Parameters used in the registration dance with the IS
register_client_secret: null,
register_session_id: null,
register_hs_url: null,
register_is_url: null,
register_id_sid: null,
// Parameters used for setting up the authentication views
defaultServerName: this.props.config.default_server_name,
defaultHsUrl: this.props.config.default_hs_url,
defaultIsUrl: this.props.config.default_is_url,
defaultServerDiscoveryError: null,
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: false,
@ -211,42 +203,19 @@ export default React.createClass({
};
},
getDefaultServerName: function() {
return this.state.defaultServerName;
},
getCurrentHsUrl: function() {
if (this.state.register_hs_url) {
return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl();
} else {
return this.getDefaultHsUrl();
}
},
getDefaultHsUrl(defaultToMatrixDotOrg) {
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
return this.state.defaultHsUrl;
},
getFallbackHsUrl: function() {
return this.props.config.fallback_hs_url;
},
getCurrentIsUrl: function() {
if (this.state.register_is_url) {
return this.state.register_is_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getIdentityServerUrl();
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
return this.props.config.fallback_hs_url;
} else {
return this.getDefaultIsUrl();
return null;
}
},
getDefaultIsUrl() {
return this.state.defaultIsUrl || "https://vector.im";
getServerProperties() {
let props = this.state.serverConfig;
if (!props) props = this.props.serverConfig; // for unit tests
if (!props) props = SdkConfig.get()["validated_server_config"];
return {serverConfig: props};
},
componentWillMount: function() {
@ -260,40 +229,6 @@ export default React.createClass({
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
}
// Set up the default URLs (async)
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
this.setState({loadingDefaultHomeserver: true});
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
// Ideally we would somehow only communicate this to the server admins, but
// given this is at login time we can't really do much besides hope that people
// will check their settings.
this.setState({
defaultServerName: null, // To un-hide any secrets people might be keeping
defaultServerDiscoveryError: _t(
"Invalid configuration: Cannot supply a default homeserver URL and " +
"a default server name",
),
});
}
// Set a default HS with query param `hs_url`
const paramHs = this.props.startingFragmentQueryParams.hs_url;
if (paramHs) {
console.log('Setting register_hs_url ', paramHs);
this.setState({
register_hs_url: paramHs,
});
}
// Set a default IS with query param `is_url`
const paramIs = this.props.startingFragmentQueryParams.is_url;
if (paramIs) {
console.log('Setting register_is_url ', paramIs);
this.setState({
register_is_url: paramIs,
});
}
// a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a
// rerender.
@ -374,8 +309,8 @@ export default React.createClass({
return Lifecycle.loadSession({
fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(),
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).then((loadedSession) => {
@ -1827,44 +1762,7 @@ export default React.createClass({
},
onServerConfigChange(config) {
const newState = {};
if (config.hsUrl) {
newState.register_hs_url = config.hsUrl;
}
if (config.isUrl) {
newState.register_is_url = config.isUrl;
}
this.setState(newState);
},
_tryDiscoverDefaultHomeserver: async function(serverName) {
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS) {
console.error("Failed to discover homeserver on startup:", discovery);
this.setState({
defaultServerDiscoveryError: discovery["m.homeserver"].error,
loadingDefaultHomeserver: false,
});
} else {
const hsUrl = discovery["m.homeserver"].base_url;
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "https://vector.im";
this.setState({
defaultHsUrl: hsUrl,
defaultIsUrl: isUrl,
loadingDefaultHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
loadingDefaultHomeserver: false,
});
}
this.setState({serverConfig: config});
},
_makeRegistrationUrl: function(params) {
@ -1883,8 +1781,7 @@ export default React.createClass({
if (
this.state.view === VIEWS.LOADING ||
this.state.view === VIEWS.LOGGING_IN ||
this.state.loadingDefaultHomeserver
this.state.view === VIEWS.LOGGING_IN
) {
const Spinner = sdk.getComponent('elements.Spinner');
return (
@ -1962,18 +1859,13 @@ export default React.createClass({
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
makeRegistrationUrl={this._makeRegistrationUrl}
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
/>
{...this.getServerProperties()}
/>
);
}
@ -1982,14 +1874,11 @@ export default React.createClass({
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
return (
<ForgotPassword
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick} />
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
);
}
@ -1999,16 +1888,11 @@ export default React.createClass({
<Login
onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
);
}

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'];
@ -95,6 +96,9 @@ module.exports = React.createClass({
// helper function to access relations for an event
getRelationsForEvent: PropTypes.func,
// whether to show reactions for an event
showReactions: PropTypes.bool,
},
componentWillMount: function() {
@ -230,6 +234,13 @@ module.exports = React.createClass({
}
},
scrollToEventIfNeeded: function(eventId) {
const node = this.eventNodes[eventId];
if (node) {
node.scrollIntoView({block: "nearest", behavior: "instant"});
}
},
/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState: function() {
@ -248,6 +259,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 +465,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,18 +538,20 @@ module.exports = React.createClass({
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
isEditing={isEditing}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
eventSendStatus={mxEv.replacementOrOwnStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
/>
</li>,
);
@ -714,7 +727,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

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,12 +16,9 @@ limitations under the License.
*/
const React = require('react');
const ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
const Matrix = require("matrix-js-sdk");
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
const dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel
@ -44,7 +42,7 @@ const NotificationPanel = React.createClass({
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview = {false}
showUrlPreview={false}
tileShape="notif"
empty={_t('You have no visible notifications')}
/>

View file

@ -304,8 +304,6 @@ module.exports = React.createClass({
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this._shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">

View file

@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
// turn this on for drop & drag console debugging galore
const debug = false;
@ -42,6 +43,7 @@ const RoomSubList = React.createClass({
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
addRoomLabel: PropTypes.string,
order: PropTypes.string.isRequired,
@ -232,7 +234,11 @@ const RoomSubList = React.createClass({
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
<AccessibleButton
onClick={ this.props.onAddRoom }
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}

View file

@ -1832,6 +1832,7 @@ module.exports = React.createClass({
membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
/>);
let topUnreadMessagesBar = null;

View file

@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({
// placeholder text to use if the timeline is empty
empty: PropTypes.string,
// whether to show reactions for an event
showReactions: PropTypes.bool,
},
statics: {
@ -204,11 +207,11 @@ const TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
MatrixClientPeg.get().on("Room.replaceEvent", this.onRoomReplaceEvent);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
@ -283,11 +286,11 @@ const TimelinePanel = React.createClass({
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
client.removeListener("Room.redaction", this.onRoomRedaction);
client.removeListener("Room.replaceEvent", this.onRoomReplaceEvent);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Event.replaced", this.onEventReplaced);
client.removeListener("sync", this.onSync);
}
},
@ -405,7 +408,13 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
}
if (payload.action === "edit_event") {
this.setState({editEvent: payload.event});
this.setState({editEvent: payload.event}, () => {
if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(),
);
}
});
}
},
@ -507,7 +516,7 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
},
onRoomReplaceEvent: function(replacedEvent, room) {
onEventReplaced: function(replacedEvent, room) {
if (this.unmounted) return;
// ignore events for other rooms
@ -553,6 +562,9 @@ const TimelinePanel = React.createClass({
},
onEventDecrypted: function(ev) {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
// Need to update as we don't display event tiles for events that
// haven't yet been decrypted. The event will have just been updated
// in place so we just need to re-render.
@ -601,6 +613,8 @@ const TimelinePanel = React.createClass({
},
sendReadReceipt: function() {
if (SettingsStore.getValue("lowBandwidth")) return;
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
@ -1261,6 +1275,7 @@ const TimelinePanel = React.createClass({
resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent}
editEvent={this.state.editEvent}
showReactions={this.props.showReactions}
/>
);
},

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector 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.
@ -23,6 +24,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import MatrixClientPeg from '../../MatrixClientPeg';
import Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher";
import {focusCapturedRef} from "../../utils/Accessibility";
const AVATAR_SIZE = 28;
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
super();
this.state = {
menuDisplayed: false,
menuFunctions: null, // should be { close: fn }
profileInfo: null,
};
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
}
async componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
try {
const profileInfo = await this._getProfileInfo();
this.setState({profileInfo});
@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component {
}
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = (payload) => {
// For accessibility
if (payload.action === "toggle_top_left_menu") {
if (this._buttonRef) this._buttonRef.click();
}
};
_getDisplayName() {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component {
}
return (
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
<AccessibleButton
className="mx_TopLeftMenuButton"
role="button"
onClick={this.onToggleMenu}
inputRef={(r) => this._buttonRef = r}
aria-label={_t("Your profile")}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={name}
@ -98,7 +121,7 @@ export default class TopLeftMenuButton extends React.Component {
resizeMethod="crop"
/>
{ nameElement }
<span className="mx_TopLeftMenuButton_chevron"></span>
<span className="mx_TopLeftMenuButton_chevron" />
</AccessibleButton>
);
}
@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component {
e.preventDefault();
e.stopPropagation();
if (this.state.menuDisplayed && this.state.menuFunctions) {
this.state.menuFunctions.close();
return;
}
const elementRect = e.currentTarget.getBoundingClientRect();
const x = elementRect.left;
const y = elementRect.top + elementRect.height;
ContextualMenu.createMenu(TopLeftMenu, {
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
chevronFace: "none",
left: x,
top: y,
userId: MatrixClientPeg.get().getUserId(),
displayName: this._getDisplayName(),
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
onFinished: () => {
this.setState({ menuDisplayed: false });
this.setState({ menuDisplayed: false, menuFunctions: null });
},
});
this.setState({ menuDisplayed: true });
this.setState({ menuDisplayed: true, menuFunctions });
}
}

View file

@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases
// Show controls to configure server details
@ -40,28 +40,14 @@ module.exports = React.createClass({
displayName: 'ForgotPassword',
propTypes: {
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onServerConfigChange: PropTypes.func.isRequired,
onLoginClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
phase: PHASE_FORGOT,
email: "",
password: "",
@ -70,11 +56,11 @@ module.exports = React.createClass({
};
},
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
submitPasswordReset: function(email, password) {
this.setState({
phase: PHASE_SENDING_EMAIL,
});
this.reset = new PasswordReset(hsUrl, identityUrl);
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).done(() => {
this.setState({
phase: PHASE_EMAIL_SENT,
@ -103,13 +89,6 @@ module.exports = React.createClass({
onSubmitForm: function(ev) {
ev.preventDefault();
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) {
@ -132,10 +111,7 @@ module.exports = React.createClass({
button: _t('Continue'),
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(
this.state.enteredHsUrl, this.state.enteredIsUrl,
this.state.email, this.state.password,
);
this.submitPasswordReset(this.state.email, this.state.password);
}
},
});
@ -148,19 +124,7 @@ module.exports = React.createClass({
});
},
onServerConfigChange: function(config) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.setState(newState);
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_FORGOT,
});
@ -190,26 +154,19 @@ module.exports = React.createClass({
renderServerDetails() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
}
return <div>
<ServerConfig
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.state.enteredHsUrl}
customIsUrl={this.state.enteredIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>
</div>;
return <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={0}
onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
},
renderForgot() {
@ -221,25 +178,22 @@ module.exports = React.createClass({
errorText = <div className="mx_Login_error">{ err }</div>;
}
let yourMatrixAccountText = _t('Your Matrix account');
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) {
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: this.props.defaultServerName,
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} else {
try {
const parsedHsUrl = new URL(this.state.enteredHsUrl);
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
errorText = <div className="mx_Login_error">{_t(
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
"enter a valid URL including the protocol prefix.",
{
hsUrl: this.state.enteredHsUrl,
})}</div>;
}
}
// If custom URLs are allowed, wire up the server details edit link.

View file

@ -20,12 +20,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../languageHandler';
import {_t, _td} from '../../../languageHandler';
import sdk from '../../../index';
import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import { AutoDiscovery } from "matrix-js-sdk";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -59,19 +59,14 @@ module.exports = React.createClass({
propTypes: {
onLoggedIn: PropTypes.func.isRequired,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about where to "sign in to".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
// went wrong. May be replaced with a different error within the
// Login component.
errorText: PropTypes.string,
// If true, the component will consider itself busy.
busy: PropTypes.bool,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
// Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users.
@ -79,12 +74,13 @@ module.exports = React.createClass({
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done.
// login shouldn't know or care how registration, password recovery,
// etc is done.
onRegisterClick: PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
},
getInitialState: function() {
@ -93,9 +89,6 @@ module.exports = React.createClass({
errorText: null,
loginIncorrect: false,
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
// used for preserving form values when changing homeserver
username: "",
phoneCountry: null,
@ -105,10 +98,6 @@ module.exports = React.createClass({
phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password",
// .well-known discovery
discoveryError: "",
findingHomeserver: false,
};
},
@ -132,6 +121,14 @@ module.exports = React.createClass({
this._unmounted = true;
},
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
},
onPasswordLoginError: function(errorText) {
this.setState({
errorText,
@ -139,10 +136,17 @@ module.exports = React.createClass({
});
},
isBusy: function() {
return this.state.busy || this.props.busy;
},
hasError: function() {
return this.state.errorText || this.props.errorText;
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver
// discovery went wrong
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
// Prevent people from submitting their password when something isn't right.
if (this.isBusy() || this.hasError()) return;
this.setState({
busy: true,
@ -164,7 +168,7 @@ module.exports = React.createClass({
const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus === 400 && usingEmail) {
errorText = _t('This homeserver does not support login using email address.');
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError(
error.data.limit_type,
error.data.admin_contact, {
@ -194,11 +198,10 @@ module.exports = React.createClass({
<div>
<div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
})
}
{_t(
'Please note you are logging into the %(hs)s server, not matrix.org.',
{hs: this.props.serverConfig.hsName},
)}
</div>
</div>
);
@ -232,21 +235,26 @@ module.exports = React.createClass({
this.setState({ username: username });
},
onUsernameBlur: function(username) {
onUsernameBlur: async function(username) {
const doWellknownLookup = username[0] === "@";
this.setState({
username: username,
discoveryError: null,
busy: doWellknownLookup, // unset later by the result of onServerConfigChange
errorText: null,
});
if (username[0] === "@") {
if (doWellknownLookup) {
const serverName = username.split(':').slice(1).join(':');
try {
// we have to append 'https://' to make the URL constructor happy
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
const url = new URL("https://" + serverName);
this._tryWellKnownDiscovery(url.hostname);
const result = await AutoDiscoveryUtils.validateServerName(serverName);
this.props.onServerConfigChange(result);
} catch (e) {
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
let message = _t("Failed to perform homeserver discovery");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({errorText: message, busy: false});
}
}
},
@ -274,32 +282,13 @@ module.exports = React.createClass({
}
},
onServerConfigChange: function(config) {
const self = this;
const newState = {
errorText: null, // reset err messages
};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl);
});
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_LOGIN,
});
@ -313,64 +302,13 @@ module.exports = React.createClass({
});
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
this.setState({
discoveryError: "",
findingHomeserver: false,
});
return;
}
this.setState({findingHomeserver: true});
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
this.setState({
discoveryError: discovery["m.homeserver"].error,
findingHomeserver: false,
});
} else if (state === AutoDiscovery.PROMPT) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
} else if (state === AutoDiscovery.SUCCESS) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
this.onServerConfigChange({
hsUrl: discovery["m.homeserver"].base_url,
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "",
});
} else {
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
this.setState({
discoveryError: _t("Unknown failure discovering homeserver"),
findingHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
findingHomeserver: false,
discoveryError: _t("Unknown error discovering homeserver"),
});
}
},
_initLoginLogic: function(hsUrl, isUrl) {
const self = this;
hsUrl = hsUrl || this.state.enteredHsUrl;
isUrl = isUrl || this.state.enteredIsUrl;
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.props.serverConfig.isUrl;
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
// TODO: TravisR - Only use this if the homeserver is the default homeserver
const fallbackHsUrl = this.props.fallbackHsUrl;
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
@ -378,8 +316,6 @@ module.exports = React.createClass({
this._loginLogic = loginLogic;
this.setState({
enteredHsUrl: hsUrl,
enteredIsUrl: isUrl,
busy: true,
loginIncorrect: false,
});
@ -445,8 +381,8 @@ module.exports = React.createClass({
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
(this.state.enteredHsUrl.startsWith("http:") ||
!this.state.enteredHsUrl.startsWith("http"))
(this.props.serverConfig.hsUrl.startsWith("http:") ||
!this.props.serverConfig.hsUrl.startsWith("http"))
) {
errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
@ -469,9 +405,9 @@ module.exports = React.createClass({
"is not blocking requests.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener"
href={this.state.enteredHsUrl}
>{ sub }</a>;
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
{ sub }
</a>;
},
},
) }
@ -484,7 +420,6 @@ module.exports = React.createClass({
renderServerComponent() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
@ -494,28 +429,19 @@ module.exports = React.createClass({
return null;
}
const serverDetails = <ServerConfig
customHsUrl={this.state.enteredHsUrl}
customIsUrl={this.state.enteredIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250}
/>;
let nextButton = null;
const serverDetailsProps = {};
if (PHASES_ENABLED) {
nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>;
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
return <div>
{serverDetails}
{nextButton}
</div>;
return <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
},
renderLoginComponentForStep() {
@ -547,13 +473,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return (
<PasswordLogin
onSubmit={this.onPasswordLogin}
@ -569,10 +488,9 @@ module.exports = React.createClass({
onPhoneNumberBlur={this.onPhoneNumberBlur}
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsName={hsName}
hsUrl={this.state.enteredHsUrl}
disableSubmit={this.state.findingHomeserver}
/>
serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()}
/>
);
},
@ -595,9 +513,9 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
const errorText = this.state.errorText || this.props.errorText;
let errorTextSection;
if (errorText) {

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector 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.
@ -17,16 +18,15 @@ limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases
// Show controls to configure server details
@ -46,18 +46,7 @@ module.exports = React.createClass({
sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired,
idSid: PropTypes.string,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
brand: PropTypes.string,
email: PropTypes.string,
// registration shouldn't know or care how login is done.
@ -66,7 +55,7 @@ module.exports = React.createClass({
},
getInitialState: function() {
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
return {
busy: false,
@ -87,8 +76,6 @@ module.exports = React.createClass({
// straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId),
serverType,
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
// Phase of the overall registration dialog.
phase: PHASE_REGISTRATION,
flows: null,
@ -100,18 +87,22 @@ module.exports = React.createClass({
this._replaceClient();
},
onServerConfigChange: function(config) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
this._replaceClient(newProps.serverConfig);
// Handle cases where the user enters "https://matrix.org" for their server
// from the advanced option - we should default to FREE at that point.
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
if (serverType !== this.state.serverType) {
// Reset the phase to default phase for the server type.
this.setState({
serverType,
phase: this.getDefaultPhaseForServerType(serverType),
});
}
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, () => {
this._replaceClient();
});
},
getDefaultPhaseForServerType(type) {
@ -136,19 +127,17 @@ module.exports = React.createClass({
// the new type.
switch (type) {
case ServerType.FREE: {
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
this.onServerConfigChange({
hsUrl,
isUrl,
});
const { serverConfig } = ServerType.TYPES.FREE;
this.props.onServerConfigChange(serverConfig);
break;
}
case ServerType.PREMIUM:
// We can accept whatever server config was the default here as this essentially
// acts as a slightly different "custom server"/ADVANCED option.
break;
case ServerType.ADVANCED:
this.onServerConfigChange({
hsUrl: this.props.defaultHsUrl,
isUrl: this.props.defaultIsUrl,
});
// Use the default config from the config
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
break;
}
@ -158,13 +147,15 @@ module.exports = React.createClass({
});
},
_replaceClient: async function() {
_replaceClient: async function(serverConfig) {
this.setState({
errorText: null,
});
if (!serverConfig) serverConfig = this.props.serverConfig;
const {hsUrl, isUrl} = serverConfig;
this._matrixClient = Matrix.createClient({
baseUrl: this.state.hsUrl,
idBaseUrl: this.state.isUrl,
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
try {
await this._makeRegisterRequest({});
@ -189,12 +180,6 @@ module.exports = React.createClass({
},
onFormSubmit: function(formVals) {
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
this.setState({
errorText: "",
busy: true,
@ -203,11 +188,25 @@ module.exports = React.createClass({
});
},
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
return this._matrixClient.requestRegisterEmailToken(
emailAddress,
clientSecret,
sendAttempt,
this.props.makeRegistrationUrl({
client_secret: clientSecret,
hs_url: this._matrixClient.getHomeserverUrl(),
is_url: this._matrixClient.getIdentityServerUrl(),
session_id: sessionId,
}),
);
},
_onUIAuthFinished: async function(success, response, extra) {
if (!success) {
let msg = response.message || response.toString();
// can we give a better error message?
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError(
response.data.limit_type,
response.data.admin_contact, {
@ -302,8 +301,7 @@ module.exports = React.createClass({
});
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_REGISTRATION,
});
@ -348,7 +346,6 @@ module.exports = React.createClass({
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
@ -365,47 +362,41 @@ module.exports = React.createClass({
</div>;
}
const serverDetailsProps = {};
if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
let serverDetails = null;
switch (this.state.serverType) {
case ServerType.FREE:
break;
case ServerType.PREMIUM:
serverDetails = <ModularServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
break;
case ServerType.ADVANCED:
serverDetails = <ServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
break;
}
let nextButton = null;
if (PHASES_ENABLED) {
nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>;
}
return <div>
<ServerTypeSelector
selected={this.state.serverType}
onChange={this.onServerTypeChange}
/>
{serverDetails}
{nextButton}
</div>;
},
@ -424,7 +415,7 @@ module.exports = React.createClass({
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={this._getUIAuthInputs()}
makeRegistrationUrl={this.props.makeRegistrationUrl}
requestEmailToken={this._requestEmailToken}
sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret}
emailSid={this.props.idSid}
@ -446,13 +437,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.hsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return <RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
@ -462,8 +446,7 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows}
hsName={hsName}
hsUrl={this.state.hsUrl}
serverConfig={this.props.serverConfig}
/>;
}
},
@ -474,7 +457,7 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent('auth.AuthPage');
let errorText;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
const err = this.state.errorText;
if (err) {
errorText = <div className="mx_Login_error">{ err }</div>;
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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.
@ -57,7 +58,6 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
* makeRegistrationUrl A function that makes a registration URL
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -374,38 +373,6 @@ export const EmailIdentityAuthEntry = React.createClass({
};
},
componentWillMount: function() {
if (this.props.stageState.emailSid === null) {
this.setState({requestingToken: true});
this._requestEmailToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
}
},
/*
* Requests a verification token by email.
*/
_requestEmailToken: function() {
const nextLink = this.props.makeRegistrationUrl({
client_secret: this.props.clientSecret,
hs_url: this.props.matrixClient.getHomeserverUrl(),
is_url: this.props.matrixClient.getIdentityServerUrl(),
session_id: this.props.authSessionId,
});
return this.props.matrixClient.requestRegisterEmailToken(
this.props.inputs.emailAddress,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
nextLink,
).then((result) => {
this.props.setEmailSid(result.sid);
});
},
render: function() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");

View file

@ -18,9 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
/*
* Configure the Modular server name.
*
@ -31,65 +37,107 @@ export default class ModularServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
// This component always uses the default IS URL and doesn't allow it
// to be changed. We still receive it as a prop here to simplify
// consumers by still passing the IS URL via onServerConfigChange.
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
delayTimeMs: 0,
}
};
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl) return;
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: this.props.defaultIsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.props.defaultIsUrl,
});
this.validateServer();
});
}
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
}
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
await this.validateServer();
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
@ -100,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent {
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
@ -113,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent {
</a>,
},
)}
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.defaultHsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
</div>
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
</div>
{submitButton}
</form>
</div>
);
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,11 +22,29 @@ import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
/**
* A pure UI component which displays a username/password form.
*/
class PasswordLogin extends React.Component {
export default class PasswordLogin extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
};
static defaultProps = {
onError: function() {},
onEditServerDetailsClick: null,
@ -40,13 +59,12 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about where to "sign in to".
hsName: null,
hsUrl: "",
disableSubmit: false,
}
};
static LOGIN_FIELD_EMAIL = "login_field_email";
static LOGIN_FIELD_MXID = "login_field_mxid";
static LOGIN_FIELD_PHONE = "login_field_phone";
constructor(props) {
super(props);
@ -193,10 +211,7 @@ class PasswordLogin extends React.Component {
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
label={_t("Username")}
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
@ -258,20 +273,22 @@ class PasswordLogin extends React.Component {
</span>;
}
let signInToText = _t('Sign in to your Matrix account');
if (this.props.hsName) {
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
}
let editLink = null;
@ -353,27 +370,3 @@ class PasswordLogin extends React.Component {
);
}
}
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
hsUrl: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -51,11 +52,7 @@ module.exports = React.createClass({
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about "your account".
hsName: PropTypes.string,
hsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
},
getDefaultProps: function() {
@ -515,20 +512,22 @@ module.exports = React.createClass({
},
render: function() {
let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
}
let editLink = null;

View file

@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
/*
* A pure UI component which displays the HS and IS to use.
@ -27,82 +30,119 @@ import { _t } from '../../../languageHandler';
export default class ServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
delayTimeMs: 0,
}
};
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
isUrl: props.customIsUrl,
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl &&
newProps.customIsUrl === this.state.isUrl) return;
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
this.validateServer();
});
}
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
}
};
onIdentityServerBlur = (ev) => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
this.validateServer();
});
}
};
onIdentityServerChange = (ev) => {
const isUrl = ev.target.value;
this.setState({ isUrl });
}
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
await this.validateServer();
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
@ -114,10 +154,24 @@ export default class ServerConfig extends React.PureComponent {
showHelpPopup = () => {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
}
};
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const errorText = this.state.errorText
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
: null;
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
@ -127,22 +181,28 @@ export default class ServerConfig extends React.PureComponent {
{ sub }
</a>,
})}
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.defaultHsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
<Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")}
placeholder={this.props.defaultIsUrl}
value={this.state.isUrl}
onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange}
/>
</div>
{errorText}
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
disabled={this.state.busy}
/>
<Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")}
placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl}
onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange}
disabled={this.state.busy}
/>
</div>
{submitButton}
</form>
</div>
);
}

View file

@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
@ -32,8 +34,13 @@ export const TYPES = {
label: () => _t('Free'),
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
description: () => _t('Join millions for free on the largest public server'),
hsUrl: 'https://matrix.org',
isUrl: 'https://vector.im',
serverConfig: makeType(ValidatedServerConfig, {
hsUrl: "https://matrix.org",
hsName: "matrix.org",
hsNameIsDifferent: false,
isUrl: "https://vector.im",
identityEnabled: true,
}),
},
PREMIUM: {
id: PREMIUM,
@ -44,6 +51,7 @@ export const TYPES = {
{sub}
</a>,
}),
identityServerUrl: "https://vector.im",
},
ADVANCED: {
id: ADVANCED,
@ -56,10 +64,11 @@ export const TYPES = {
},
};
export function getTypeFromHsUrl(hsUrl) {
export function getTypeFromServerConfig(config) {
const {hsUrl} = config;
if (!hsUrl) {
return null;
} else if (hsUrl === TYPES.FREE.hsUrl) {
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
return FREE;
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
// This is an unlikely case to reach, as Modular defaults to hiding the
@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent {
selected: PropTypes.string,
// Handler called when the selected type changes.
onChange: PropTypes.func.isRequired,
}
};
constructor(props) {
super(props);
@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent {
e.stopPropagation();
const type = e.currentTarget.dataset.id;
this.updateSelectedType(type);
}
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
@ -104,9 +105,13 @@ module.exports = React.createClass({
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
const urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
let urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
}
}
let defaultImageUrl = null;
@ -133,40 +138,7 @@ module.exports = React.createClass({
}
},
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
*/
_getInitialLetter: function(name) {
if (name.length < 1) {
return undefined;
}
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
let chars = 1;
const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
const second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
@ -176,20 +148,20 @@ module.exports = React.createClass({
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{ initialLetter }
</EmojiText>
</span>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
width={width} height={height} aria-hidden="true" />
);
if (onClick != null) {
return (

View file

@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import sdk from "../../../index";
import DMRoomMap from '../../../utils/DMRoomMap';
import Avatar from '../../../Avatar';
module.exports = React.createClass({
displayName: 'RoomAvatar',
@ -89,7 +89,6 @@ module.exports = React.createClass({
props.resizeMethod,
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), // lowest priority
].filter(function(url) {
return (url != null && url != "");
});
@ -98,41 +97,14 @@ module.exports = React.createClass({
getRoomAvatarUrl: function(props) {
if (!props.room) return null;
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
return Avatar.avatarUrlForRoom(
props.room,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
},
getOneToOneAvatar: function(props) {
const room = props.room;
if (!room) {
return null;
}
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
}
return null;
},
onRoomAvatarClick: function() {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),

View file

@ -1,5 +1,6 @@
/*
Copyright 2018, 2019 New Vector 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.
@ -29,6 +30,10 @@ export class TopLeftMenu extends React.Component {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageSection = null;
let homePageItem = null;
if (this.hasHomePage()) {
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
</ul>;
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}>
{_t("Home")}
</li>;
}
let signInOutSection;
let signInOutItem;
if (isGuest) {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
</ul>;
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}>
{_t("Sign in")}
</li>;
} else {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
</ul>;
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}>
{_t("Sign out")}
</li>;
}
return <div className="mx_TopLeftMenu">
<div className="mx_TopLeftMenu_section_noIcon">
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}>
{_t("Settings")}
</li>;
return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}>
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
{homePageSection}
<ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
{homePageItem}
{settingsItem}
{signInOutItem}
</ul>
{signInOutSection}
</div>;
}

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

@ -28,6 +28,7 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
@ -67,7 +68,7 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",

View file

@ -63,6 +63,10 @@ export default function AccessibleButton(props) {
};
}
// Pass through the ref - used for keyboard shortcut access to some buttons
restProps.ref = restProps.inputRef;
delete restProps.inputRef;
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") +
@ -89,6 +93,7 @@ export default function AccessibleButton(props) {
*/
AccessibleButton.propTypes = {
children: PropTypes.node,
inputRef: PropTypes.func,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,

View file

@ -1,43 +0,0 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 New Vector Ltd
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 {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
const {element, children, addAlt, ...restProps} = props;
// fast path: simple regex to detect strings that don't contain
// emoji and just return them
if (containsEmoji(children)) {
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
return React.createElement(element, restProps);
} else {
return React.createElement(element, restProps, children);
}
}
EmojiText.propTypes = {
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {
element: 'span',
addAlt: true,
};

View file

@ -195,13 +195,13 @@ export default class ImageView extends React.Component {
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" onClick={ this.rotateCounterClockwise }>
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_rotateClockwise" onClick={ this.rotateClockwise }>
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }>
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
</AccessibleButton>
<div className="mx_ImageView_shim">

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 });
});
@ -114,13 +117,9 @@ module.exports = React.createClass({
return null;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
<EmojiText>
{ summaries.join(", ") }
</EmojiText>
{ summaries.join(", ") }
</span>
);
},
@ -132,7 +131,7 @@ module.exports = React.createClass({
* included before "and [n] others".
*/
_renderNameList: function(users) {
return this._renderCommaSeparatedList(users, this.props.summaryLength);
return formatCommaSeparatedList(users, this.props.summaryLength);
},
/**
@ -283,35 +282,6 @@ module.exports = React.createClass({
return res;
},
/**
* Constructs a written English string representing `items`, with an optional limit on
* the number of items included in the result. If specified and if the length of
*`items` is greater than the limit, the string "and n others" will be appended onto
* the result.
* If `items` is empty, returns the empty string. If there is only one item, return
* it.
* @param {string[]} items the items to construct a string from.
* @param {number?} itemLimit the number by which to limit the list.
* @returns {string} a string constructed by joining `items` with a comma between each
* item, but with the last item appended as " and [lastItem]".
*/
_renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0,
);
if (items.length === 0) {
return "";
} else if (items.length === 1) {
return items[0];
} else if (remaining > 0) {
items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else {
const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
}
},
_renderAvatars: function(roomMembers) {
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
return (

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -21,12 +22,14 @@ import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames';
export default class MessageEditor extends React.Component {
static propTypes = {
@ -40,22 +43,28 @@ export default class MessageEditor extends React.Component {
constructor(props, context) {
super(props, context);
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event),
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
this.state = {
autoComplete: null,
room,
};
this._editorRef = null;
this._autocompleteRef = null;
this._hasModifications = false;
}
_getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
}
_updateEditorState = (caret) => {
@ -71,48 +80,99 @@ export default class MessageEditor extends React.Component {
}
_onInput = (event) => {
this._hasModifications = true;
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
this.model.update(text, event.inputType, caret);
}
_isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
}
_isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
}
_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();
} else if (event.key === "ArrowUp") {
if (this._hasModifications || !this._isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
}
} else if (event.key === "ArrowDown") {
if (this._hasModifications || !this._isCaretAtEnd()) {
return;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
}
event.preventDefault();
}
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});
dis.dispatch({action: 'focus_composer'});
}
_onSaveClicked = () => {
_sendEdit = () => {
const newContent = {
"msgtype": "m.text",
"body": textSerialize(this.model),
};
if (requiresHtml(this.model)) {
const contentBody = {
msgtype: newContent.msgtype,
body: ` * ${newContent.body}`,
};
const formattedBody = htmlSerializeIfNeeded(this.model);
if (formattedBody) {
newContent.format = "org.matrix.custom.html";
newContent.formatted_body = htmlSerialize(this.model);
newContent.formatted_body = formattedBody;
contentBody.format = newContent.format;
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
}
const content = Object.assign({
"m.new_content": newContent,
@ -120,12 +180,13 @@ export default class MessageEditor extends React.Component {
"rel_type": "m.replace",
"event_id": this.props.event.getId(),
},
}, newContent);
}, contentBody);
const roomId = this.props.event.getRoomId();
this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
}
_onAutoCompleteConfirm = (completion) => {
@ -138,6 +199,8 @@ export default class MessageEditor extends React.Component {
componentDidMount() {
this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
this._editorRef.focus();
}
render() {
@ -157,7 +220,7 @@ export default class MessageEditor extends React.Component {
</div>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div className="mx_MessageEditor">
return <div className={classNames("mx_MessageEditor", this.props.className)}>
{ autoComplete }
<div
className="mx_MessageEditor_editor"
@ -166,10 +229,11 @@ export default class MessageEditor extends React.Component {
onInput={this._onInput}
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
aria-label={_t("Edit message")}
></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

@ -63,6 +63,11 @@ export default class ReplyThread extends React.Component {
static getParentEventId(ev) {
if (!ev || ev.isRedacted()) return;
// XXX: For newer relations (annotations, replacements, etc.), we now
// have a `getRelation` helper on the event, and you might assume it
// could be used here for replies as well... However, the helper
// currently assumes the relation has a `rel_type`, which older replies
// do not, so this block is left as-is for now.
const mRelatesTo = ev.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
const mInReplyTo = mRelatesTo['m.in_reply_to'];

View file

@ -0,0 +1,56 @@
/*
Copyright 2019 New Vector Ltd.
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 sdk from '../../../index';
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
tooltip: PropTypes.string.isRequired,
};
constructor() {
super();
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({hover: true});
};
onMouseOut = () => {
this.setState({hover: false});
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
return (
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}>
{this.props.children}
<Tooltip
label={this.props.tooltip}
visible={this.state.hover}
className={"mx_TextWithTooltip_tooltip"} />
</span>
);
}
}

View file

@ -79,6 +79,10 @@ module.exports = React.createClass({
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
} else {
// The tooltip is larger than the parent height: figure out what offset
// we need so that we're still centered.
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset;

View file

@ -117,7 +117,6 @@ export default React.createClass({
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ?
@ -129,9 +128,9 @@ export default React.createClass({
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
});
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
{ groupName }
</EmojiText>;
</div>;
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {

View file

@ -180,7 +180,6 @@ module.exports = React.createClass({
this.props.groupMember.displayname || this.props.groupMember.userId
);
const EmojiText = sdk.getComponent('elements.EmojiText');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return (
<div className="mx_MemberInfo">
@ -189,7 +188,7 @@ module.exports = React.createClass({
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
{ avatarElement }
<EmojiText element="h2">{ groupMemberName }</EmojiText>
<h2>{ groupMemberName }</h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View file

@ -149,7 +149,6 @@ module.exports = React.createClass({
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -221,7 +220,7 @@ module.exports = React.createClass({
</AccessibleButton>
{ avatarElement }
<EmojiText element="h2">{ groupRoomName }</EmojiText>
<h2>{ groupRoomName }</h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View file

@ -145,6 +145,7 @@ function remoteRender(event) {
a.target = data.target;
a.download = data.download;
a.style = data.style;
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
a.href = window.URL.createObjectURL(data.blob);
a.appendChild(img);
a.appendChild(document.createTextNode(data.textContent));

View file

@ -172,8 +172,8 @@ export default class MImageBody extends React.Component {
// thumbnail resolution will be unnecessarily reduced.
// custom timeline widths seems preferable.
const pixelRatio = window.devicePixelRatio;
const thumbWidth = 800 * pixelRatio;
const thumbHeight = 600 * pixelRatio;
const thumbWidth = Math.round(800 * pixelRatio);
const thumbHeight = Math.round(600 * pixelRatio);
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {

View file

@ -23,7 +23,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import { createMenu } from '../../structures/ContextualMenu';
import SettingsStore from '../../../settings/SettingsStore';
import { isContentActionable } from '../../../utils/EventUtils';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
@ -148,12 +148,12 @@ export default class MessageActionBar extends React.PureComponent {
title={_t("Reply")}
onClick={this.onReplyClick}
/>;
if (this.isEditingEnabled()) {
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")}
onClick={this.onEditClick}
/>;
}
}
if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) {
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")}
onClick={this.onEditClick}
/>;
}
return <div className="mx_MessageActionBar">

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

@ -37,6 +37,7 @@ export default class ReactionDimension extends React.PureComponent {
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.remove", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
@ -44,6 +45,7 @@ export default class ReactionDimension extends React.PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
this.onReactionsChange();
}
@ -55,6 +57,10 @@ export default class ReactionDimension extends React.PureComponent {
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
@ -82,7 +88,7 @@ export default class ReactionDimension extends React.PureComponent {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === option;
return mxEvent.getRelation().key === option;
});
if (!reactionForOption) {
continue;
@ -107,7 +113,11 @@ export default class ReactionDimension extends React.PureComponent {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
return reactions.getAnnotationsBySender()[userId];
const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) {
return null;
}
return [...myReactions.values()];
}
onOptionClick = (ev) => {
@ -158,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

@ -34,6 +34,7 @@ export default class ReactionsRow extends React.PureComponent {
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.remove", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
@ -45,6 +46,7 @@ export default class ReactionsRow extends React.PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
this.onReactionsChange();
}
@ -56,6 +58,10 @@ export default class ReactionsRow extends React.PureComponent {
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
@ -80,7 +86,11 @@ export default class ReactionsRow extends React.PureComponent {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
return reactions.getAnnotationsBySender()[userId];
const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) {
return null;
}
return [...myReactions.values()];
}
render() {
@ -101,13 +111,13 @@ export default class ReactionsRow extends React.PureComponent {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === content;
return mxEvent.getRelation().key === content;
});
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,84 @@
/*
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 { unicodeToShortcode } 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 = unicodeToShortcode(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) => {
if (!shortName) {
return null;
}
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

@ -19,7 +19,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
@ -95,7 +94,6 @@ export default React.createClass({
},
render() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const {mxEvent} = this.props;
const colorClass = getUserNameColorClass(mxEvent.getSender());
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
@ -117,7 +115,7 @@ export default React.createClass({
/>;
}
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
const nameElem = name || '';
// Name + flair
const nameFlair = <span>

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector 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.
@ -22,6 +23,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 +90,12 @@ module.exports = React.createClass({
componentDidMount: function() {
this._unmounted = false;
if (!this.props.isEditing) {
this._applyFormatting();
}
},
_applyFormatting() {
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
@ -123,8 +130,14 @@ module.exports = React.createClass({
}
},
componentDidUpdate: function() {
this.calculateUrlPreview();
componentDidUpdate: function(prevProps) {
if (!this.props.isEditing) {
const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) {
this._applyFormatting();
}
}
},
componentWillUnmount: function() {
@ -140,14 +153,16 @@ 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);
},
calculateUrlPreview: function() {
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview && !this.state.links.length) {
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);
if (links.length) {
// de-dup the links (but preserve ordering)
@ -425,8 +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() {
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this.props.isEditing) {
const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
@ -436,6 +482,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>;
@ -462,12 +511,12 @@ module.exports = React.createClass({
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<EmojiText
<span
className="mx_MEmoteBody_sender"
onClick={this.onEmoteSenderClick}
>
{ name }
</EmojiText>
</span>
&nbsp;
{ body }
{ widgets }

View file

@ -20,7 +20,6 @@ const React = require('react');
import PropTypes from 'prop-types';
const TextForEvent = require('../../../TextForEvent');
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'TextualEvent',
@ -31,11 +30,10 @@ module.exports = React.createClass({
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null;
return (
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText>
<div className="mx_TextualEvent">{ text }</div>
);
},
});

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

@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component {
}
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
{ completionResult.provider.renderCompletions(completions) }
</div>
) : null;

View file

@ -111,7 +111,6 @@ const EntityTile = React.createClass({
let nameEl;
const {name} = this.props;
const EmojiText = sdk.getComponent('elements.EmojiText');
if (!this.props.suppressOnHover) {
const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
}
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
<div className="mx_EntityTile_name" dir="auto">
{ name }
</EmojiText>
</div>
{presenceLabel}
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
<div className="mx_EntityTile_name" dir="auto">
{name}
</EmojiText>
</div>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div>
);
} else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
<div className="mx_EntityTile_name" dir="auto">{ name }</div>
);
}

View file

@ -160,8 +160,11 @@ module.exports = withMatrixClient(React.createClass({
// show twelve hour timestamps
isTwelveHour: PropTypes.bool,
// helper function to access relations for an event
// helper function to access relations for this event
getRelationsForEvent: PropTypes.func,
// whether to show reactions for this event
showReactions: PropTypes.bool,
},
getDefaultProps: function() {
@ -198,7 +201,7 @@ module.exports = withMatrixClient(React.createClass({
const client = this.props.matrixClient;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
}
},
@ -223,7 +226,7 @@ module.exports = withMatrixClient(React.createClass({
const client = this.props.matrixClient;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
}
},
@ -485,6 +488,7 @@ module.exports = withMatrixClient(React.createClass({
getReactions() {
if (
!this.props.showReactions ||
!this.props.getRelationsForEvent ||
!SettingsStore.isFeatureEnabled("feature_reactions")
) {
@ -520,7 +524,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 +547,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 +625,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;
@ -674,14 +682,13 @@ module.exports = withMatrixClient(React.createClass({
switch (this.props.tileShape) {
case 'notif': {
const EmojiText = sdk.getComponent('elements.EmojiText');
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</EmojiText>
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
@ -780,6 +787,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}
@ -789,7 +797,7 @@ module.exports = withMatrixClient(React.createClass({
{ actionBar }
</div>
{
// The avatar goes after the event tile as it's absolutly positioned to be over the
// The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
// the need for further z-indexing chaos)
}

View file

@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
}
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const EmojiText = sdk.getComponent('elements.EmojiText');
let backButton;
if (this.props.member.roomId) {
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo_name">
{ backButton }
{ e2eIconElement }
<EmojiText element="h2">{ memberName }</EmojiText>
<h2>{ memberName }</h2>
</div>
{ avatarElement }
<div className="mx_MemberInfo_container">

View file

@ -40,21 +40,18 @@ import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
import Autocomplete from './Autocomplete';
import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessages from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
import {
asciiRegexp, unicodeRegexp, shortnameToUnicode,
asciiList, mapUnicodeToShort, toShort,
} from 'emojione';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeUserPermalink} from "../../../matrix-to";
import ReplyPreview from "./ReplyPreview";
@ -62,10 +59,9 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
import AccessibleButton from '../elements/AccessibleButton';
import {findEditableEvent} from '../../../utils/EventUtils';
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
@ -144,7 +140,6 @@ export default class MessageComposerInput extends React.Component {
client: MatrixClient;
autocomplete: Autocomplete;
historyManager: ComposerHistoryManager;
constructor(props, context) {
super(props, context);
@ -273,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
case 'emoji':
// XXX: apparently you can't return plain strings from serializer rules
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
return <b>{ obj.data.get('emojiUnicode') }</b>;
// So instead we temporarily wrap emoji from RTE in a span.
return <span>{ obj.data.get('emojiUnicode') }</span>;
}
return this.renderNode({
node: obj,
@ -335,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
}
componentWillUnmount() {
@ -375,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
forComposerQuote: true,
returnString: true,
emojiOne: false,
});
const fragment = this.html.deserialize(html);
// FIXME: do we want to put in a permalink to the original quote here?
@ -483,6 +475,7 @@ export default class MessageComposerInput extends React.Component {
sendTyping(isTyping) {
if (!SettingsStore.getValue('sendTypingNotifications')) return;
if (SettingsStore.getValue('lowBandwidth')) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT,
@ -538,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
// Automatic replacement of plaintext emoji to Unicode emoji
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emojiMatch) {
// plaintext -> hex unicode
const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emoticonMatch) {
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
const unicodeEmoji = data ? data.unicode : '';
const range = Range.create({
anchor: {
key: editorState.startText.key,
offset: currentStartOffset - emojiMatch[1].length - 1,
offset: currentStartOffset - emoticonMatch[1].length - 1,
},
focus: {
key: editorState.startText.key,
@ -561,54 +552,6 @@ export default class MessageComposerInput extends React.Component {
}
}
// emojioneify any emoji
let foundEmoji;
do {
foundEmoji = false;
for (const node of editorState.document.getTexts()) {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
EMOJI_REGEX.lastIndex = 0;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
// if we replaced an emoji, start again looking for more
// emoji in the new editor state since doing the replacement
// will change the node structure & offsets so we can't compute
// insertion ranges from node.key / match.index anymore.
foundEmoji = true;
break;
}
}
}
} while (foundEmoji);
// work around weird bug where inserting emoji via the macOS
// emoji picker can leave the selection stuck in the emoji's
// child text. This seems to happen due to selection getting
// moved in the normalisation phase after calculating these changes
if (editorState.selection.anchor.key &&
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
change = change.moveToStartOfNextText();
editorState = change.value;
}
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
let blockType = editorState.blocks.first().type;
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
@ -1046,6 +989,12 @@ export default class MessageComposerInput extends React.Component {
return change.insertText('\n');
}
if (this.autocomplete.countCompletions() > 0) {
this.autocomplete.hide();
ev.preventDefault();
return true;
}
const editorState = this.state.editorState;
const lastBlock = editorState.blocks.last();
@ -1087,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
if (cmd) {
if (!cmd.error) {
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
this.setState({
editorState: this.createEditorState(),
}, ()=>{
@ -1165,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
let sendTextFn = ContentHelpers.makeTextMessage;
this.historyManager.save(
editorState,
this.state.isRichTextEnabled ? 'rich' : 'markdown',
);
if (commandText && commandText.startsWith('/me')) {
if (replyingToEv) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -1244,14 +1187,16 @@ export default class MessageComposerInput extends React.Component {
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return;
} else {
if (!selection.anchor.isAtEndOfNode(document)) return;
}
const selected = this.selectHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
} else {
this.moveAutocompleteSelection(up);
@ -1259,54 +1204,6 @@ export default class MessageComposerInput extends React.Component {
}
};
selectHistory = async (up) => {
const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message
if (this.historyManager.currentIndex === this.historyManager.history.length) {
// We can't go any further - there isn't any more history, so nop.
if (!up) {
return;
}
this.setState({
currentlyComposedEditorState: this.state.editorState,
});
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
// True when we return to the message being composed currently
this.setState({
editorState: this.state.currentlyComposedEditorState,
});
this.historyManager.currentIndex = this.historyManager.history.length;
return;
}
let editorState;
const historyItem = this.historyManager.getItem(delta);
if (!historyItem) return;
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
editorState = this.richToMdEditorState(historyItem.value);
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
editorState = this.mdToRichEditorState(historyItem.value);
} else {
editorState = historyItem.value;
}
// Move selection to the end of the selected history
const change = editorState.change().moveToEndOfNode(editorState.document);
// We don't call this.onChange(change) now, as fixups on stuff like emoji
// should already have been done and persisted in the history.
editorState = change.value;
this.suppressAutoComplete = true;
this.setState({ editorState }, ()=>{
this._editor.focus();
});
return true;
};
onTab = async (e) => {
this.setState({
someCompletions: null,
@ -1475,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
}
case 'emoji': {
const { data } = node;
const emojiUnicode = data.get('emojiUnicode');
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
const shortname = toShort(emojiUnicode);
const className = classNames('mx_emojione', {
mx_emojione_selected: isSelected,
});
const style = {};
if (props.selected) style.border = '1px solid blue';
return <img className={ className } src={ uri }
title={ shortname } alt={ emojiUnicode } style={style}
/>;
return data.get('emojiUnicode');
}
}
};

View file

@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
if (!this.state.event) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
{ '💬 ' + _t('Replying') }
</EmojiText>
</div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
onClick={cancelQuoting} />

View file

@ -147,7 +147,6 @@ module.exports = React.createClass({
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const EmojiText = sdk.getComponent('elements.EmojiText');
let searchStatus = null;
let cancelButton = null;
@ -191,10 +190,10 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
<div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
{ searchStatus }
</div>;

View file

@ -750,6 +750,7 @@ module.exports = React.createClass({
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
addRoomLabel: _t("Start chat"),
},
{
list: this.state.lists['im.vector.fake.recent'],

View file

@ -342,7 +342,6 @@ module.exports = React.createClass({
badge = <div className={badgeClasses}>{ badgeContent }</div>;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
let label;
let subtextLabel;
let tooltip;
@ -354,14 +353,7 @@ module.exports = React.createClass({
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
} else {
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
}
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
} else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import WhoIsTyping from '../../../WhoIsTyping';
import Timer from '../../../utils/Timer';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -212,15 +211,13 @@ module.exports = React.createClass({
return (<div className="mx_WhoIsTypingTile_empty" />);
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<li className="mx_WhoIsTypingTile">
<div className="mx_WhoIsTypingTile_avatars">
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div>
<div className="mx_WhoIsTypingTile_label">
<EmojiText>{ typingString }</EmojiText>
{ typingString }
</div>
</li>
);

View file

@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
} else if (this.state.loading) {
return <Spinner />;
} else if (this.state.backupInfo) {
const EmojiText = sdk.getComponent('elements.EmojiText');
let clientBackupStatus;
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("This device is backing up your keys. ")}<EmojiText></EmojiText></p>
<p> {_t("This device is backing up your keys. ")}</p>
</div>;
} else {
clientBackupStatus = <div>

View file

@ -52,6 +52,8 @@ 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} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
</div>
</div>
);

View file

@ -36,7 +36,6 @@ export default class VerificationShowSas extends React.Component {
render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let sasDisplay;
let sasCaption;
@ -44,7 +43,7 @@ export default class VerificationShowSas extends React.Component {
const emojiBlocks = this.props.sas.emoji.map(
(emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}>
<div className="mx_VerificationShowSas_emojiSas_emoji">
<EmojiText addAlt={false}>{emoji[0]}</EmojiText>
{ emoji[0] }
</div>
<div className="mx_VerificationShowSas_emojiSas_label">
{_t(capFirst(emoji[1]))}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -17,11 +18,12 @@ limitations under the License.
import {UserPillPart, RoomPillPart, PlainPart} from "./parts";
export default class AutocompleteWrapperModel {
constructor(updateCallback, getAutocompleterComponent, updateQuery) {
constructor(updateCallback, getAutocompleterComponent, updateQuery, room) {
this._updateCallback = updateCallback;
this._getAutocompleterComponent = getAutocompleterComponent;
this._updateQuery = updateQuery;
this._query = null;
this._room = room;
}
onEscape(e) {
@ -37,8 +39,24 @@ export default class AutocompleteWrapperModel {
this._updateCallback({close: true});
}
onTab() {
//forceCompletion here?
async onTab(e) {
const acComponent = this._getAutocompleterComponent();
if (acComponent.state.completionList.length === 0) {
// Force completions to show for the text currently entered
await acComponent.forceComplete();
// Select the first item by moving "down"
await acComponent.onDownArrow();
} else {
if (e.shiftKey) {
await acComponent.onUpArrow();
} else {
await acComponent.onDownArrow();
}
}
this._updateCallback({
close: true,
});
}
onUpArrow() {
@ -83,7 +101,8 @@ export default class AutocompleteWrapperModel {
case "@": {
const displayName = completion.completion;
const userId = completion.completionId;
return new UserPillPart(userId, displayName);
const member = this._room.getMember(userId);
return new UserPillPart(userId, displayName, member);
}
case "#": {
const displayAlias = completion.completionId;

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -19,13 +20,23 @@ export function setCaretPosition(editor, model, caretPosition) {
sel.removeAllRanges();
const range = document.createRange();
const {parts} = model;
const {index} = caretPosition;
let {offset} = caretPosition;
let lineIndex = 0;
let nodeIndex = -1;
for (let i = 0; i <= caretPosition.index; ++i) {
for (let i = 0; i <= index; ++i) {
const part = parts[i];
if (part && part.type === "newline") {
lineIndex += 1;
nodeIndex = -1;
if (i < index) {
lineIndex += 1;
nodeIndex = -1;
} else {
// if index points at a newline part,
// put the caret at the end of the previous part
// so it stays on the same line
const prevPart = parts[i - 1];
offset = prevPart ? prevPart.text.length : 0;
}
} else {
nodeIndex += 1;
}
@ -33,14 +44,11 @@ export function setCaretPosition(editor, model, caretPosition) {
let focusNode;
const lineNode = editor.childNodes[lineIndex];
if (lineNode) {
if (lineNode.childNodes.length === 0 && caretPosition.offset === 0) {
focusNode = lineNode.childNodes[nodeIndex];
if (!focusNode) {
focusNode = lineNode;
} else {
focusNode = lineNode.childNodes[nodeIndex];
if (focusNode && focusNode.nodeType === Node.ELEMENT_NODE) {
focusNode = focusNode.childNodes[0];
}
} else if (focusNode.nodeType === Node.ELEMENT_NODE) {
focusNode = focusNode.childNodes[0];
}
}
// node not found, set caret at end
@ -49,7 +57,7 @@ export function setCaretPosition(editor, model, caretPosition) {
range.collapse(false);
} else {
// make sure we have a text node
range.setStart(focusNode, caretPosition.offset);
range.setStart(focusNode, offset);
range.collapse(true);
}
sel.addRange(range);

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -16,48 +17,198 @@ limitations under the License.
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
import { walkDOMDepthFirst } from "./dom";
function parseHtmlMessage(html) {
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine
const nodes = Array.from(new DOMParser().parseFromString(html, "text/html").body.childNodes);
const parts = nodes.map(n => {
switch (n.nodeType) {
case Node.TEXT_NODE:
return new PlainPart(n.nodeValue);
case Node.ELEMENT_NODE:
switch (n.nodeName) {
case "MX-REPLY":
return null;
case "A": {
const {href} = n;
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
const resourceId = pillMatch[1]; // The room/user ID
const prefix = pillMatch[2]; // The first character of prefix
switch (prefix) {
case "@": return new UserPillPart(resourceId, n.textContent);
case "#": return new RoomPillPart(resourceId, n.textContent);
default: return new PlainPart(n.textContent);
}
}
case "BR":
return new NewlinePart("\n");
default:
return new PlainPart(n.textContent);
}
default:
return null;
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
function parseLink(a, room) {
const {href} = a;
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
const resourceId = pillMatch[1]; // The room/user ID
const prefix = pillMatch[2]; // The first character of prefix
switch (prefix) {
case "@":
return new UserPillPart(
resourceId,
a.textContent,
room.getMember(resourceId),
);
case "#":
return new RoomPillPart(resourceId);
default: {
if (href === a.textContent) {
return new PlainPart(a.textContent);
} else {
return new PlainPart(`[${a.textContent}](${href})`);
}
}
}).filter(p => !!p);
}
}
function parseCodeBlock(n) {
const parts = [];
const preLines = ("```\n" + n.textContent + "```").split("\n");
preLines.forEach((l, i) => {
parts.push(new PlainPart(l));
if (i < preLines.length - 1) {
parts.push(new NewlinePart("\n"));
}
});
return parts;
}
export function parseEvent(event) {
function parseElement(n, room) {
switch (n.nodeName) {
case "A":
return parseLink(n, room);
case "BR":
return new NewlinePart("\n");
case "EM":
return new PlainPart(`*${n.textContent}*`);
case "STRONG":
return new PlainPart(`**${n.textContent}**`);
case "PRE":
return parseCodeBlock(n);
case "CODE":
return new PlainPart(`\`${n.textContent}\``);
case "DEL":
return new PlainPart(`<del>${n.textContent}</del>`);
case "LI":
if (n.parentElement.nodeName === "OL") {
return new PlainPart(` 1. `);
} else {
return new PlainPart(` - `);
}
default:
// don't textify block nodes we'll decend into
if (!checkDecendInto(n)) {
return new PlainPart(n.textContent);
}
}
}
function checkDecendInto(node) {
switch (node.nodeName) {
case "PRE":
// a code block is textified in parseCodeBlock
// as we don't want to preserve markup in it,
// so no need to decend into it
return false;
default:
return checkBlockNode(node);
}
}
function checkBlockNode(node) {
switch (node.nodeName) {
case "PRE":
case "BLOCKQUOTE":
case "DIV":
case "P":
case "UL":
case "OL":
case "LI":
return true;
default:
return false;
}
}
function checkIgnored(n) {
if (n.nodeType === Node.TEXT_NODE) {
// riot adds \n text nodes in a lot of places,
// which should be ignored
return n.nodeValue === "\n";
} else if (n.nodeType === Node.ELEMENT_NODE) {
return n.nodeName === "MX-REPLY";
}
return true;
}
function prefixQuoteLines(isFirstNode, parts) {
const PREFIX = "> ";
// a newline (to append a > to) wouldn't be added to parts for the first line
// if there was no content before the BLOCKQUOTE, so handle that
if (isFirstNode) {
parts.splice(0, 0, new PlainPart(PREFIX));
}
for (let i = 0; i < parts.length; i += 1) {
if (parts[i].type === "newline") {
parts.splice(i + 1, 0, new PlainPart(PREFIX));
i += 1;
}
}
}
function parseHtmlMessage(html, room) {
// no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
const parts = [];
let lastNode;
let inQuote = false;
function onNodeEnter(n) {
if (checkIgnored(n)) {
return false;
}
if (n.nodeName === "BLOCKQUOTE") {
inQuote = true;
}
const newParts = [];
if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
newParts.push(new NewlinePart("\n"));
}
if (n.nodeType === Node.TEXT_NODE) {
newParts.push(new PlainPart(n.nodeValue));
} else if (n.nodeType === Node.ELEMENT_NODE) {
const parseResult = parseElement(n, room);
if (parseResult) {
if (Array.isArray(parseResult)) {
newParts.push(...parseResult);
} else {
newParts.push(parseResult);
}
}
}
if (newParts.length && inQuote) {
const isFirstPart = parts.length === 0;
prefixQuoteLines(isFirstPart, newParts);
}
parts.push(...newParts);
// extra newline after quote, only if there something behind it...
if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
parts.push(new NewlinePart("\n"));
}
lastNode = null;
return checkDecendInto(n);
}
function onNodeLeave(n) {
if (checkIgnored(n)) {
return;
}
if (n.nodeName === "BLOCKQUOTE") {
inQuote = false;
}
lastNode = n;
}
walkDOMDepthFirst(rootNode, onNodeEnter, onNodeLeave);
return parts;
}
export function parseEvent(event, room) {
const content = event.getContent();
if (content.format === "org.matrix.custom.html") {
return parseHtmlMessage(content.formatted_body || "");
return parseHtmlMessage(content.formatted_body || "", room);
} else {
const body = content.body || "";
const lines = body.split("\n");

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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,22 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
function walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback) {
let node = editor.firstChild;
while (node && node !== editor) {
enterNodeCallback(node);
if (node.firstChild) {
export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
let node = rootNode.firstChild;
while (node && node !== rootNode) {
const shouldDecend = enterNodeCallback(node);
if (shouldDecend && node.firstChild) {
node = node.firstChild;
} else if (node.nextSibling) {
node = node.nextSibling;
} else {
while (!node.nextSibling && node !== editor) {
while (!node.nextSibling && node !== rootNode) {
node = node.parentElement;
if (node !== editor) {
if (node !== rootNode) {
leaveNodeCallback(node);
}
}
if (node !== editor) {
if (node !== rootNode) {
node = node.nextSibling;
}
}
@ -61,6 +62,7 @@ export function getCaretOffsetAndText(editor, sel) {
}
text += nodeText;
}
return true;
}
function leaveNodeCallback(node) {

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -61,6 +62,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 +99,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 +184,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;
@ -215,8 +232,9 @@ export default class EditorModel {
index += 1;
this._insertPart(index, splitPart);
}
} else {
// not-editable, insert str after this part
} else if (offset !== 0) {
// not-editable part, caret is not at start,
// so insert str after this part
addLen += part.text.length - offset;
index += 1;
}
@ -261,4 +279,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

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -15,6 +16,8 @@ limitations under the License.
*/
import AutocompleteWrapperModel from "./autocomplete";
import Avatar from "../Avatar";
import MatrixClientPeg from "../MatrixClientPeg";
class BasePart {
constructor(text = "") {
@ -57,7 +60,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);
}
@ -150,21 +153,21 @@ class PillPart extends BasePart {
toDOMNode() {
const container = document.createElement("span");
container.className = this.type;
container.className = this.className;
container.appendChild(document.createTextNode(this.text));
this.setAvatar(container);
return container;
}
updateDOMNode(node) {
const textNode = node.childNodes[0];
if (textNode.textContent !== this.text) {
// console.log("changing pill text from", textNode.textContent, "to", this.text);
textNode.textContent = this.text;
}
if (node.className !== this.type) {
// console.log("turning", node.className, "into", this.type);
node.className = this.type;
if (node.className !== this.className) {
node.className = this.className;
}
this.setAvatar(node);
}
canUpdateDOMNode(node) {
@ -174,14 +177,28 @@ class PillPart extends BasePart {
node.childNodes[0].nodeType === Node.TEXT_NODE;
}
// helper method for subclasses
_setAvatarVars(node, avatarUrl, initialLetter) {
const avatarBackground = `url('${avatarUrl}')`;
const avatarLetter = `'${initialLetter}'`;
// check if the value is changing,
// otherwise the avatars flicker on every keystroke while updating.
if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
node.style.setProperty("--avatar-background", avatarBackground);
}
if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) {
node.style.setProperty("--avatar-letter", avatarLetter);
}
}
get canEdit() {
return false;
}
}
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,22 +222,84 @@ 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 {
constructor(displayAlias) {
super(displayAlias, displayAlias);
this._room = this._findRoomByAlias(displayAlias);
}
_findRoomByAlias(alias) {
const client = MatrixClientPeg.get();
if (alias[0] === '#') {
return client.getRooms().find((r) => {
return r.getAliases().includes(alias);
});
} else {
return client.getRoom(alias);
}
}
setAvatar(node) {
let initialLetter = "";
let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
if (!avatarUrl) {
initialLetter = Avatar.getInitialLetter(this._room.name);
avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`;
}
this._setAvatarVars(node, avatarUrl, initialLetter);
}
get type() {
return "room-pill";
}
get className() {
return "mx_RoomPill mx_Pill";
}
}
export class UserPillPart extends PillPart {
constructor(userId, displayName, member) {
super(userId, displayName);
this._member = member;
}
setAvatar(node) {
const name = this._member.name || this._member.userId;
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId);
let avatarUrl = Avatar.avatarUrlForMember(
this._member,
16 * window.devicePixelRatio,
16 * window.devicePixelRatio);
let initialLetter = "";
if (avatarUrl === defaultAvatarUrl) {
// the url from defaultAvatarUrlForString is meant to go in an img element,
// which has the base of the document. we're using it in css,
// which has the base of the theme css file, two levels deeper than the document,
// so go up to the level of the document.
avatarUrl = `../../${avatarUrl}`;
initialLetter = Avatar.getInitialLetter(name);
}
this._setAvatarVars(node, avatarUrl, initialLetter);
}
get type() {
return "user-pill";
}
get className() {
return "mx_UserPill mx_Pill";
}
}
@ -234,8 +313,16 @@ export class PillCandidatePart extends PlainPart {
return this._autoCompleteCreator(updateCallback);
}
acceptsInsertion(chr) {
return true;
acceptsInsertion(chr, i) {
if ((this.text.length + i) === 0) {
return true;
} else {
return super.acceptsInsertion(chr, i);
}
}
merge() {
return false;
}
acceptsRemoval(position, chr) {
@ -248,9 +335,14 @@ export class PillCandidatePart extends PlainPart {
}
export class PartCreator {
constructor(getAutocompleterComponent, updateQuery) {
constructor(getAutocompleterComponent, updateQuery, room) {
this._autoCompleteCreator = (updateCallback) => {
return new AutocompleteWrapperModel(updateCallback, getAutocompleterComponent, updateQuery);
return new AutocompleteWrapperModel(
updateCallback,
getAutocompleterComponent,
updateQuery,
room,
);
};
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector 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.
@ -60,12 +61,13 @@ export function renderModel(editor, model) {
let foundBR = false;
let partNode = lineContainer.firstChild;
while (partNode) {
const nextNode = partNode.nextSibling;
if (!foundBR && partNode.tagName === "BR") {
foundBR = true;
} else {
lineContainer.removeChild(partNode);
}
partNode = partNode.nextSibling;
partNode = nextNode;
}
if (!foundBR) {
lineContainer.appendChild(document.createElement("br"));

View file

@ -1,18 +1,45 @@
export function htmlSerialize(model) {
/*
Copyright 2019 New Vector 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.
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 Markdown from '../Markdown';
export function mdSerialize(model) {
return model.parts.reduce((html, part) => {
switch (part.type) {
case "newline":
return html + "<br />";
return html + "\n";
case "plain":
case "pill-candidate":
return html + part.text;
case "room-pill":
case "user-pill":
return html + `<a href="https://matrix.to/#/${part.resourceId}">${part.text}</a>`;
return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`;
}
}, "");
}
export function htmlSerializeIfNeeded(model) {
const md = mdSerialize(model);
const parser = new Markdown(md);
if (!parser.isPlainText()) {
return parser.toHTML();
}
}
export function textSerialize(model) {
return model.parts.reduce((text, part) => {
switch (part.type) {

View file

@ -1821,5 +1821,67 @@
"Want more than a community? <a>Get your own server</a>": "Искате повече от общност? <a>Сдобийте се със собствен сървър</a>",
"You are logged in to another account": "Влезли сте в друг акаунт",
"Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Благодарим, че потвърждавате имейла си! Акаунтът, с които сте влезли тук (%(sessionUserId)s) изглежда е различен от акаунтът за който потвърждавате имейл адреса (%(verifiedUserId)s). Ако искате да влезете в акаунт %(verifiedUserId2)s, моля първо излезте.",
"Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Промяната на паролата Ви, ще анулира всички ключове за шифроване от-край-до-край по всички Ваши устройства, правейки историята на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете от друго устройство преди да промените паролата си."
"Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Промяната на паролата Ви, ще анулира всички ключове за шифроване от-край-до-край по всички Ваши устройства, правейки историята на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете от друго устройство преди да промените паролата си.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Дали използвате 'breadcrumbs' функцията (аватари над списъка със стаи)",
"Replying With Files": "Отговаряне с файлове",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Все още не е възможно да отговорите с файл. Искате ли да качите файла без той да бъде отговор?",
"The file '%(fileName)s' failed to upload.": "Файлът '%(fileName)s' не можа да бъде качен.",
"Room upgrade confirmation": "Потвърждение на обновяването на стаята",
"Upgrading a room can be destructive and isn't always necessary.": "Обновяването на стаята може да бъде деструктивно и не винаги е задължително.",
"Room upgrades are usually recommended when a room version is considered <i>unstable</i>. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Обновяването на стаи обикновено се препоръчва за стаи с версии считащи се за <i>нестабилни</i>. Нестабилните версии може да имат бъгове, липсващи функции или проблеми със сигурността.",
"Room upgrades usually only affect <i>server-side</i> processing of the room. If you're having problems with your Riot client, please file an issue with <issueLink />.": "Обновяванията на стаи обикновено повлияват само <i>сървърната</i> обработка. Ако имате проблем с Riot, моля съобщете за него със <issueLink />.",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Внимание</b>: Обновяването на стаята <i>няма автоматично да прехвърли членовете в новата версия на стаята.</i> Ще изпратим съобщение в старата стая с връзка към новата - членовете на стаята ще трябва да кликнат на връзката за да влязат в новата стая.",
"Upgrade": "Обнови",
"Adds a custom widget by URL to the room": "Добавя собствено приспособление от URL в стаята",
"Please supply a https:// or http:// widget URL": "Моля, укажете https:// или http:// адрес на приспособление",
"You cannot modify widgets in this room.": "Не можете да модифицирате приспособления в тази стая.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s премахна покана към %(targetDisplayName)s за присъединяване в стаята.",
"Show recent room avatars above the room list": "Показвай аватари на скоро-използваните стаи над списъка със стаи",
"Enable desktop notifications for this device": "Включи известия на работния плот за това устройство",
"Enable audible notifications for this device": "Включи звукови уведомления за това устройство",
"Upgrade this room to the recommended room version": "Обнови тази стая до препоръчаната версия на стаята",
"This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Тази стая използва версия на стая <roomVersion />, която сървърът счита за <i>нестабилна</i>.",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Обновяването на тази стая ще изключи текущата стая и ще създаде обновена стая със същото име.",
"Failed to revoke invite": "Неуспешно оттегляне на поканата",
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Поканата не можа да бъде оттеглена. Или има временен проблем със сървъра, или нямате достатъчно права за да оттеглите поканата.",
"Revoke invite": "Оттегли поканата",
"Invited by %(sender)s": "Поканен от %(sender)s",
"Maximize apps": "Максимизирай приложенията",
"Rotate counter-clockwise": "Завърти обратно на часовниковата стрелка",
"Rotate clockwise": "Завърти по часовниковата стрелка",
"GitHub issue": "GitHub проблем",
"Notes": "Бележки",
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Моля включете допълнителни сведения, които ще помогнат за анализиране на проблема, като например: какво правихте когато възникна проблема, идентификатори на стаи, идентификатори на потребители и т.н.",
"Sign out and remove encryption keys?": "Излизане и премахване на ключовете за шифроване?",
"To help us prevent this in future, please <a>send us logs</a>.": "За да ни помогнете да предотвратим това в бъдеще, моля <a>изпратете логове</a>.",
"Missing session data": "Липсват данни за сесията",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Липсват данни за сесията, като например ключове за шифровани съобщения. За да поправите това, излезте и влезте отново, възстановявайки ключовете от резервно копие.",
"Your browser likely removed this data when running low on disk space.": "Най-вероятно браузърът Ви е премахнал тези данни поради липса на дисково пространство.",
"Upload files (%(current)s of %(total)s)": "Качване на файлове (%(current)s от %(total)s)",
"Upload files": "Качи файлове",
"Upload": "Качи",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Файлът е <b>прекалено голям</b> за да се качи. Максималният допустим размер е %(limit)s, докато този файл е %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Тези файлове са <b>прекалено големи</b> за да се качат. Максималният допустим размер е %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Някои файлове са <b>прекалено големи</b> за да се качат. Максималният допустим размер е %(limit)s.",
"Upload %(count)s other files|other": "Качи %(count)s други файла",
"Upload %(count)s other files|one": "Качи %(count)s друг файл",
"Cancel All": "Откажи всички",
"Upload Error": "Грешка при качване",
"A widget would like to verify your identity": "Приспособление иска да потвърди идентичността Ви",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Приспособлението от адрес %(widgetUrl)s иска да потвърди идентичността Ви. Ако позволите това, приспособлението ще може да потвърди потребителския Ви идентификатор, без да може да извършва действия с него.",
"Remember my selection for this widget": "Запомни избора ми за това приспособление",
"Deny": "Откажи",
"Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot не успя да вземе списъка с протоколи от сървъра. Този сървър може да е прекалено стар за да поддържа чужди мрежи.",
"Riot failed to get the public room list.": "Riot не успя да вземе списъка с публични стаи.",
"The homeserver may be unavailable or overloaded.": "Сървърът може да не е наличен или претоварен.",
"You have %(count)s unread notifications in a prior version of this room.|other": "Имате %(count)s непрочетени известия в предишна версия на тази стая.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Имате %(count)s непрочетено известие в предишна версия на тази стая.",
"A conference call could not be started because the integrations server is not available": "Не може да бъде започнат конферентен разговор, защото сървърът за интеграции не е достъпен",
"The server does not support the room version specified.": "Сървърът не поддържа указаната версия на стая.",
"Name or Matrix ID": "Име или Matrix идентификатор",
"Email, name or Matrix ID": "Имейл, име или Matrix идентификатор",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Потвърдете, че искате да обновите стаята от <oldVersion /> до <newVersion />.",
"Changes your avatar in this current room only": "Променя снимката Ви само в тази стая",
"Unbans user with given ID": "Премахва блокирането на потребител с даден идентификатор",
"Sends the given message coloured as a rainbow": "Изпраща текущото съобщение оцветено като дъга"
}

View file

@ -1428,7 +1428,7 @@
"General failure": "Allgemeiner Fehler",
"Failed to perform homeserver discovery": "Fehler beim Aufspüren des Heimservers",
"Unknown failure discovering homeserver": "Unbekannter Fehler beim Aufspüren des Heimservers",
"Great! This passphrase looks strong enough.": "Gut! Diese Passphrase sieht start genug aus.",
"Great! This passphrase looks strong enough.": "Gut! Diese Passphrase sieht stark genug aus.",
"Secure your encrypted message history with a Recovery Passphrase.": "Sichere deine sichere Nachrichtenhistorie mit einer Wiederherstellungspassphrase.",
"If you don't want encrypted message history to be available on other devices, <button>opt out</button>.": "Wenn du deine verschlüsselte Nachrichtenhistorie nicht auf anderen Geräten verfügbar haben möchtest, <button>wiederspreche</button>.",
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Oder, wenn du keine Wiederherstellungspassphrase erzeugen möchtest, überspringe diesen Schritt und <button>lade einen Wiederherstellungsschlüssel herunter</button>.",
@ -1807,5 +1807,6 @@
"Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Danke für das Verifizieren deiner E-Mail! Das Konto, mit dem du angemeldet bist (%(sessionUserId)s) scheint ein anderes zu sein als das wofür die die E-Mail verifizierst (%(verifiedUserId)s). Wenn du dich als %(verifiedUserId2)s anmelden willst, melde dich zuerst ab.",
"Could not load user profile": "Konnte Nutzerprofil nicht laden",
"Your Matrix account": "Dein Matrixkonto",
"Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s"
"Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s",
"Show recent room avatars above the room list": "Zeige die letzten Avatare über der Raumliste an (neu laden um Änderungen zu übernehmen)"
}

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",
@ -249,10 +249,15 @@
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
"%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
"No homeserver URL provided": "No homeserver URL provided",
"Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration",
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
"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?",
@ -334,6 +339,8 @@
"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",
"Low bandwidth mode": "Low bandwidth mode",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@ -800,6 +807,7 @@
"Invites": "Invites",
"Favourites": "Favourites",
"People": "People",
"Start chat": "Start chat",
"Rooms": "Rooms",
"Low priority": "Low priority",
"Historical": "Historical",
@ -914,6 +922,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/>",
@ -923,6 +932,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",
@ -993,7 +1004,9 @@
"Communities": "Communities",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",
"Rotate Left": "Rotate Left",
"Rotate counter-clockwise": "Rotate counter-clockwise",
"Rotate Right": "Rotate Right",
"Rotate clockwise": "Rotate clockwise",
"Download this file": "Download this file",
"Integrations Error": "Integrations Error",
@ -1047,17 +1060,14 @@
"%(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",
"Edit message": "Edit message",
"Power level": "Power level",
"Custom level": "Custom level",
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
"Room directory": "Room directory",
"Start chat": "Start chat",
"And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User",
@ -1318,6 +1328,7 @@
"Code": "Code",
"Submit": "Submit",
"Start authentication": "Start authentication",
"Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
"Your Modular server": "Your Modular server",
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
"Server Name": "Server Name",
@ -1326,12 +1337,11 @@
"The phone number field must not be blank.": "The phone number field must not be blank.",
"The password field must not be blank.": "The password field must not be blank.",
"Email": "Email",
"Username on %(hs)s": "Username on %(hs)s",
"Username": "Username",
"Phone": "Phone",
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
"Sign in to your Matrix account": "Sign in to your Matrix account",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
"Change": "Change",
"Sign in with": "Sign in with",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
@ -1352,8 +1362,8 @@
"Email (optional)": "Email (optional)",
"Confirm": "Confirm",
"Phone (optional)": "Phone (optional)",
"Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
"Use an email address to recover your account.": "Use an email address to recover your account.",
"Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.",
"Other servers": "Other servers",
@ -1420,7 +1430,6 @@
"This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Filter room names": "Filter room names",
"Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
"Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@ -1436,7 +1445,6 @@
"Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
"You are logged in to another account": "You are logged in to another account",
"Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.",
"Unknown error discovering homeserver": "Unknown error discovering homeserver",
"Logout": "Logout",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!",
@ -1475,6 +1483,7 @@
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"Add room": "Add room",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed",
@ -1495,6 +1504,7 @@
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",
"Guest": "Guest",
"Your profile": "Your profile",
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
@ -1504,9 +1514,8 @@
"A new password must be entered.": "A new password must be entered.",
"New passwords must match each other.": "New passwords must match each other.",
"Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.",
"Your Matrix account": "Your Matrix account",
"Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.",
"Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
"Send Reset Email": "Send Reset Email",
"Sign in instead": "Sign in instead",
@ -1530,7 +1539,6 @@
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
"The phone number entered looks invalid": "The phone number entered looks invalid",
"Unknown failure discovering homeserver": "Unknown failure discovering homeserver",
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",

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

@ -1392,5 +1392,112 @@
"Okay": "Bone",
"Success!": "Sukceso!",
"Retry": "Reprovi",
"Set up": "Agordi"
"Set up": "Agordi",
"A conference call could not be started because the integrations server is not available": "Grupa voko ne povis komenciĝi, ĉar la kuniga servilo estas neatingebla",
"Replying With Files": "Respondado kun dosieroj",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nun ne eblas respondi kun dosiero. Ĉu vi volas alŝuti la dosieron sen respondo?",
"The file '%(fileName)s' failed to upload.": "Malsukcesis alŝuti dosieron «%(fileName)s».",
"The server does not support the room version specified.": "La servilo ne subtenas la donitan ĉambran version.",
"Name or Matrix ID": "Nomo aŭ Matrix-identigilo",
"Email, name or Matrix ID": "Retpoŝtadreso, nomo, aŭ Matrix-identigilo",
"Upgrades a room to a new version": "Gradaltigas ĉambron al nova versio",
"Room upgrade confirmation": "Konfirmo de ĉambra gradaltigo",
"Upgrading a room can be destructive and isn't always necessary.": "Gradaltigo de ĉambro povas esti detrua kaj ne estas ĉiam necesa.",
"Room upgrades are usually recommended when a room version is considered <i>unstable</i>. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Gradaltigoj de ĉambroj estas kutime rekomendataj kiam ĉambra versio estas opiniata <i>malstabila</i>. Malstabilaj ĉambraj versioj povas kunhavi erarojn, mankojn de funkcioj, aŭ malsekuraĵojn.",
"Room upgrades usually only affect <i>server-side</i> processing of the room. If you're having problems with your Riot client, please file an issue with <issueLink />.": "Ĉambraj gradaltigoj efikas nur sur <i>servil-flanka</i> funkciado de la ĉambro. Se vi havas problemon kun via kliento (Riot), bonvolu raparti problemon per <issueLink />.",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Averto</b>: Gradaltigo de ĉambro <i>ne transmetos ĉiujn ĉambranojn al la nova versio de la ĉambro.</i> Ni afiŝos ligilon al la nova ĉambro en la malnova versio de la ĉambro ĉambranoj devos tien klaki por aliĝi al la nova ĉambro.",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Bonvolu konfirmi, ke vi certe volas gradaltigi ĉi tiun ĉambron de <oldVersion /> al <newVersion />.",
"Upgrade": "Gradaltigi",
"Changes your display nickname in the current room only": "Ŝanĝas vian vidigan nomon nur en la nuna ĉambro",
"Changes your avatar in this current room only": "Ŝanĝas vian profilbildon nur en la nuna ĉambro",
"Gets or sets the room topic": "Ekhavas aŭ agordas la temon de la ĉambro",
"This room has no topic.": "Ĉi tiu ĉambro ne havas temon.",
"Sets the room name": "Agordas nomon de la ĉambro",
"Sends the given message coloured as a rainbow": "Sendas la mesaĝon ĉielarke kolorigitan",
"Sends the given emote coloured as a rainbow": "Sendas la mienon ĉielarke kolorigitan",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s gradaltigis ĉi tiun ĉambron.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s publikigis la ĉambron al kiu ajn konas la ligilon.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s necesigis invitojn por aliĝoj al la ĉambro.",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s ŝanĝis la aliĝan regulon al %(rule)s",
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s permesis al gastoj aliĝi al la ĉambro.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s malpermesis al gastoj aliĝi al la ĉambro.",
"Unbans user with given ID": "Malforbaras uzanton kun la donita identigilo",
"Please supply a https:// or http:// widget URL": "Bonvolu doni URL-on de fenestraĵo kun https:// aŭ http://",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s ŝanĝis aliron de gastoj al %(rule)s",
"%(displayName)s is typing …": "%(displayName)s tajpas…",
"%(names)s and %(count)s others are typing …|other": "%(names)s kaj %(count)s aliaj tajpas…",
"%(names)s and %(count)s others are typing …|one": "%(names)s kaj unu alia tajpas…",
"%(names)s and %(lastPerson)s are typing …": "%(names)s kaj %(lastPerson)s tajpas…",
"Unrecognised address": "Nerekonita adreso",
"User %(userId)s is already in the room": "Uzanto %(userId)s jam enas la ĉambron",
"User %(user_id)s may or may not exist": "Uzanto %(user_id)s eble ne ekzistas",
"The user must be unbanned before they can be invited.": "Necesas malforbari ĉi tiun uzanton antaŭ ol ĝin inviti.",
"The user's homeserver does not support the version of the room.": "Hejmservilo de ĉi tiu uzanto ne subtenas la version de la ĉambro.",
"No need for symbols, digits, or uppercase letters": "Ne necesas simboloj, cirefoj, aŭ majuskloj",
"Render simple counters in room header": "Bildigi simplajn kalkulilojn en la ĉapo de la fenestro",
"Edit messages after they have been sent (refresh to apply changes)": "Redakti mesaĝojn senditajn (aktualigu por apliki ŝanĝojn)",
"React to messages with emoji (refresh to apply changes)": "Reagi al mesaĝoj per bildsignoj (aktualigu por apliki ŝanĝojn)",
"Enable Emoji suggestions while typing": "Ŝalti proponojn de bildsignoj dum tajpado",
"Show a placeholder for removed messages": "Meti kovrilon anstataŭ forigitajn mesaĝojn",
"Show join/leave messages (invites/kicks/bans unaffected)": "Montri mesaĝojn pri aliĝo/foriro (neteme pri invitoj/forpeloj/forbaroj)",
"Show avatar changes": "Montri ŝanĝojn de profilbildoj",
"Show display name changes": "Montri ŝanĝojn de vidigaj nomoj",
"Show read receipts sent by other users": "Montri legokonfirmojn senditajn de aliaj uzantoj",
"Show avatars in user and room mentions": "Montri profilbildojn en mencioj de uzantoj kaj ĉambroj",
"Enable big emoji in chat": "Ŝalti grandajn bildsignojn en babilejo",
"Send typing notifications": "Sendi sciigojn pri tajpado",
"Allow Peer-to-Peer for 1:1 calls": "Permesi samtavolan teĥnikon por duopaj vokoj",
"Prompt before sending invites to potentially invalid matrix IDs": "Averti antaŭ ol sendi invitojn al eble nevalidaj Matrix-identigiloj",
"Order rooms in the room list by most important first instead of most recent": "Ordigi ĉambrojn en listo de ĉambroj laŭ graveco anstataŭ freŝeco",
"Messages containing my username": "Mesaĝoj kun mia salutnomo",
"When rooms are upgraded": "Kiam ĉambroj gradaltiĝas",
"The other party cancelled the verification.": "La alia kontrolano nuligis la kontrolon.",
"You've successfully verified this user.": "Vi sukcese kontrolis ĉi tiun uzanton.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sekuraj mesaĝoj kun ĉi tiu uzanto estas tutvoje ĉirfitaj kaj nelegeblaj al ceteruloj.",
"Verify this user by confirming the following emoji appear on their screen.": "Kontrolu ĉi tiun uzanton per konfirmo, ke la jenaj bildsignoj aperis sur ĝia ekrano.",
"Verify this user by confirming the following number appears on their screen.": "Kontrolu ĉu tiun uzanton per konfirmo, ke la jena numero aperis sur ĝia ekrano.",
"Unable to find a supported verification method.": "Ne povas trovi subtenatan metodon de kontrolo.",
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "Por la plej bona sekureco, ni rekomendas fari ĉi tion persone, aŭ per alia, fidata komunikilo.",
"Santa": "Kristnaska viro",
"Thumbs up": "Dikfingro supren",
"Paperclip": "Paperkuntenilo",
"Pin": "Pinglo",
"Your homeserver does not support device management.": "Via hejmservilo ne subtenas administradon de aparatoj.",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Ni sendis al vi retleteron por konfirmi vian adreson. Bonvolu sekvi la tieajn intrukciojn kaj poste klaki al la butono sube.",
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Ĉu vi certas? Vi perdos ĉiujn viajn ĉifritajn mesaĝojn, se viaj ŝlosiloj ne estas savkopiitaj.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Ĉifritaj mesaĝoj estas sekurigitaj per tutvoja ĉifrado. Nur vi kaj la ricevonto(j) havas la ŝlosilojn necesajn por legado.",
"This device is backing up your keys. ": "Ĉi tiu aparato savkopias viajn ŝlosilojn. ",
"Custom user status messages": "Propraj uzantoaj statmesaĝoj",
"Group & filter rooms by custom tags (refresh to apply changes)": "Grupigi kaj filtri ĉambrojn per propraj etikedoj (aktualigu por ŝanĝojn apliki)",
"Restore from Backup": "Rehavi el savkopio",
"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.": "Tiu ĉi aparato ne <b>ne savkopias viajn ŝlosilojn</b>, sed vi jam havas savkopion, kiun vi povas restarigi, kaj aldonadi al ĝi de nun.",
"Backing up %(sessionsRemaining)s keys...": "Savkopianta %(sessionsRemaining)s ŝlosilojn…",
"All keys backed up": "Ĉiuj ŝlosiloj estas savkopiitaj",
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.": "Savkopio havas subskribon de <verify>nekonata</verify> aparato kun la identigilo %(deviceId)s.",
"Backup has a <validity>valid</validity> signature from this device": "Savkopio havas <validity>validan</validity> subskribon de ĉi tiu aparato",
"Backup has an <validity>invalid</validity> signature from this device": "Savkopio havas <validity>nevalidan</validity> subskribon de tiu ĉi aparato",
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>": "Savkopio havas <validity>validan</validity> subskribon de <verify>kontrolita</verify> aparato <device></device>",
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>": "Savkopio havas <validity>validan</validity> subskribon de <verify>nekontrolita</verify> aparato <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>verified</verify> device <device></device>": "Savkopio havas <validity>nevalidan</validity> subskribon de <verify>kontrolita</verify> aparato <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>": "Savkopio havas <validity>nevalidan</validity> subskribon de <verify>nekontrolita</verify> aparato <device></device>",
"Backup is not signed by any of your devices": "Savkopio estas subskribita de neniu el viaj aparatoj",
"This backup is trusted because it has been restored on this device": "Ĉi tiu savkopio estas fidata ĉar ĝi estis restarigita por ĉi tiu aparato",
"Backup version: ": "Versio de savkopio: ",
"Algorithm: ": "Algoritmo: ",
"Your keys are <b>not being backed up from this device</b>.": "Viaj ŝlosiloj <b>ne estas savkopiataj de ĉi tiu aparato</b>.",
"Back up your keys before signing out to avoid losing them.": "Savkopiu viajn ŝlosilojn antaŭ adiaŭo, por ilin ne perdi.",
"Add an email address to configure email notifications": "Aldonu retpoŝtadreson por agordi retpoŝtajn sciigojn",
"Enable desktop notifications for this device": "Ŝalti labortablajn sciigojn por ĉi tiu aparato",
"Enable audible notifications for this device": "Ŝalti sonajn sciigojn por ĉi tiu aparato",
"Unable to verify phone number.": "Ne povas kontroli telefonnumeron.",
"Verification code": "Kontrola kodo",
"Deactivating your account is a permanent action - be careful!": "Malaktivigo de via konto estas nemalfarebla atentu!",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se vi raportis eraron per GitHub, senerariga protokolo povas helpi al ni spuri la problemon. Senerariga protokolo povas enhavi datumojn pri uzo de la aplikaĵo, inkluzive vian salutnomon, identigilojn aŭ nomojn de la vizititaj ĉambroj aŭ grupoj, kaj salutnomojn de aliaj uzantoj. Ili ne enhavas mesaĝojn.",
"Close button should minimize window to tray": "Ferma butono devas nur plejetigi la fenestron",
"Accept all %(invitedRooms)s invites": "Akcepti ĉiujn %(invitedRooms)s invitojn",
"Missing media permissions, click the button below to request.": "Mankas aŭdovidaj permesoj; klaku al la suba butono por peti.",
"Request media permissions": "Peti aŭdovidajn permesojn",
"Audio Output": "Sona eligo",
"this room": "ĉi tiu ĉambro",
"View older messages in %(roomName)s.": "Montri pli malnovajn mesaĝojn en %(roomName)s."
}

View file

@ -603,7 +603,7 @@
"Waiting for response from server": "Esperando una respuesta del servidor",
"Leave": "Salir",
"Uploaded on %(date)s by %(user)s": "Subido el %(date)s por %(user)s",
"Send Custom Event": "Enviar Evento Personalizado",
"Send Custom Event": "Enviar evento personalizado",
"All notifications are currently disabled for all targets.": "Las notificaciones están deshabilitadas para todos los objetivos.",
"Failed to send logs: ": "Error al enviar registros: ",
"delete the alias.": "eliminar el alias.",
@ -669,7 +669,7 @@
"All messages (noisy)": "Todos los mensajes (ruidoso)",
"Enable them now": "Habilitarlos ahora",
"Messages containing my user name": "Mensajes que contienen mi nombre de usuario",
"Toolbox": "Caja de Herramientas",
"Toolbox": "Caja de herramientas",
"Collecting logs": "Recolectando registros",
"more": "más",
"GitHub issue link:": "Enlace de incidencia en GitHub:",
@ -728,7 +728,7 @@
"Failed to change settings": "Error al cambiar los ajustes",
"View Community": "Ver la comunidad",
"%(count)s Members|one": "%(count)s miembro",
"Developer Tools": "Herramientas de Desarrollo",
"Developer Tools": "Herramientas de desarrollo",
"View Source": "Ver Fuente",
"Event Content": "Contenido del Evento",
"Unable to fetch notification target list": "No se puede obtener la lista de objetivos de notificación",
@ -1285,7 +1285,7 @@
"Language and region": "Idioma y región",
"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.": "También puedes definir un servidor de identidad personalizado, pero no podrás invitar a usuarios o ser inivitado usando direcciones de correo.",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fichero %(fileName)s supera el tamaño límite del servidor para subidas",
"Unable to load! Check your network connectivity and try again.": "No es posible cargar! Comprueba tu conexión de red e inténtalo de nuevo.",
"Unable to load! Check your network connectivity and try again.": "¡No es posible cargar! Comprueba tu conexión de red e inténtalo de nuevo.",
"Failed to invite users to the room:": "Fallo al invitar usuarios a la sala:",
"Upgrades a room to a new version": "Actualiza una sala a una nueva versión",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s actualizó esta sala.",

View file

@ -1824,5 +1824,6 @@
"Riot failed to get the public room list.": "Riot-ek ezin izan du du gelen zerrenda publikoa eskuratu.",
"The homeserver may be unavailable or overloaded.": "Hasiera-zerbitzaria eskuraezin edo kargatuegia egon daiteke.",
"You have %(count)s unread notifications in a prior version of this room.|other": "Irakurri gabeko %(count)s jakinarazpen dituzu gela honen aurreko bertsio batean.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Irakurri gabeko %(count)s jakinarazpen duzu gela honen aurreko bertsio batean."
"You have %(count)s unread notifications in a prior version of this room.|one": "Irakurri gabeko %(count)s jakinarazpen duzu gela honen aurreko bertsio batean.",
"Replying With Files": "Fitxategiekin erantzutea"
}

View file

@ -37,7 +37,7 @@
"Default Device": "Oletuslaite",
"Microphone": "Mikrofoni",
"Camera": "Kamera",
"Advanced": "Kehittyneet",
"Advanced": "Edistynyt",
"Algorithm": "Algoritmi",
"Hide removed messages": "Piilota poistetut viestit",
"Always show message timestamps": "Näytä aina viestien aikaleimat",
@ -70,7 +70,7 @@
"and %(count)s others...|one": "ja yksi muu...",
"Ban": "Anna porttikielto",
"Banned users": "Porttikiellon saanneet käyttäjät",
"Bans user with given id": "Antaa porttikiellon käyttäjälle jolla on annettu tunniste",
"Bans user with given id": "Antaa porttikiellon tunnuksen mukaiselle käyttäjälle",
"Bulk Options": "Bulkkiasetukset",
"Changes your display nickname": "Muuttaa näyttönimesi",
"Changes colour scheme of current room": "Muuttaa tamänhetkisen huoneen väritystä",
@ -175,7 +175,7 @@
"Incoming call from %(name)s": "Saapuva puhelu käyttäjältä %(name)s",
"Incoming video call from %(name)s": "Saapuva videopuhelu käyttäjältä %(name)s",
"Incoming voice call from %(name)s": "Saapuva äänipuhelu käyttäjältä %(name)s",
"Incorrect username and/or password.": "Virheellinen käyttäjänimi ja/tai salasana.",
"Incorrect username and/or password.": "Virheellinen käyttäjätunnus ja/tai salasana.",
"Incorrect verification code": "Virheellinen varmennuskoodi",
"Integrations Error": "Integraatiovirhe",
"Interface Language": "Käyttöliittymän kieli",
@ -185,13 +185,13 @@
"Invite new room members": "Kutsu lisää jäseniä huoneeseen",
"Invited": "Kutsuttu",
"Invites": "Kutsut",
"Invites user with given id to current room": "Kutsuu annetun käyttäjätunnisteen mukaisen käyttäjän huoneeseen",
"Invites user with given id to current room": "Kutsuu tunnuksen mukaisen käyttäjän huoneeseen",
"Sign in with": "Tunnistus",
"Join Room": "Liity huoneeseen",
"Joins room with given alias": "Liittyy huoneeseen jolla on annettu alias",
"Jump to first unread message.": "Hyppää ensimmäiseen lukemattomaan viestiin.",
"Kick": "Poista huoneesta",
"Kicks user with given id": "Poistaa käyttäjätunnisteen mukaisen käyttäjän huoneesta",
"Kicks user with given id": "Poistaa tunnuksen mukaisen käyttäjän huoneesta",
"Labs": "Laboratorio",
"Last seen": "Viimeksi nähty",
"Leave room": "Poistu huoneesta",
@ -217,8 +217,8 @@
"not specified": "ei määritetty",
"(not supported by this browser)": "(ei tuettu tässä selaimessa)",
"<not supported>": "<ei tuettu>",
"AM": "AM",
"PM": "PM",
"AM": "ap.",
"PM": "ip.",
"NOT verified": "EI varmennettu",
"NOTE: Apps are not end-to-end encrypted": "Huom: Ohjelmat eivät käytä osapuolten välistä salausta",
"No display name": "Ei näyttönimeä",
@ -328,7 +328,7 @@
"Turn Markdown off": "Ota Markdown pois käytöstä",
"Turn Markdown on": "Ota Markdown käyttöön",
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s otti osapuolten välisen salauksen käyttöön (algoritmi %(algorithm)s).",
"Username invalid: %(errMessage)s": "Virheellinen käyttäjänimi: %(errMessage)s",
"Username invalid: %(errMessage)s": "Käyttäjätunnus ei kelpaa: %(errMessage)s",
"Users": "Käyttäjät",
"Verification": "Varmennus",
"verified": "varmennettu",
@ -370,13 +370,13 @@
"Your password has been reset": "Salasanasi on palautettu",
"You should not yet trust it to secure data": "Sinun ei vielä kannata luottaa siihen turvataksesi dataa",
"Your home server does not support device management.": "Kotipalvelimesi ei tue laitteiden hallintaa.",
"Sun": "Su",
"Mon": "Ma",
"Tue": "Ti",
"Wed": "Ke",
"Thu": "To",
"Fri": "Pe",
"Sat": "La",
"Sun": "su",
"Mon": "ma",
"Tue": "ti",
"Wed": "ke",
"Thu": "to",
"Fri": "pe",
"Sat": "la",
"Set a display name:": "Aseta näyttönimi:",
"This server does not support authentication with a phone number.": "Tämä palvelin ei tue autentikointia puhelinnumeron avulla.",
"Missing password.": "Salasana puuttuu.",
@ -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",
@ -502,7 +502,7 @@
"Usage": "Käyttö",
"Use compact timeline layout": "Käytä tiivistä aikajanaa",
"Use with caution": "Käytä varoen",
"User ID": "Käyttäjätunniste",
"User ID": "Käyttäjätunnus",
"User Interface": "Käyttöliittymä",
"User name": "Käyttäjänimi",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s asetti aiheeksi \"%(topic)s\".",
@ -520,7 +520,7 @@
"Server unavailable, overloaded, or something else went wrong.": "Palvelin on saavuttamattomissa, ylikuormitettu tai jotain muuta meni vikaan.",
"The email address linked to your account must be entered.": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.",
"The visibility of existing history will be unchanged": "Olemassaolevan viestihistorian näkyvyys ei muutu",
"To get started, please pick a username!": "Valitse käyttäjänimi aloittaaksesi!",
"To get started, please pick a username!": "Aloita valitsemalla käyttäjätunnus!",
"To use it, just wait for autocomplete results to load and tab through them.": "Käyttääksesi sitä odota vain automaattitäydennyksiä ja selaa niiden läpi.",
"To reset your password, enter the email address linked to your account": "Syötä tiliisi liitetty sähköpostiosoite uudelleenalustaaksesi salasanasi",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Aikajanan tietty hetki yritettiin ladata, mutta sinulla ei ole oikeutta nähdä kyseistä viestiä.",
@ -538,18 +538,18 @@
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Salasanan muuttaminen onnistui. Et saa push-ilmoituksia muilla laitteilla ennen kuin kirjaudut niihin sisään",
"You seem to be in a call, are you sure you want to quit?": "Sinulla näyttää olevan puhelu kesken. Haluatko varmasti lopettaa?",
"You seem to be uploading files, are you sure you want to quit?": "Näytät lataavan tiedostoja. Oletko varma että haluat lopettaa?",
"Jan": "tammikuu",
"Feb": "helmikuu",
"Mar": "maaliskuu",
"Apr": "huhtikuu",
"May": "toukokuu",
"Jun": "kesäkuu",
"Jul": "heinäkuu",
"Aug": "elokuu",
"Sep": "syyskuu",
"Oct": "lokakuu",
"Nov": "marraskuu",
"Dec": "joulukuu",
"Jan": "tammi",
"Feb": "helmi",
"Mar": "maalis",
"Apr": "huhti",
"May": "touko",
"Jun": "kesä",
"Jul": "heinä",
"Aug": "elo",
"Sep": "syys",
"Oct": "loka",
"Nov": "marras",
"Dec": "joulu",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "Käyttäjänimet voivat sisältää vain kirjaimia, numeroita, pisteitä, viivoja ja alaviivoja.",
"To continue, please enter your password.": "Ole hyvä ja syötä salasanasi jatkaaksesi.",
"Verifies a user, device, and pubkey tuple": "Varmentaa käyttäjän, laitteen ja julkisen avaimen kolmikon",
@ -580,8 +580,8 @@
"Start chatting": "Aloita keskustelu",
"Start Chatting": "Aloita keskustelu",
"Click on the button below to start chatting!": "Paina nappia alla aloittaaksesi keskustelu!",
"Username available": "Käyttäjänimi saatavissa",
"Username not available": "Käyttäjänimi ei ole saatavissa",
"Username available": "Käyttäjätunnus saatavilla",
"Username not available": "Käyttäjätunnus ei ole saatavissa",
"Something went wrong!": "Jokin meni vikaan!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Tästä tulee tilisi nimi <span></span> -kotipalvelimella, tai voit valita <a>toisen palvelimen</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Jos sinulla on jo Matrix-tili voit <a>kirjautua</a>.",
@ -715,7 +715,7 @@
"An email has been sent to %(emailAddress)s": "Sähköpostia lähetetty osoitteeseen %(emailAddress)s",
"Please check your email to continue registration.": "Ole hyvä ja tarkista sähköpostisi jatkaaksesi.",
"A text message has been sent to %(msisdn)s": "Tekstiviesti lähetetty numeroon %(msisdn)s",
"Username on %(hs)s": "Käyttäjänimi palvelimella %(hs)s",
"Username on %(hs)s": "Käyttäjätunnus palvelimella %(hs)s",
"Custom server": "Muu palvelin",
"Remove from community": "Poista yhteisöstä",
"Disinvite this user from community?": "Peruuta tämän käyttäjän kutsu yhteisöön?",
@ -736,8 +736,8 @@
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"were unbanned %(count)s times|other": "porttikiellot poistettiin %(count)s kertaa",
"And %(count)s more...|other": "Ja %(count)s lisää...",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix huonetunniste",
"Matrix ID": "Matrix-tunnus",
"Matrix Room ID": "Matrix-huonetunnus",
"email address": "sähköpostiosoite",
"Try using one of the following valid address types: %(validTypesList)s.": "Kokeile käyttää yhtä näistä kelvollisista osoitetyypeistä: %(validTypesList)s.",
"You have entered an invalid address.": "Olet syöttänyt virheellisen sähköpostiosoitteen.",
@ -796,7 +796,7 @@
"Please note you are logging into the %(hs)s server, not matrix.org.": "Huomaa että olet kirjautumassa palvelimelle %(hs)s, etkä palvelimelle matrix.org.",
"Sign in to get started": "Kirjaudu aloittaksesi",
"Upload an avatar:": "Lataa profiilikuva:",
"Deops user with given id": "Poistaa annetun tunnisteen omaavalta käyttäjältä ylläpito-oikeudet",
"Deops user with given id": "Poistaa tunnuksen mukaiselta käyttäjältä ylläpito-oikeudet",
"Ignores a user, hiding their messages from you": "Jättää käyttäjän huomioimatta, jotta hänen viestejään ei näytetä sinulle",
"Stops ignoring a user, showing their messages going forward": "Lopettaa käyttäjän huomiotta jättämisen, jotta hänen viestinsä näytetään sinulle",
"Notify the whole room": "Ilmoita koko huoneelle",
@ -810,8 +810,8 @@
"Answer": "Vastaa",
"Call Timeout": "Puhelun aikakatkaisu",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s %(time)s",
"Ignored user": "Estetyt käyttäjät",
"Unignored user": "Sallitut käyttäjät",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s",
@ -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",
@ -1345,12 +1345,12 @@
"We encountered an error trying to restore your previous session.": "Törmäsimme ongelmaan yrittäessämme palauttaa edellistä istuntoasi.",
"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 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ä, 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ä",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kohdissa, joissa tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän tunnuksen, kyseinen tieto poistetaan ennen palvelimelle lähettämistä.",
"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",
@ -1406,7 +1406,7 @@
"Enable Community Filter Panel": "Ota käyttöön yhteisön suodatinpaneeli",
"Allow Peer-to-Peer for 1:1 calls": "Salli vertaisten väliset yhteydet kahdenkeskisissä puheluissa",
"Prompt before sending invites to potentially invalid matrix IDs": "Kysy varmistus ennen kutsujen lähettämistä mahdollisesti epäkelpoihin Matrix ID:hin",
"Messages containing my username": "Viestit, jotka sisältävät käyttäjänimeni",
"Messages containing my username": "Viestit, jotka sisältävät käyttäjätunnukseni",
"Messages containing @room": "Viestit, jotka sisältävät sanan ”@room”",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Turvalliset viestit tämän käyttäjän kanssa ovat salattuja päästä päähän, eivätkä kolmannet osapuolet voi lukea niitä.",
"Thumbs up": "Peukut ylös",
@ -1438,7 +1438,7 @@
"For help with using Riot, click <a>here</a>.": "Saadaksesi apua Riotin käyttämisessä, klikkaa <a>tästä</a>.",
"For help with using Riot, click <a>here</a> or start a chat with our bot using the button below.": "Saadaksesi apua Riotin käytössä, klikkaa <a>tästä</a> tai aloita keskustelu bottimme kanssa alla olevasta painikkeesta.",
"Bug reporting": "Virheiden raportointi",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät ohjelman käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien ID:t tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät sovelluksen käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien tunnukset tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.",
"Autocomplete delay (ms)": "Automaattisen täydennyksen viive (ms)",
"To link to this room, please add an alias.": "Lisää alias linkittääksesi tähän huoneeseen.",
"Ignored users": "Hiljennetyt käyttäjät",
@ -1465,7 +1465,7 @@
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "Parhaan turvallisuuden takaamiseksi suosittelemme, että teet tämän kasvotusten tai muun luotetun viestintäkeinon avulla.",
"Scissors": "Sakset",
"Which officially provided instance you are using, if any": "Mitä virallisesti saatavilla olevaa instanssia käytät, jos mitään",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s",
"Missing roomId.": "roomId puuttuu.",
"Forces the current outbound group session in an encrypted room to be discarded": "Pakottaa hylkäämään nykyisen ulospäin suuntautuvan ryhmäistunnon salatussa huoneessa",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s otti käyttöön tyylin ryhmille %(groups)s tässä huoneessa.",
@ -1558,7 +1558,7 @@
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Tapahtuman, johon oli vastattu, lataaminen epäonnistui. Se joko ei ole olemassa tai sinulla ei ole oikeutta katsoa sitä.",
"The following users may not exist": "Seuraavat käyttäjät eivät välttämättä ole olemassa",
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Alla luetelluille Matrix ID:ille ei löytynyt profiileja. Haluaisitko kutsua ne siitä huolimatta?",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät ohjelman käyttödataa, kuten käyttäjätunnuksesi, huoneiden ja ryhmien ID:t tai aliakset, joissa olet vieraillut sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät sovelluksen käyttödataa, kuten käyttäjätunnuksesi, vierailemiesi huoneiden ja ryhmien tunnukset tai aliakset, sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Ennen lokien lähettämistä sinun täytyy <a>luoda Githubiin issue (kysymys/ongelma)</a>, joka sisältää kuvauksen ongelmastasi.",
"What GitHub issue are these logs for?": "Mihin Github-issueen nämä lokit liittyvät?",
"Notes:": "Huomiot:",
@ -1566,7 +1566,7 @@
"Community IDs cannot be empty.": "Yhteisön ID:t eivät voi olla tyhjänä.",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Jotta et menetä keskusteluhistoriaasi, sinun täytyy tallentaa huoneen avaimet ennen kuin kirjaudut ulos. Joudut käyttämään uudempaa Riotin versiota tätä varten",
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Olet aikaisemmin käyttänyt uudempaa Riotin versiota koneella %(host)s. Jotta voit käyttää tätä versiota osapuolten välisellä salauksella, sinun täytyy kirjautua ulos ja kirjautua takaisin sisään. ",
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä tunnusta samalla käyttäjä-ID:llä. Tunnuksesi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimelta. <b>Tämä toimenpide on lopullinen eikä sitä voi kumota.</b>",
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä samaa käyttäjätunnusta. Tilisi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimeltasi. <b>Tämä toimenpidettä ei voi kumota.</b>",
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Tilisi poistaminen käytöstä <b>ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä.</b> Jos haluaisit meidän unohtavan viestisi, rastita alla oleva ruutu.",
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Viestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Vaikka se, että unohdamme viestisi, tarkoittaa, ettei viestejäsi jaeta enää uusille tai rekisteröitymättömille käyttäjille, käyttäjät, jotka ovat jo saaneet viestisi pystyvät lukemaan jatkossakin omaa kopiotaan viesteistäsi.",
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on poistettu käytöstä (b>Varoitus:</b> tästä seuraa, että tulevat käyttäjät näkevät epätäydellisen version keskusteluista)",
@ -1627,7 +1627,7 @@
"Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Käytä sähköpostia tilisi palauttamiseen. Muut käyttäjät voivat kutsua sinut huoneisiin yhteystiedoillasi.",
"Other servers": "Muut palvelimet",
"Enter custom server URLs <a>What does this mean?</a>": "Syötä mukautettujen palvelinten osoitteet. <a>Mitä tämä tarkoittaa?</a>",
"Free": "Vapaa",
"Free": "Ilmainen",
"Join millions for free on the largest public server": "Liity ilmaiseksi miljoonien joukkoon suurimmalla julkisella palvelimella",
"Premium": "Premium",
"Premium hosting for organisations <a>Learn more</a>": "Premium-ylläpitoa organisaatioille. <a>Lue lisää</a>",
@ -1713,7 +1713,7 @@
"Send debug logs and reload Riot": "Lähetä debug-lokit ja päivitä Riot",
"Reload Riot without sending logs": "Päivitä Riot lähettämättä lokeja",
"A widget would like to verify your identity": "Sovelma haluaisi vahvistaa identiteettisi",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi vahvistaa identiteettisi. Jos sallit tämän, sovelma pystyy vahvistamaan käyttäjä-ID:si, mutta ei voi toimia nimelläsi.",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi todentaa henkilöllisyytesi. Jos sallit tämän, sovelma pystyy todentamaan käyttäjätunnuksesi, muttei voi toimia nimissäsi.",
"Remember my selection for this widget": "Muista valintani tälle sovelmalle",
"Deny": "Kiellä",
"Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Tunnistimme dataa, joka on lähtöisin vanhasta Riotin versiosta. Tämä aiheuttaa toimintahäiriöitä osapuolten välisessä salauksessa vanhassa versiossa. Viestejä, jotka ovat salattu osapuolten välisellä salauksella, ei välttämättä voida purkaa tällä versiolla. Tämä voi myös aiheuttaa epäonnistumisia viestien välityksessä tämän version kanssa. Jos kohtaat ongelmia, kirjaudu ulos ja takaisin sisään. Säilyttääksesi viestihistoriasi, tallenna salausavaimesi ja tuo ne takaisin kirjauduttuasi takaisin sisälle.",
@ -1760,5 +1760,95 @@
"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ätunnus",
"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",
"A conference call could not be started because the integrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä",
"When rooms are upgraded": "Kun huoneet päivitetään",
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Yhdistä tämä laite avainten varmuuskopiointiin ennen kuin kirjaudut ulos, jotta et menetä mahdollisia vain tällä laitteella olevia avaimia.",
"Rejecting invite …": "Hylätään kutsua …",
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s poisti sinut huoneesta %(roomName)s",
"Edited at %(date)s.": "Muokattu %(date)s.",
"edited": "muokattu",
"To help us prevent this in future, please <a>send us logs</a>.": "Voit auttaa meitä estämään tämän toistumisen <a>lähettämällä meille lokeja</a>.",
"Name or Matrix ID": "Nimi tai Matrix-tunnus",
"Email, name or Matrix ID": "Sähköposti, nimi tai Matrix-tunnus",
"Edited at %(date)s": "Muokattu %(date)s",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Tiedosto on <b>liian iso</b> ladattavaksi. Tiedostojen kokoraja on %(limit)s mutta tämä tiedosto on %(sizeOfThisFile)s.",
"Unbans user with given ID": "Poistaa porttikiellon tunnuksen mukaiselta käyttäjältä"
}

View file

@ -198,7 +198,7 @@
"No devices with registered encryption keys": "Pas dappareil avec des clés de chiffrement enregistrées",
"No more results": "Fin des résultats",
"No results": "Pas de résultat",
"unknown error code": "Code d'erreur inconnu",
"unknown error code": "code derreur inconnu",
"OK": "OK",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Une fois le chiffrement activé dans un salon il ne peut pas être désactivé (pour le moment)",
"Only people who have been invited": "Seules les personnes ayant été invitées",
@ -313,7 +313,7 @@
"Unknown room %(roomId)s": "Salon inconnu %(roomId)s",
"Unmute": "Activer le son",
"Upload avatar": "Télécharger une photo de profil",
"Upload Failed": "Erreur lors de l'envoi",
"Upload Failed": "Échec de lenvoi",
"Upload Files": "Télécharger les fichiers",
"Upload file": "Envoyer un fichier",
"Usage": "Utilisation",
@ -422,9 +422,9 @@
"You must join the room to see its files": "Vous devez rejoindre le salon pour voir ses fichiers",
"Reject all %(invitedRooms)s invites": "Rejeter la totalité des %(invitedRooms)s invitations",
"Start new chat": "Démarrer une nouvelle discussion",
"Failed to invite": "Echec de l'invitation",
"Failed to invite user": "Echec lors de l'invitation de l'utilisateur",
"Failed to invite the following users to the %(roomName)s room:": "Echec lors de linvitation des utilisateurs suivants dans le salon %(roomName)s :",
"Failed to invite": "Échec de linvitation",
"Failed to invite user": "Échec lors de l'invitation de l'utilisateur",
"Failed to invite the following users to the %(roomName)s room:": "Échec de linvitation des utilisateurs suivants dans le salon %(roomName)s :",
"Confirm Removal": "Confirmer la suppression",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Voulez-vous vraiment supprimer cet événement ? Notez que si vous supprimez le changement du nom ou du sujet dun salon, il est possible que ce changement soit annulé.",
"Unknown error": "Erreur inconnue",
@ -641,7 +641,7 @@
"Revoke widget access": "Révoquer les accès du widget",
"Sets the room topic": "Défini le sujet du salon",
"To get started, please pick a username!": "Pour commencer, choisissez un nom d'utilisateur !",
"Unable to create widget.": "Impossible de créer un widget.",
"Unable to create widget.": "Impossible de créer le widget.",
"Unbans user with given id": "Révoque le bannissement de l'utilisateur à partir de son identifiant",
"You are not in this room.": "Vous n'êtes pas dans ce salon.",
"You do not have permission to do that in this room.": "Vous n'avez pas la permission d'effectuer cette action dans ce salon.",
@ -716,7 +716,7 @@
"%(names)s and %(count)s others are typing|other": "%(names)s et %(count)s autres écrivent",
"Jump to read receipt": "Aller à l'accusé de lecture",
"World readable": "Lisible publiquement",
"Guests can join": "Les invités peuvent rejoindre le salon",
"Guests can join": "Accessible aux invités",
"To invite users into the room, you must be a": "Pour inviter des utilisateurs dans le salon, vous devez être un(e)",
"To configure the room, you must be a": "Pour configurer le salon, vous devez être un(e)",
"To kick users, you must be a": "Pour expulser des utilisateurs, vous devez être",
@ -1019,7 +1019,7 @@
"Cancel Sending": "Annuler l'envoi",
"This Room": "Ce salon",
"The Home Server may be too old to support third party networks": "Le serveur d'accueil semble trop ancien pour supporter des réseaux tiers",
"Noisy": "Bruyant",
"Noisy": "Sonore",
"Room not found": "Salon non trouvé",
"Messages containing my display name": "Messages contenant mon nom affiché",
"Messages in one-to-one chats": "Messages dans les discussions directes",
@ -1490,7 +1490,7 @@
"The other party cancelled the verification.": "L'autre personne a annulé la vérification.",
"Verified!": "Vérifié !",
"You've successfully verified this user.": "Vous avez vérifié cet utilisateur avec succès.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par personne d'autre.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par dautres personnes.",
"Got It": "Compris",
"Verify this user by confirming the following number appears on their screen.": "Vérifier cet utilisateur en confirmant que le nombre suivant apparaît sur leur écran.",
"For maximum security, we reccommend you do this in person or use another trusted means of communication.": "Pour une sécurité maximale, nous recommandons que vous fassiez cela en personne ou que vous utilisiez un autre moyen sûr de communication.",
@ -1569,7 +1569,7 @@
"Begin Verifying": "Commencer la vérification",
"Waiting for partner to accept...": "Nous attendons que le partenaire accepte…",
"Use two-way text verification": "Utiliser la vérification textuelle bidirectionnelle",
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour que ce soit un utilisateur de confiance. Faire confiance aux utilisateurs vous apporte la tranquillité d'esprit quand vous utilisez des messages chiffrés de bout en bout.",
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour que ce soit un utilisateur de confiance. Faire confiance aux utilisateurs vous apporte une tranquillité desprit quand vous utilisez des messages chiffrés de bout en bout.",
"Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "En vérifiant cet utilisateur, son appareil sera marqué comme appareil de confiance, et le vôtre sera aussi marqué comme appareil de confiance pour lui.",
"Waiting for partner to confirm...": "Nous attendons que le partenaire confirme…",
"Incoming Verification Request": "Demande de vérification entrante",
@ -1867,5 +1867,112 @@
"Riot failed to get the public room list.": "Riot na pas pu récupérer la liste des salons publics.",
"The homeserver may be unavailable or overloaded.": "Le serveur daccueil est peut-être indisponible ou surchargé.",
"You have %(count)s unread notifications in a prior version of this room.|other": "Vous avez %(count)s notifications non lues dans une version précédente de ce salon.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Vous avez %(count)s notification non lue dans une version précédente de ce salon."
"You have %(count)s unread notifications in a prior version of this room.|one": "Vous avez %(count)s notification non lue dans une version précédente de ce salon.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Si vous utilisez ou non la fonction « fil dariane » (les avatars au-dessus de la liste des salons)",
"Replying With Files": "Répondre avec des fichiers",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Pour le moment, il nest pas possible de répondre avec un fichier. Souhaitez-vous envoyer ce fichier sans répondre ?",
"The file '%(fileName)s' failed to upload.": "Le fichier « %(fileName)s » na pas pu être envoyé.",
"Show recent room avatars above the room list": "Afficher les avatars des salons récents au-dessus de la liste des salons",
"Rotate counter-clockwise": "Pivoter dans le sens inverse des aiguilles dune montre",
"Rotate clockwise": "Pivoter dans le sens des aiguilles dune montre",
"GitHub issue": "Rapport GitHub",
"Notes": "Notes",
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Sil y a des informations supplémentaires qui pourraient nous aider à analyser le problème, comme ce que vous faisiez, lidentifiant du salon ou des utilisateurs etc, veuillez les préciser ici.",
"Sign out and remove encryption keys?": "Se déconnecter et supprimer les clés de chiffrement ?",
"To help us prevent this in future, please <a>send us logs</a>.": "Pour nous aider à éviter cela dans le futur, veuillez <a>nous envoyer les journaux</a>.",
"Missing session data": "Données de la session manquantes",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Des données de la session, dont les clés des messages chiffrés, sont manquantes. Déconnectez-vous et reconnectez-vous pour régler ce problème, en restaurant les clés depuis la sauvegarde.",
"Your browser likely removed this data when running low on disk space.": "Votre navigateur a sûrement supprimé ces données car il restait peu despace sur le disque.",
"Upload files (%(current)s of %(total)s)": "Envoi des fichiers (%(current)s sur %(total)s)",
"Upload files": "Envoyer les fichiers",
"Upload": "Envoyer",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Le fichier est <b>trop lourd</b> pour être envoyé. La taille limite est de %(limit)s mais la taille de ce fichier est de %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Ces fichiers sont <b>trop lourds</b> pour être envoyés. La taille limite des fichiers est de %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Certains fichiers sont <b>trop lourds</b> pour être envoyés. La taille limite des fichiers est de %(limit)s.",
"Upload %(count)s other files|other": "Envoyer %(count)s autres fichiers",
"Upload %(count)s other files|one": "Envoyer %(count)s autre fichier",
"Cancel All": "Tout annuler",
"Upload Error": "Erreur denvoi",
"A conference call could not be started because the integrations server is not available": "Lappel en téléconférence na pas pu être lancé car les intégrations du serveur ne sont pas disponibles",
"The server does not support the room version specified.": "Le serveur ne prend pas en charge la version de salon spécifiée.",
"Name or Matrix ID": "Nom ou identifiant Matrix",
"Email, name or Matrix ID": "E-mail, nom ou identifiant Matrix",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Veuillez confirmer la mise à niveau de ce salon de <oldVersion /> à <newVersion />.",
"Changes your avatar in this current room only": "Change votre avatar seulement dans le salon actuel",
"Unbans user with given ID": "Révoque le bannissement de lutilisateur ayant lidentifiant fourni",
"Sends the given message coloured as a rainbow": "Envoie le message coloré aux couleurs de larc-en-ciel",
"Sends the given emote coloured as a rainbow": "Envoie lémoji coloré aux couleurs de larc-en-ciel",
"The user's homeserver does not support the version of the room.": "Le serveur daccueil de lutilisateur ne prend pas en charge la version de ce salon.",
"Edit messages after they have been sent (refresh to apply changes)": "Éditer les messages après leur envoi (actualiser pour appliquer les changements)",
"React to messages with emoji (refresh to apply changes)": "Réagir aux messages avec des émojis (actualiser pour appliquer les changements)",
"When rooms are upgraded": "Quand les salons sont mis à niveau",
"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.": "Cet appareil <b>ne sauvegarde pas vos clés</b>, mais vous avez une sauvegarde existante que vous pouvez restaurer et joindre.",
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connecter cet appareil à la sauvegarde de clés avant de vous déconnecter pour éviter de perdre des clés qui pourraient nêtre présentes que sur cet appareil.",
"Connect this device to Key Backup": "Connecter cet appareil à la sauvegarde de clés",
"Backup has an <validity>invalid</validity> signature from this device": "La sauvegarde a une signature <validity>invalide</validity> depuis cet appareil",
"this room": "ce salon",
"View older messages in %(roomName)s.": "Voir les messages plus anciens dans %(roomName)s.",
"Joining room …": "Inscription au salon…",
"Loading …": "Chargement…",
"Rejecting invite …": "Rejet de linvitation…",
"Join the conversation with an account": "Rejoindre la conversation avec un compte",
"Sign Up": "Sinscrire",
"Sign In": "Se connecter",
"You were kicked from %(roomName)s by %(memberName)s": "Vous avez été expulsé(e) de %(roomName)s par %(memberName)s",
"Reason: %(reason)s": "Motif : %(reason)s",
"Forget this room": "Oublier ce salon",
"Re-join": "Revenir",
"You were banned from %(roomName)s by %(memberName)s": "Vous avez été banni(e) de %(roomName)s par %(memberName)s",
"Something went wrong with your invite to %(roomName)s": "Une erreur est survenue avec votre invitation à %(roomName)s",
"%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s a été retourné en essayant de valider votre invitation. Vous pouvez essayer de transmettre cette information à ladministrateur du salon.",
"You can only join it with a working invite.": "Vous ne pouvez le rejoindre quavec une invitation fonctionnelle.",
"You can still join it because this is a public room.": "Vous pouvez quand même le rejoindre car cest un salon public.",
"Join the discussion": "Rejoindre la discussion",
"Try to join anyway": "Essayer de le rejoindre quand même",
"This invite to %(roomName)s wasn't sent to your account": "Cette invitation à %(roomName)s na pas été envoyée à votre compte",
"Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Connectez-vous avec un autre compte, demandez une autre invitation ou ajoutez ladresse e-mail %(email)s à ce compte.",
"Do you want to chat with %(user)s?": "Voulez-vous discuter avec %(user)s ?",
"Do you want to join %(roomName)s?": "Voulez-vous rejoindre %(roomName)s ?",
"<userName/> invited you": "<userName/> vous a invité(e)",
"You're previewing %(roomName)s. Want to join it?": "Ceci est un aperçu de %(roomName)s. Voulez-vous le rejoindre ?",
"%(roomName)s can't be previewed. Do you want to join it?": "Vous ne pouvez pas avoir daperçu de %(roomName)s. Voulez-vous le rejoindre ?",
"This room doesn't exist. Are you sure you're at the right place?": "Ce salon nexiste pas. Êtes-vous vraiment au bon endroit ?",
"Try again later, or ask a room admin to check if you have access.": "Réessayez plus tard ou demandez à ladministrateur du salon si vous y avez accès.",
"%(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>.": "%(errcode)s a été retourné en essayant daccéder au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez <issueLink>soumettre un rapport danomalie</issueLink>.",
"This room has already been upgraded.": "Ce salon a déjà été mis à niveau.",
"Agree or Disagree": "Accepter ou refuser",
"Like or Dislike": "Aimer ou ne pas aimer",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>ont réagi avec %(shortName)s</reactedWith>",
"Edited at %(date)s.": "Édité à %(date)s.",
"edited": "édité",
"Rotate Left": "Tourner à gauche",
"Rotate Right": "Tourner à droite",
"View Servers in Room": "Voir les serveurs dans le salon",
"Use an email address to recover your account": "Utiliser une adresse e-mail pour récupérer votre compte",
"Enter email address (required on this homeserver)": "Saisir ladresse e-mail (obligatoire sur ce serveur daccueil)",
"Doesn't look like a valid email address": "Cela ne ressemble pas a une adresse e-mail valide",
"Enter password": "Saisir le mot de passe",
"Password is allowed, but unsafe": "Ce mot de passe est autorisé, mais peu sûr",
"Nice, strong password!": "Bien joué, un mot de passe robuste !",
"Passwords don't match": "Les mots de passe ne correspondent pas",
"Other users can invite you to rooms using your contact details": "Dautres utilisateurs peuvent vous inviter à des salons grâce à vos informations de contact",
"Enter phone number (required on this homeserver)": "Saisir le numéro de téléphone (obligatoire sur ce serveur daccueil)",
"Doesn't look like a valid phone number": "Cela ne ressemble pas à un numéro de téléphone valide",
"Use letters, numbers, dashes and underscores only": "Utilisez uniquement des lettres, chiffres, traits dunion et tirets bas",
"Enter username": "Saisir le nom dutilisateur",
"Some characters not allowed": "Certains caractères ne sont pas autorisés",
"Use an email address to recover your account.": "Utilisez une adresse e-mail pour récupérer votre compte.",
"Other users can invite you to rooms using your contact details.": "Dautre utilisateurs peuvent vous inviter à des salons en utilisant vos informations de contact.",
"Error loading Riot": "Erreur lors du chargement de Riot",
"If this is unexpected, please contact your system administrator or technical support representative.": "Si cela est inattendu, veuillez contacter votre administrateur système ou un représentant du support technique.",
"Failed to get autodiscovery configuration from server": "Échec de la découverte automatique de la configuration depuis le serveur",
"Invalid base_url for m.homeserver": "base_url pour m.homeserver non valide",
"Homeserver URL does not appear to be a valid Matrix homeserver": "LURL du serveur daccueil ne semble pas être un serveur daccueil Matrix valide",
"Invalid base_url for m.identity_server": "base_url pour m.identity_server non valide",
"Identity server URL does not appear to be a valid identity server": "LURL du serveur didentité ne semble pas être un serveur didentité valide",
"Edited at %(date)s": "Édité à %(date)s",
"Show hidden events in timeline": "Afficher les évènements cachés dans lhistorique",
"Your profile": "Votre profil",
"Add room": "Ajouter un salon",
"Edit message": "Éditer le message"
}

View file

@ -772,5 +772,25 @@
"To ban users, you must be a": "उपयोगकर्ताओं को प्रतिबंधित करने के लिए, आपको होना चाहिए",
"To remove other users' messages, you must be a": "अन्य उपयोगकर्ताओं के संदेशों को हटाने के लिए, आपको होना चाहिए",
"To notify everyone in the room, you must be a": "कमरे में सभी को सूचित करने के लिए, आपको होना चाहिए",
"No users have specific privileges in this room": "इस कमरे में किसी भी उपयोगकर्ता के विशेष विशेषाधिकार नहीं हैं"
"No users have specific privileges in this room": "इस कमरे में किसी भी उपयोगकर्ता के विशेष विशेषाधिकार नहीं हैं",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "आप 'ब्रेडक्रंब' सुविधा का उपयोग कर रहे हैं या नहीं (कमरे की सूची के ऊपर अवतार)",
"Replying With Files": "फाइलों के साथ उत्तर",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "इस समय किसी फ़ाइल के साथ उत्तर देना संभव नहीं है। क्या आप इस फ़ाइल को बिना उत्तर दिए अपलोड करना चाहेंगे?",
"The file '%(fileName)s' failed to upload.": "फ़ाइल '%(fileName)s' अपलोड करने में विफल रही।",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "एक सादे पाठ संदेश के लिए ¯\\_(ツ)_/¯ प्रस्तुत करता है",
"Room upgrade confirmation": "रूम के उन्नयन की पुष्टि",
"Upgrading a room can be destructive and isn't always necessary.": "एक कमरे को अपग्रेड करना विनाशकारी हो सकता है और हमेशा आवश्यक नहीं होता है।",
"Room upgrades are usually recommended when a room version is considered <i>unstable</i>. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "एक कमरे के संस्करण को <i>अस्थिर</i> माना जाता है, तो आमतौर पर कमरे के उन्नयन की सिफारिश की जाती है। अस्थिर कमरे के संस्करणों में बग, लापता विशेषताएं या सुरक्षा कमजोरियां हो सकती हैं।",
"Room upgrades usually only affect <i>server-side</i> processing of the room. If you're having problems with your Riot client, please file an issue with <issueLink />.": "रूम का उन्नयन आमतौर पर केवल रूम के <i>सर्वर-साइड</i> को प्रभावित करता है। यदि आपको अपने रायट क्लाइंट के साथ समस्या हो रही है, तो कृपया <issueLink/> के साथ एक समस्या दर्ज करें।",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>चेतावनी</b>: किसी कमरे को अपग्रेड करना <i>कमरे के सदस्यों को कमरे के नए संस्करण में स्वचालित रूप से माइग्रेट नहीं करना है।</i> हम कमरे के पुराने संस्करण में नए कमरे के लिए एक लिंक पोस्ट करेंगे। नए कमरे में शामिल होने के लिए कमरे के सदस्यों को इस लिंक पर क्लिक करना होगा।",
"Upgrade": "अपग्रेड",
"Adds a custom widget by URL to the room": "रूम में URL द्वारा एक कस्टम विजेट जोड़ता है",
"Please supply a https:// or http:// widget URL": "कृपया एक https:// या http:// विजेट URL की आपूर्ति करें",
"You cannot modify widgets in this room.": "आप इस रूम में विजेट्स को संशोधित नहीं कर सकते।",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ने कमरे में शामिल होने के लिए %(targetDisplayName)s के निमंत्रण को रद्द कर दिया।",
"User %(userId)s is already in the room": "उपयोगकर्ता %(userId)s पहले से ही रूम में है",
"The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।",
"Show recent room avatars above the room list": "रूम की सूची के ऊपर हाल के अवतारों को दिखाएं",
"Enable desktop notifications for this device": "इस उपकरण के लिए डेस्कटॉप सूचनाएं सक्षम करें",
"Enable audible notifications for this device": "इस उपकरण के लिए श्रव्य सूचनाएँ सक्षम करें"
}

View file

@ -961,7 +961,7 @@
"Failed to add tag %(tagName)s to room": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s",
"Clear filter": "Szűrő törlése",
"Disable Community Filter Panel": "Közösség keresési panel tiltása",
"Did you know: you can use communities to filter your Riot.im experience!": "Tudtad, hogy a Riot.im élmény fokozásához használhatsz közösségeket?",
"Did you know: you can use communities to filter your Riot.im experience!": "Tudtad, hogy a Riot.im élmény fokozásához használhatsz közösségeket!",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "A szűrő beállításához húzd a közösség avatarját a szűrő panel fölé a képernyő bal szélén. A szűrő panelen az avatarra kattintva bármikor leszűrheted azokat a szobákat és embereket akik a megadott közösséghez tartoznak.",
"Your key share request has been sent - please check your other devices for key share requests.": "A kulcs megosztási kérést elküldtük - ellenőrizd a többi eszközödön a kulcs megosztási kéréseket.",
"Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "A kulcs megosztási kérelem automatikusan el lett küldve a többi eszközödre. Ha elutasítottad vagy törölted a kérést a másik eszközön ide kattintva újra kérheted a kulcsokat.",
@ -1175,8 +1175,8 @@
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Kérlek töröld az összes általam küldött üzenetet amikor a fiókomat felfüggesztem (<b>Figyelem:</b> ez azt eredményezheti, hogy a jövőbeni felhasználók csak részleges beszélgetést látnak majd)",
"e.g. %(exampleValue)s": "pl. %(exampleValue)s",
"Reload widget": "Kisalkalmazás újratöltése",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Kérlek segíts javítani a Riot.im-et azzal, hogy <UsageDataLink>anonim felhasználási adatokat</UsageDataLink> küldesz. Ez szütit (cookie) fog használni (lásd a <PolicyLink>sütire vonatkozó szabályozásunkat</PolicyLink>).",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Kérlek segíts javítani a Riot.im-et azzal, hogy <UsageDataLink>anonim felhasználási adatokat</UsageDataLink> küldesz. Ez szütit (cookie) fog használni.",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Kérlek segíts javítani a Riot.im-et azzal, hogy <UsageDataLink>anonim felhasználási adatokat</UsageDataLink> küldesz. Ez sütit (cookie) fog használni (lásd a <PolicyLink>sütire vonatkozó szabályozásunkat</PolicyLink>).",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Kérlek segíts javítani a Riot.im-et azzal, hogy <UsageDataLink>anonim felhasználási adatokat</UsageDataLink> küldesz. Ez sütit (cookie) fog használni.",
"Yes, I want to help!": "Igen, segítek!",
"Can't leave Server Notices room": "Nem lehet elhagyni a Szerver Üzenetek szobát",
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Ez a szoba fontos szerverüzenetek közlésére jött létre, nem tudsz kilépni belőle.",
@ -1867,5 +1867,111 @@
"Riot failed to get the public room list.": "Riotnak nem sikerült beszereznie a nyilvános szoba listát.",
"The homeserver may be unavailable or overloaded.": "A Matrix szerver elérhetetlen vagy túlterhelt.",
"You have %(count)s unread notifications in a prior version of this room.|other": "%(count)s olvasatlan értesítésed van a régi verziójú szobában.",
"You have %(count)s unread notifications in a prior version of this room.|one": "%(count)s olvasatlan értesítésed van a régi verziójú szobában."
"You have %(count)s unread notifications in a prior version of this room.|one": "%(count)s olvasatlan értesítésed van a régi verziójú szobában.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Használsz vagy nem „morzsákat” (profilképek a szobalista felett)",
"Replying With Files": "Válasz fájlokkal",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Egyenlőre nem lehet fájlal válaszolni. Szeretnéd feltölteni a fájlt úgy, hogy az nem egy válasz lesz?",
"The file '%(fileName)s' failed to upload.": "A %(fileName)s fájlt nem sikerült feltölteni.",
"Show recent room avatars above the room list": "A legfrissebb szoba profilképét mutassa a szoba lista felett",
"Rotate counter-clockwise": "Óramutató járásával ellentétesen fordít",
"Rotate clockwise": "Óramutató járásával megegyező irányba fordít",
"GitHub issue": "GitHub hibajegy",
"Notes": "Megjegyzések",
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Ha a hiba felderítésében további adat is segítséget adhat, mint az, hogy mit csináltál éppen, mi a szoba-, felhasználó azonosítója, stb... itt add meg.",
"Sign out and remove encryption keys?": "Kilépés és a titkosítási kulcsok törlése?",
"To help us prevent this in future, please <a>send us logs</a>.": "Segíts abban, hogy ez később ne fordulhasson elő, kérlek küld el a <a>naplókat</a>.",
"Missing session data": "A kapcsolati adat hiányzik",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Néhány kapcsolati adat hiányzik, beleértve a titkosított üzenetek kulcsait. Lépj ki és jelentkezz vissza a hiba javításához és állítsd vissza mentésből a kulcsokat.",
"Your browser likely removed this data when running low on disk space.": "A böngésző valószínűleg törölte ezeket az adatokat amikor lecsökkent a szabad lemezterület.",
"Upload files (%(current)s of %(total)s)": "Fájlok feltöltése (%(current)s / %(total)s)",
"Upload files": "Fájlok feltöltése",
"Upload": "Feltöltés",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Ez a fájl <b>túl nagy</b>, hogy fel lehessen tölteni. A fájl méret korlát %(limit)s de a fájl %(sizeOfThisFile)s méretű.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "A fájl <b>túl nagy</b> a feltöltéshez. A fájlméret korlát %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Néhány fájl <b>túl nagy</b>, hogy fel lehessen tölteni. A fájlméret korlát %(limit)s.",
"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",
"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",
"A conference call could not be started because the integrations server is not available": "A konferencia hívást nem lehet elkezdeni mert az integrációs szerver nem érhető el",
"Name or Matrix ID": "Név vagy Matrix azon.",
"Email, name or Matrix ID": "E-mail, név vagy Matrix azon.",
"Unbans user with given ID": "Visszaengedi a megadott azonosítójú felhasználót",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>ezzel reagált: %(shortName)s</reactedWith>",
"Edited at %(date)s.": "Szerkesztve ekkor: %(date)s.",
"edited": "szerkesztve",
"Edited at %(date)s": "Szerkesztve: %(date)s",
"Show hidden events in timeline": "Rejtett események megmutatása az idővonalon",
"Add room": "Szoba hozzáadása",
"Your profile": "Profilod"
}

View file

@ -1822,5 +1822,32 @@
"Riot failed to get the public room list.": "Riot non è riuscito ad ottenere l'elenco di stanze pubbliche.",
"The homeserver may be unavailable or overloaded.": "L'homeserver potrebbe non essere disponibile o sovraccarico.",
"You have %(count)s unread notifications in a prior version of this room.|other": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza."
"You have %(count)s unread notifications in a prior version of this room.|one": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Che tu stia usando o meno la funzione 'breadcrumbs' (avatar sopra l'elenco delle stanze)",
"A conference call could not be started because the integrations server is not available": "Non è stato possibile avviare una conferenza perchè il server di integrazione non è disponibile",
"Replying With Files": "Risposta con dei file",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Al momento non è possibile rispondere con un file. Vorresti inviare questo file senza rispondere?",
"The file '%(fileName)s' failed to upload.": "Invio del file '%(fileName)s' fallito.",
"The server does not support the room version specified.": "Il server non supporta la versione di stanza specificata.",
"Name or Matrix ID": "Nome o ID Matrix",
"Email, name or Matrix ID": "Email, nome o ID Matrix",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Conferma di volere continuare nell'aggiornamento di questa stanza dalla <oldVersion /> alla <newVersion />.",
"Changes your avatar in this current room only": "Cambia il tuo avatar solo nella stanza attuale",
"Unbans user with given ID": "Riammette l'utente con l'ID dato",
"Sends the given message coloured as a rainbow": "Invia il messaggio dato colorato come un arcobaleno",
"Sends the given emote coloured as a rainbow": "Invia l'emoticon dato colorato come un arcobaleno",
"The user's homeserver does not support the version of the room.": "L'homeserver dell'utente non supporta la versione della stanza.",
"Show recent room avatars above the room list": "Mostra gli avatar recenti della stanza sopra l'elenco stanze",
"Edit messages after they have been sent (refresh to apply changes)": "Modifica i messaggi dopo l'invio (ricarica per applicare le modifiche)",
"React to messages with emoji (refresh to apply changes)": "Reagisci ai messaggi con emoji (ricarica per applicare le modifiche)",
"Show hidden events in timeline": "Mostra eventi nascosti nella timeline",
"When rooms are upgraded": "Quando le stanze vengono aggiornate",
"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.": "Questo dispositivo <b>non sta facendo backup delle tue chiavi</b>, ma hai un backup esistente che puoi ripristinare e aggiungere da adesso in poi.",
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connetti questo dispositivo al backup chiavi prima di disconnetterti per evitare di perdere chiavi che potrebbero essere solo in questo dispositivo.",
"Connect this device to Key Backup": "Connetti questo dispositibo al Backup Chiavi",
"Backup has an <validity>invalid</validity> signature from this device": "Il backup ha un firma <validity>non valida</validity> da questo dispositivo",
"this room": "questa stanza",
"View older messages in %(roomName)s.": "Vedi messaggi più vecchi in %(roomName)s.",
"Joining room …": "Ingresso nella stanza …",
"Loading …": "Caricamento …"
}

View file

@ -1126,5 +1126,10 @@
"Collapse panel": "Sakļaut (saritināt) paneli",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Tavā pašreizējā pārlūkā aplikācijas izskats un uzvedība var būt pilnīgi neatbilstoša, kā arī dažas no visām funkcijām var nedarboties. Ja vēlies turpināt izmantot šo pārlūku, Tu vari arī turpināt, apzinoties, ka šajā gadījumā esi viens/a ar iespējamo problēmu!",
"Checking for an update...": "Lūkojos pēc aktualizācijas...",
"There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti"
"There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti",
"e.g. %(exampleValue)s": "piemēram %(exampleValue)s",
"e.g. <CurrentPageURL>": "piemēram <CurrentPageURL>",
"Your device resolution": "Tavas iekārtas izšķirtspēja",
"Sign In": "Ienākt",
"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.": "Varat arī iestatīt pielāgotu identitātes serveri, bet jūs nevarēsiet uzaicināt lietotājus izmantojot e-pasta adresi, kā arī tikt uzaicināts pēc e-pasta adreses."
}

View file

@ -1771,5 +1771,35 @@
"Riot failed to get the public room list.": "Riot kon de lijst met openbare gesprekken niet verkrijgen.",
"The homeserver may be unavailable or overloaded.": "De thuisserver is mogelijk onbereikbaar of overbelast.",
"You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een voorgaande versie van dit gesprek.",
"You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen melding in een voorgaande versie van dit gesprek."
"You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen melding in een voorgaande versie van dit gesprek.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de 'broodkruimels'-functie al dan niet gebruikt (avatars boven de gesprekslijst)",
"Replying With Files": "Beantwoorden met bestanden",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momenteel is het niet mogelijk om met een bestand te antwoorden. Wilt u dit bestand uploaden zonder te antwoorden?",
"The file '%(fileName)s' failed to upload.": "Het bestand %(fileName)s kon niet geüpload worden.",
"Show recent room avatars above the room list": "Recente gespreksavatars weergeven boven de gesprekslijst",
"Rotate counter-clockwise": "Tegen de klok in draaien",
"Rotate clockwise": "Met de klok mee draaien",
"GitHub issue": "GitHub-melding",
"Notes": "Opmerkingen",
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Indien er extra context zou kunnen helpen om het probleem te analyseren, zoals wat u aan het doen was, relevante gespreks-IDs, gebruikers-IDs, enz., gelieve deze informatie dan hier mee te geven.",
"Sign out and remove encryption keys?": "Afmelden en versleutelingssleutels verwijderen?",
"To help us prevent this in future, please <a>send us logs</a>.": "Gelieve <a>ons logboeken te sturen</a> om dit in de toekomst te helpen voorkomen.",
"Missing session data": "Sessiegegevens ontbreken",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, inclusief sleutels voor versleutelde berichten, ontbreken. Meld u af en weer aan om dit op te lossen, en herstel de sleutels uit de back-up.",
"Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens mogelijk verwijderd toen de beschikbare opslagruimte vol was.",
"Upload files (%(current)s of %(total)s)": "Bestanden worden geüpload (%(current)s van %(total)s)",
"Upload files": "Bestanden uploaden",
"Upload": "Uploaden",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is <b>te groot</b> om te uploaden. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Deze bestanden zijn <b>te groot</b> om te uploaden. De bestandsgroottelimiet is %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn <b>te groot</b> om te uploaden. De bestandsgroottelimiet is %(limit)s.",
"Upload %(count)s other files|other": "%(count)s overige bestanden uploaden",
"Upload %(count)s other files|one": "%(count)s overig bestand uploaden",
"Cancel All": "Alles annuleren",
"Upload Error": "Uploadfout",
"A conference call could not be started because the integrations server is not available": "Daar de integratieserver onbereikbaar is kon het groepsaudiogesprek niet gestart worden.",
"The server does not support the room version specified.": "De server ondersteunt deze versie van gesprekken niet.",
"Name or Matrix ID": "Naam of Matrix-ID",
"Email, name or Matrix ID": "E-mail, naam, of matrix-ID",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Bevestig dat u dit gesprek van <oldVersion /> wilt opwaarderen naar <newVersion />."
}

View file

@ -128,7 +128,7 @@
"Are you sure you want to reject the invitation?": "Czy na pewno chcesz odrzucić zaproszenie?",
"Are you sure you want to upload the following files?": "Czy na pewno chcesz przesłać następujące pliki?",
"Autoplay GIFs and videos": "Automatycznie odtwarzaj GIFy i filmiki",
"%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował %(targetName)s.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował(a) %(targetName)s.",
"Ban": "Zbanuj",
"Bans user with given id": "Blokuje użytkownika o podanym ID",
"Blacklisted": "Umieszczono na czarnej liście",
@ -456,7 +456,7 @@
"Unable to create widget.": "Nie można utworzyć widżetu.",
"Unable to remove contact information": "Nie można usunąć informacji kontaktowych",
"Unable to verify email address.": "Weryfikacja adresu e-mail nie powiodła się.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował/a %(targetName)s.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował(a) %(targetName)s.",
"Unable to capture screen": "Nie można zrobić zrzutu ekranu",
"Unable to enable Notifications": "Nie można włączyć powiadomień",
"Unable to load device list": "Nie można załadować listy urządzeń",
@ -1337,5 +1337,109 @@
"Show read receipts": "Wyświetl potwierdzenia odczytu",
"Send typing notifications": "Wyślij powiadomienia o pisaniu",
"I don't want my encrypted messages": "Nie chcę moich zaszyfrowanych wiadomości",
"You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości"
"You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości",
"Verified!": "Zweryfikowano!",
"Dog": "Pies",
"Cat": "Kot",
"Lion": "Lew",
"Horse": "Koń",
"Unicorn": "Jednorożec",
"Pig": "Świnia",
"Elephant": "Słoń",
"Rabbit": "Królik",
"Panda": "Panda",
"Rooster": "Kogut",
"Penguin": "Pingwin",
"Turtle": "Żółw",
"Fish": "Ryba",
"Octopus": "Ośmiornica",
"Butterfly": "Motyl",
"Flower": "Kwiat",
"Tree": "Drzewo",
"Cactus": "Kaktus",
"Mushroom": "Grzyb",
"Moon": "Księżyc",
"Cloud": "Chmura",
"Fire": "Ogień",
"Banana": "Banan",
"Apple": "Jabłko",
"Strawberry": "Truskawka",
"Corn": "Kukurydza",
"Pizza": "Pizza",
"Cake": "Ciasto",
"Heart": "Serce",
"Robot": "Robot",
"Hat": "Kapelusz",
"Glasses": "Okulary",
"Umbrella": "Parasol",
"Hourglass": "Klepsydra",
"Clock": "Zegar",
"Light bulb": "Żarówka",
"Book": "Książka",
"Pencil": "Ołówek",
"Paperclip": "Spinacz",
"Scissors": "Nożyczki",
"Padlock": "Kłódka",
"Key": "Klucz",
"Telephone": "Telefon",
"Flag": "Flaga",
"Train": "Pociąg",
"Bicycle": "Rower",
"Aeroplane": "Samolot",
"Rocket": "Rakieta",
"Trophy": "Trofeum",
"Guitar": "Gitara",
"Trumpet": "Trąbka",
"Bell": "Dzwonek",
"Anchor": "Kotwica",
"Headphones": "Słuchawki",
"Folder": "Folder",
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "W celu zapewnienia maksymalnego bezpieczeństwa zalecamy, abyś zrobił to osobiście lub skorzystał z innego zaufanego środka komunikacji.",
"Phone Number": "Numer telefonu",
"Display Name": "Wyświetlana nazwa",
"Set a new account password...": "Ustaw nowe hasło do konta…",
"Email addresses": "Adresy E-mail",
"Phone numbers": "Numery telefonów",
"Language and region": "Język i region",
"Theme": "Motyw",
"Account management": "Zarządzanie kontem",
"Bug reporting": "Zgłaszanie błędów",
"Versions": "Wersje",
"Preferences": "Preferencje",
"Timeline": "Oś czasu",
"Room list": "Lista pokoi",
"Security & Privacy": "Bezpieczeństwo i prywatność",
"Room Addresses": "Adresy pokoju",
"Change room avatar": "Zmień awatar pokoju",
"Change room name": "Zmień nazwę pokoju",
"Change permissions": "Zmieniać uprawnienia",
"Change topic": "Zmieniać temat",
"Default role": "Domyślna rola",
"Send messages": "Wysyłanie wiadomości",
"Change settings": "Zmieniać ustawienia",
"Remove messages": "Usuwanie wiadomości",
"Notify everyone": "Powiadamianie wszystkich",
"Roles & Permissions": "Role i uprawnienia",
"Encryption": "Szyfrowanie",
"Join the conversation with an account": "Przyłącz się do rozmowy przy użyciu konta",
"Sign Up": "Zarejestruj się",
"Join the discussion": "Dołącz do dyskusji",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s nie może być wyświetlony. Chcesz do niego dołączyć?",
"Main address": "Główny adres",
"Room avatar": "Awatar pokoju",
"Upload room avatar": "Prześlij awatar pokoju",
"Room Name": "Nazwa pokoju",
"Room Topic": "Temat pokoju",
"Power level": "Poziom uprawnień",
"Verify by comparing a short text string.": "Weryfikuj, porównując krótki ciąg tekstu.",
"Begin Verifying": "Rozpocznij weryfikację",
"Waiting for partner to accept...": "Czekanie, aż partner zaakceptuje…",
"Room Settings - %(roomName)s": "Ustawienia pokoju - %(roomName)s",
"Doesn't look like a valid phone number": "To nie wygląda na poprawny numer telefonu",
"Globe": "Ziemia",
"Smiley": "Uśmiech",
"Spanner": "Klucz francuski",
"Santa": "Mikołaj",
"Gift": "Prezent",
"Hammer": "Młotek"
}

View file

@ -1 +1,14 @@
{}
{
"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.",
"Your language of choice": "Vaš jezik po izbiri"
}

Some files were not shown because too many files have changed in this diff Show more