Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup

This commit is contained in:
J. Ryan Stinnett 2019-12-11 10:05:20 +00:00
commit fae819dfe5
149 changed files with 3358 additions and 12421 deletions

View file

@ -1,10 +1,6 @@
steps:
- label: ":eslint: Lint"
command:
# TODO: Remove hacky chmod for BuildKite
- "echo '--- Setup'"
- "chmod +x ./scripts/ci/*.sh"
- "chmod +x ./scripts/*"
- "echo '--- Install js-sdk'"
- "./scripts/ci/install-deps.sh"
- "yarn lintwithexclusions"

View file

@ -1,3 +1,117 @@
Changes in [1.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5) (2019-12-09)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5-rc.1...v1.7.5)
* No changes since rc.1
Changes in [1.7.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5-rc.1) (2019-12-04)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.4...v1.7.5-rc.1)
* Remove logs before running end-to-end tests
[\#3700](https://github.com/matrix-org/matrix-react-sdk/pull/3700)
* Update from Weblate
[\#3699](https://github.com/matrix-org/matrix-react-sdk/pull/3699)
* Match e2e icons on events to expectations
[\#3697](https://github.com/matrix-org/matrix-react-sdk/pull/3697)
* Match room upgrade warning to the new design
[\#3695](https://github.com/matrix-org/matrix-react-sdk/pull/3695)
* Remove unused translations
[\#3683](https://github.com/matrix-org/matrix-react-sdk/pull/3683)
* Remove broken velocity-ui animation
[\#3678](https://github.com/matrix-org/matrix-react-sdk/pull/3678)
* Update from Weblate
[\#3696](https://github.com/matrix-org/matrix-react-sdk/pull/3696)
* Hide Remove button in message editing history if you don't have permission
to redact
[\#3685](https://github.com/matrix-org/matrix-react-sdk/pull/3685)
* Add an option to invite users to upgraded private rooms
[\#3684](https://github.com/matrix-org/matrix-react-sdk/pull/3684)
* Do not trap Key ContextMenu into composer for keyboard a11y
[\#3689](https://github.com/matrix-org/matrix-react-sdk/pull/3689)
* Make EmojiPicker filtering case-insensitive
[\#3690](https://github.com/matrix-org/matrix-react-sdk/pull/3690)
* Ensure the settings page accurately represents theme choices
[\#3686](https://github.com/matrix-org/matrix-react-sdk/pull/3686)
* Ensure read receipts end up with a valid reference to checkUnmounting
[\#3688](https://github.com/matrix-org/matrix-react-sdk/pull/3688)
* Convert Velociraptor component to a class
[\#3687](https://github.com/matrix-org/matrix-react-sdk/pull/3687)
* Add a link to the labs feature documentation
[\#3675](https://github.com/matrix-org/matrix-react-sdk/pull/3675)
* Improve translatable strings for calls
[\#3682](https://github.com/matrix-org/matrix-react-sdk/pull/3682)
* Don't assume that diffs will have an appropriate child node
[\#3680](https://github.com/matrix-org/matrix-react-sdk/pull/3680)
* Fix persisted widgets getting stuck at loading screens
[\#3681](https://github.com/matrix-org/matrix-react-sdk/pull/3681)
* Add button to clear all notification counts, sometimes stuck in historical
[\#2959](https://github.com/matrix-org/matrix-react-sdk/pull/2959)
* Fix multi-invite error dialog messaging
[\#3679](https://github.com/matrix-org/matrix-react-sdk/pull/3679)
* Make the communities button behave more like a toggle
[\#3670](https://github.com/matrix-org/matrix-react-sdk/pull/3670)
* Change read markers to use CSS transitions
[\#3674](https://github.com/matrix-org/matrix-react-sdk/pull/3674)
* fix font smoothing to match figma
[\#3677](https://github.com/matrix-org/matrix-react-sdk/pull/3677)
* Update breadcrumbs when we do eventually see upgraded rooms
[\#3669](https://github.com/matrix-org/matrix-react-sdk/pull/3669)
* Fix override behaviour of system vs defined themes
[\#3673](https://github.com/matrix-org/matrix-react-sdk/pull/3673)
* console.log doesn't take %s substitutions
[\#3671](https://github.com/matrix-org/matrix-react-sdk/pull/3671)
* EventIndex: Move the checkpoint loading logic into the init method.
[\#3648](https://github.com/matrix-org/matrix-react-sdk/pull/3648)
* Clarify that cross-signing is in development
[\#3668](https://github.com/matrix-org/matrix-react-sdk/pull/3668)
* Hide tooltips with CSS when they aren't visible
[\#3665](https://github.com/matrix-org/matrix-react-sdk/pull/3665)
* a11y: adjustments for toasts
[\#3667](https://github.com/matrix-org/matrix-react-sdk/pull/3667)
* Update from Weblate
[\#3666](https://github.com/matrix-org/matrix-react-sdk/pull/3666)
* Null check on thumbnail_file
[\#3664](https://github.com/matrix-org/matrix-react-sdk/pull/3664)
* Fix double date separator for room upgrade tiles
[\#3662](https://github.com/matrix-org/matrix-react-sdk/pull/3662)
* Show incoming verification requests in in-app notifications
[\#3661](https://github.com/matrix-org/matrix-react-sdk/pull/3661)
* Show m.room.create event before the ELS on room upgrade
[\#3655](https://github.com/matrix-org/matrix-react-sdk/pull/3655)
* Convert MessagePanel to React class
[\#3656](https://github.com/matrix-org/matrix-react-sdk/pull/3656)
* Make addEventListener conditional
[\#3657](https://github.com/matrix-org/matrix-react-sdk/pull/3657)
* Fix e2e icons
[\#3653](https://github.com/matrix-org/matrix-react-sdk/pull/3653)
* Workaround for soft-crash with calls on startup
[\#3654](https://github.com/matrix-org/matrix-react-sdk/pull/3654)
* Catch exceptions when we can't play audio
[\#3652](https://github.com/matrix-org/matrix-react-sdk/pull/3652)
* Rename section heading for integrations in settings
[\#3650](https://github.com/matrix-org/matrix-react-sdk/pull/3650)
* Update copy for widgets not using message encryption
[\#3651](https://github.com/matrix-org/matrix-react-sdk/pull/3651)
* Ignore media actions
[\#3649](https://github.com/matrix-org/matrix-react-sdk/pull/3649)
* Add an option to disable the use of integration managers for provisioning
[\#3646](https://github.com/matrix-org/matrix-react-sdk/pull/3646)
* Move many widget options to a context menu
[\#3645](https://github.com/matrix-org/matrix-react-sdk/pull/3645)
* Re-add encryption warning to widget permission prompt
[\#3644](https://github.com/matrix-org/matrix-react-sdk/pull/3644)
* Update CIDER docs now that it is used for main composer as well
[\#3647](https://github.com/matrix-org/matrix-react-sdk/pull/3647)
* get rid of bluebird
[\#3593](https://github.com/matrix-org/matrix-react-sdk/pull/3593)
* Remove getBaseTheme
[\#3638](https://github.com/matrix-org/matrix-react-sdk/pull/3638)
* ReactionsRowButtonTooltip: fix null dereference if emoji owner left room
[\#3643](https://github.com/matrix-org/matrix-react-sdk/pull/3643)
* Add eslint-plugin-jest because we inherit js-sdk's eslintrc and it wants
[\#3642](https://github.com/matrix-org/matrix-react-sdk/pull/3642)
Changes in [1.7.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.4) (2019-11-27)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3...v1.7.4)

View file

@ -174,12 +174,6 @@ React
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
```
Not doing so is acceptable in a single case: in function-refs:
```jsx
<Foo ref={(self) => this.component = self}>
```
- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass`
- You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor):
@ -208,3 +202,5 @@ React
```
- Think about whether your component really needs state: are you duplicating
information in component state that could be derived from the model?
- Avoid things marked as Legacy or Deprecated in React 16 (e.g string refs and legacy contexts)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "1.7.4",
"version": "1.7.5",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -88,7 +88,7 @@
"linkifyjs": "^2.1.6",
"lodash": "^4.17.14",
"lolex": "4.2",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-js-sdk": "2.4.6",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"png-chunks-extract": "^1.0.0",
@ -110,6 +110,7 @@
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
"velocity-animate": "^1.5.2",
"what-input": "^5.2.6",
"whatwg-fetch": "^1.1.1",
"zxcvbn": "^4.4.2"
},

View file

@ -66,6 +66,7 @@
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss";
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
@import "./views/dialogs/_SetEmailDialog.scss";
@import "./views/dialogs/_SetMxIdDialog.scss";
@import "./views/dialogs/_SetPasswordDialog.scss";

View file

@ -49,23 +49,23 @@ limitations under the License.
padding: 0;
list-style: none;
li.mx_TopLeftMenu_icon_home::after {
.mx_TopLeftMenu_icon_home::after {
mask-image: url('$(res)/img/feather-customised/home.svg');
}
li.mx_TopLeftMenu_icon_settings::after {
.mx_TopLeftMenu_icon_settings::after {
mask-image: url('$(res)/img/feather-customised/settings.svg');
}
li.mx_TopLeftMenu_icon_signin::after {
.mx_TopLeftMenu_icon_signin::after {
mask-image: url('$(res)/img/feather-customised/sign-in.svg');
}
li.mx_TopLeftMenu_icon_signout::after {
.mx_TopLeftMenu_icon_signout::after {
mask-image: url('$(res)/img/feather-customised/sign-out.svg');
}
li::after {
.mx_AccessibleButton::after {
mask-repeat: no-repeat;
mask-position: 0 center;
mask-size: 16px;
@ -78,14 +78,14 @@ limitations under the License.
background-color: $primary-fg-color;
}
li {
.mx_AccessibleButton {
position: relative;
cursor: pointer;
white-space: nowrap;
padding: 5px 20px 5px 43px;
}
li:hover {
.mx_AccessibleButton:hover {
background-color: $menu-selected-color;
}
}

View file

@ -0,0 +1,37 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomUpgradeWarningDialog {
max-width: 38vw;
width: 38vw;
}
.mx_RoomUpgradeWarningDialog .mx_SettingsFlag {
font-weight: 700;
.mx_ToggleSwitch {
display: inline-block;
vertical-align: middle;
margin-left: 8px;
float: right;
}
.mx_SettingsFlag_label {
display: inline-block;
vertical-align: middle;
}
}

View file

@ -169,6 +169,7 @@ limitations under the License.
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
@ -347,27 +348,28 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
}
.mx_EventTile_e2eIcon {
display: block;
position: absolute;
top: 8px;
top: 6px;
left: 46px;
width: 15px;
height: 15px;
cursor: pointer;
mask-size: 14px;
mask-repeat: no-repeat;
mask-position: 0;
display: block;
bottom: 0;
right: 0;
opacity: 0.2;
background-repeat: no-repeat;
background-size: contain;
}
.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $warning-color;
background-image: url('$(res)/img/e2e/warning.svg');
opacity: 1;
}
.mx_EventTile_e2eIcon_unencrypted {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $composer-e2e-icon-color;
background-image: url('$(res)/img/e2e/warning.svg');
opacity: 1;
}
.mx_EventTile_e2eIcon_hidden {

View file

@ -37,6 +37,10 @@ limitations under the License.
mask-position: center;
}
.mx_SearchBar_buttons {
display: inherit;
}
.mx_SearchBar_button {
border: 0;
margin: 0 0 0 22px;

0
scripts/ci/build.sh Normal file → Executable file
View file

2
scripts/ci/end-to-end-tests.sh Normal file → Executable file
View file

@ -36,7 +36,7 @@ echo "--- Install synapse & other dependencies"
./install.sh
# install static webserver to server symlinked local copy of riot
./riot/install-webserver.sh
mkdir logs
mkdir logs || rm -r logs/*
echo "+++ Running end-to-end tests"
TESTS_STARTED=1
./run.sh --no-sandbox --log-directory logs/

0
scripts/ci/install-deps.sh Normal file → Executable file
View file

0
scripts/ci/riot-unit-tests.sh Normal file → Executable file
View file

0
scripts/ci/unit-tests.sh Normal file → Executable file
View file

View file

@ -78,6 +78,7 @@ export const Key = {
CONTROL: "Control",
META: "Meta",
SHIFT: "Shift",
CONTEXT_MENU: "ContextMenu",
LESS_THAN: "<",
GREATER_THAN: ">",

View file

@ -153,13 +153,8 @@ function _onStartDmFinished(shouldInvite, addrs) {
}
}
function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
if (!shouldInvite) return;
const addrTexts = addrs.map((addr) => addr.address);
// Invite new users to a room
inviteMultipleToRoom(roomId, addrTexts).then((result) => {
export function inviteUsersToRoom(roomId, userIds) {
return inviteMultipleToRoom(roomId, userIds).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
return _showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
@ -172,6 +167,15 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
});
}
function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
if (!shouldInvite) return;
const addrTexts = addrs.map((addr) => addr.address);
// Invite new users to a room
inviteUsersToRoom(roomId, addrTexts);
}
// TODO: Immutable DMs replaces this
function _isDmChat(addrTexts) {
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') {

View file

@ -32,6 +32,7 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
import {inviteUsersToRoom} from "./RoomInvite";
const singleMxcUpload = async () => {
return new Promise((resolve) => {
@ -154,70 +155,58 @@ export const CommandMap = {
return reject(_t("You do not have the required permissions to use this command."));
}
const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog");
const {finished} = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
QuestionDialog, {
title: _t('Room upgrade confirmation'),
description: (
<div>
<p>{_t("Upgrading a room can be destructive and isn't always necessary.")}</p>
<p>
{_t(
"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": (sub) => <i>{sub}</i>,
},
)}
</p>
<p>
{_t(
"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": (sub) => <i>{sub}</i>,
"issueLink": () => {
return <a href="https://github.com/vector-im/riot-web/issues/new/choose"
target="_blank" rel="noopener">
https://github.com/vector-im/riot-web/issues/new/choose
</a>;
},
},
)}
</p>
<p>
{_t(
"<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": (sub) => <b>{sub}</b>,
"i": (sub) => <i>{sub}</i>,
},
)}
</p>
<p>
{_t(
"Please confirm that you'd like to go forward with upgrading this room " +
"from <oldVersion /> to <newVersion />.",
{},
{
oldVersion: () => <code>{room ? room.getVersion() : "1"}</code>,
newVersion: () => <code>{args}</code>,
},
)}
</p>
</div>
),
button: _t("Upgrade"),
});
RoomUpgradeWarningDialog, {roomId: roomId, targetVersion: args}, /*className=*/null,
/*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(([confirm]) => {
if (!confirm) return;
return success(finished.then(async ([resp]) => {
if (!resp.continue) return;
return cli.upgradeRoom(roomId, args);
let checkForUpgradeFn;
try {
const upgradePromise = cli.upgradeRoom(roomId, args);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (resp.invite) {
checkForUpgradeFn = async (newRoom) => {
// The upgradePromise should be done by the time we await it here.
const {replacement_room: newRoomId} = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
await upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
'Double check that your server supports the room version chosen and try again.'),
});
}
}));
}
return reject(this.getUsage());

View file

@ -36,6 +36,8 @@ module.exports = {
return false;
} else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') {
return false;
} else if (ev.getType() == 'm.room.server_acl') {
return false;
}
const EventTile = sdk.getComponent('rooms.EventTile');
return EventTile.haveTileForEvent(ev);

View file

@ -1,7 +1,6 @@
const React = require('react');
const ReactDom = require('react-dom');
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const Velocity = require('velocity-animate');
/**
@ -11,10 +10,8 @@ const Velocity = require('velocity-animate');
* from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries.
*/
module.exports = createReactClass({
displayName: 'Velociraptor',
propTypes: {
export default class Velociraptor extends React.Component {
static propTypes = {
// either a list of child nodes, or a single child.
children: PropTypes.any,
@ -26,82 +23,71 @@ module.exports = createReactClass({
// a list of transition options from the corresponding startStyle
enterTransitionOpts: PropTypes.array,
},
};
getDefaultProps: function() {
return {
startStyles: [],
enterTransitionOpts: [],
};
},
static defaultProps = {
startStyles: [],
enterTransitionOpts: [],
};
constructor(props) {
super(props);
componentWillMount: function() {
this.nodes = {};
this._updateChildren(this.props.children);
},
}
componentWillReceiveProps: function(nextProps) {
this._updateChildren(nextProps.children);
},
componentDidUpdate() {
this._updateChildren(this.props.children);
}
/**
* update `this.children` according to the new list of children given
*/
_updateChildren: function(newChildren) {
const self = this;
_updateChildren(newChildren) {
const oldChildren = this.children || {};
this.children = {};
React.Children.toArray(newChildren).forEach(function(c) {
React.Children.toArray(newChildren).forEach((c) => {
if (oldChildren[c.key]) {
const old = oldChildren[c.key];
const oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left != c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
if (oldNode && oldNode.style.left !== c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => {
// special case visibility because it's nonsensical to animate an invisible element
// so we always hidden->visible pre-transition and visible->hidden after
if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') {
if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
oldNode.style.visibility = c.props.style.visibility;
}
});
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
oldNode.style.visibility = c.props.style.visibility;
}
// clone the old element with the props (and children) of the new element
// so prop updates are still received by the children.
self.children[c.key] = React.cloneElement(old, c.props, c.props.children);
this.children[c.key] = React.cloneElement(old, c.props, c.props.children);
} else {
// new element. If we have a startStyle, use that as the style and go through
// the enter animations
const newProps = {};
const restingStyle = c.props.style;
const startStyles = self.props.startStyles;
const startStyles = this.props.startStyles;
if (startStyles.length > 0) {
const startStyle = startStyles[0];
newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
}
newProps.ref = ((n) => self._collectNode(
newProps.ref = ((n) => this._collectNode(
c.key, n, restingStyle,
));
self.children[c.key] = React.cloneElement(c, newProps);
this.children[c.key] = React.cloneElement(c, newProps);
}
});
},
}
/**
* called when a child element is mounted/unmounted
*
* @param {string} k key of the child
* @param {null|Object} node On mount: React node. On unmount: null
* @param {Object} restingStyle final style
*/
_collectNode: function(k, node, restingStyle) {
_collectNode(k, node, restingStyle) {
if (
node &&
this.nodes[k] === undefined &&
@ -125,12 +111,12 @@ module.exports = createReactClass({
// and then we animate to the resting state
Velocity(domNode, restingStyle,
transitionOpts[i-1])
.then(() => {
// once we've reached the resting state, hide the element if
// appropriate
domNode.style.visibility = restingStyle.visibility;
});
transitionOpts[i-1])
.then(() => {
// once we've reached the resting state, hide the element if
// appropriate
domNode.style.visibility = restingStyle.visibility;
});
/*
console.log("enter:",
@ -153,13 +139,13 @@ module.exports = createReactClass({
if (domNode) Velocity.Utilities.removeData(domNode);
}
this.nodes[k] = node;
},
}
render: function() {
render() {
return (
<span>
{ Object.values(this.children) }
</span>
);
},
});
}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import FileSaver from 'file-saver';
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
@ -44,6 +44,9 @@ export default createReactClass({
componentWillMount: function() {
this._unmounted = false;
this._passphrase1 = createRef();
this._passphrase2 = createRef();
},
componentWillUnmount: function() {
@ -53,8 +56,8 @@ export default createReactClass({
_onPassphraseFormSubmit: function(ev) {
ev.preventDefault();
const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) {
const passphrase = this._passphrase1.current.value;
if (passphrase !== this._passphrase2.current.value) {
this.setState({errStr: _t('Passphrases must match')});
return false;
}
@ -148,7 +151,7 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase1' id='passphrase1'
<input ref={this._passphrase1} id='passphrase1'
autoFocus={true} size='64' type='password'
disabled={disableForm}
/>
@ -161,7 +164,7 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase2' id='passphrase2'
<input ref={this._passphrase2} id='passphrase2'
size='64' type='password'
disabled={disableForm}
/>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -56,6 +56,9 @@ export default createReactClass({
componentWillMount: function() {
this._unmounted = false;
this._file = createRef();
this._passphrase = createRef();
},
componentWillUnmount: function() {
@ -63,15 +66,15 @@ export default createReactClass({
},
_onFormChange: function(ev) {
const files = this.refs.file.files || [];
const files = this._file.current.files || [];
this.setState({
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
});
},
_onFormSubmit: function(ev) {
ev.preventDefault();
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
this._startImport(this._file.current.files[0], this._passphrase.current.value);
return false;
},
@ -146,7 +149,10 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='file' id='importFile' type='file'
<input
ref={this._file}
id='importFile'
type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
@ -159,8 +165,11 @@ export default createReactClass({
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase' id='passphrase'
size='64' type='password'
<input
ref={this._passphrase}
id='passphrase'
size='64'
type='password'
onChange={this._onFormChange}
disabled={disableForm} />
</div>

View file

@ -0,0 +1,484 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
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, {useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Key} from "../../Keyboard";
import sdk from "../../index";
import AccessibleButton from "../views/elements/AccessibleButton";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
function getOrCreateContainer() {
let container = document.getElementById(ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
}
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
// Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
export class ContextMenu extends React.Component {
static propTypes = {
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
menuWidth: PropTypes.number,
menuHeight: PropTypes.number,
chevronOffset: PropTypes.number,
chevronFace: PropTypes.string, // top, bottom, left, right or none
// Function to be called on menu close
onFinished: PropTypes.func.isRequired,
menuPaddingTop: PropTypes.number,
menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number,
zIndex: PropTypes.number,
// If true, insert an invisible screen-sized element behind the
// menu that when clicked will close it.
hasBackground: PropTypes.bool,
// on resize callback
windowResize: PropTypes.func,
catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true)
};
static defaultProps = {
hasBackground: true,
catchTab: true,
};
constructor() {
super();
this.state = {
contextMenuElem: null,
};
// persist what had focus when we got initialized so we can return it after
this.initialFocus = document.activeElement;
}
componentWillUnmount() {
// return focus to the thing which had it before us
this.initialFocus.focus();
}
collectContextMenuRect = (element) => {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
let first = element.querySelector('[role^="menuitem"]');
if (!first) {
first = element.querySelector('[tab-index]');
}
if (first) {
first.focus();
}
this.setState({
contextMenuElem: element,
});
};
onContextMenu = (e) => {
if (this.props.onFinished) {
this.props.onFinished();
e.preventDefault();
const x = e.clientX;
const y = e.clientY;
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
// a context menu and its click-guard are up without completely rewriting how the context menus work.
setImmediate(() => {
const clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(
'contextmenu', true, true, window, 0,
0, 0, x, y, false, false,
false, false, 0, null,
);
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
});
}
};
_onMoveFocus = (element, up) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
element = up ? element.lastElementChild : element.firstElementChild;
descending = true;
}
}
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
if (element) {
element.focus();
}
};
_onMoveFocusHomeEnd = (element, up) => {
let results = element.querySelectorAll('[role^="menuitem"]');
if (!results) {
results = element.querySelectorAll('[tab-index]');
}
if (results && results.length) {
if (up) {
results[0].focus();
} else {
results[results.length - 1].focus();
}
}
};
_onKeyDown = (ev) => {
let handled = true;
switch (ev.key) {
case Key.TAB:
if (!this.props.catchTab) {
handled = false;
break;
}
// fallthrough
case Key.ESCAPE:
this.props.onFinished();
break;
case Key.ARROW_UP:
this._onMoveFocus(ev.target, true);
break;
case Key.ARROW_DOWN:
this._onMoveFocus(ev.target, false);
break;
case Key.HOME:
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
break;
case Key.END:
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
break;
default:
handled = false;
}
if (handled) {
// consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault();
}
};
renderMenu(hasBackground=this.props.hasBackground) {
const position = {};
let chevronFace = null;
const props = this.props;
if (props.top) {
position.top = props.top;
} else {
position.bottom = props.bottom;
}
if (props.left) {
position.left = props.left;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
const padding = 10;
const chevronOffset = {};
if (props.chevronFace) {
chevronFace = props.chevronFace;
}
const hasChevron = chevronFace && chevronFace !== "none";
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
const target = position.top;
// By default, no adjustment is made
let adjusted = target;
// If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window.
if (contextMenuRect) {
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
}
position.top = adjusted;
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
}
let chevron;
if (hasChevron) {
chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
}
const menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': !hasChevron && position.left,
'mx_ContextualMenu_right': !hasChevron && position.right,
'mx_ContextualMenu_top': !hasChevron && position.top,
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
});
const menuStyle = {};
if (props.menuWidth) {
menuStyle.width = props.menuWidth;
}
if (props.menuHeight) {
menuStyle.height = props.menuHeight;
}
if (!isNaN(Number(props.menuPaddingTop))) {
menuStyle["paddingTop"] = props.menuPaddingTop;
}
if (!isNaN(Number(props.menuPaddingLeft))) {
menuStyle["paddingLeft"] = props.menuPaddingLeft;
}
if (!isNaN(Number(props.menuPaddingBottom))) {
menuStyle["paddingBottom"] = props.menuPaddingBottom;
}
if (!isNaN(Number(props.menuPaddingRight))) {
menuStyle["paddingRight"] = props.menuPaddingRight;
}
const wrapperStyle = {};
if (!isNaN(Number(props.zIndex))) {
menuStyle["zIndex"] = props.zIndex + 1;
wrapperStyle["zIndex"] = props.zIndex;
}
let background;
if (hasBackground) {
background = (
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
);
}
return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
{ chevron }
{ props.children }
</div>
{ background }
</div>
);
}
render() {
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
}
}
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} title={label} aria-label={label} aria-haspopup={true} aria-expanded={isExpanded}>
{ children }
</AccessibleButton>
);
};
ContextMenuButton.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string.isRequired,
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
};
// Semantic component for representing a role=menuitem
export const MenuItem = ({children, label, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItem.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Semantic component for representing a role=group for grouping menu radios/checkboxes
export const MenuGroup = ({children, label, ...props}) => {
return <div {...props} role="group" aria-label={label}>
{ children }
</div>;
};
MenuGroup.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string.isRequired,
className: PropTypes.string, // optional
};
// Semantic component for representing a role=menuitemcheckbox
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItemCheckbox.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
active: PropTypes.bool.isRequired,
disabled: PropTypes.bool, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Semantic component for representing a role=menuitemradio
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItemRadio.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
active: PropTypes.bool.isRequired,
disabled: PropTypes.bool, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect, chevronOffset=12) => {
const left = elementRect.right + window.pageXOffset + 3;
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
return {left, top, chevronOffset};
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
export const aboveLeftOf = (elementRect, chevronFace="none") => {
const menuOptions = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset;
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < window.innerHeight / 2) {
menuOptions.top = buttonBottom;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
return menuOptions;
};
export const useContextMenu = () => {
const button = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const open = () => {
setIsOpen(true);
};
const close = () => {
setIsOpen(false);
};
return [isOpen, button, open, close, setIsOpen];
};
export default class LegacyContextMenu extends ContextMenu {
render() {
return this.renderMenu(false);
}
}
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
export function createMenu(ElementClass, props) {
const onFinished = function(...args) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
if (props && props.onFinished) {
props.onFinished.apply(null, args);
}
};
const menu = <LegacyContextMenu
{...props}
onFinished={onFinished} // eslint-disable-line react/jsx-no-bind
windowResize={onFinished} // eslint-disable-line react/jsx-no-bind
>
<ElementClass {...props} onFinished={onFinished} />
</LegacyContextMenu>;
ReactDOM.render(menu, getOrCreateContainer());
return {close: onFinished};
}

View file

@ -1,253 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {focusCapturedRef} from "../../utils/Accessibility";
import {KeyCode} from "../../Keyboard";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
function getOrCreateContainer() {
let container = document.getElementById(ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
}
export default class ContextualMenu extends React.Component {
propTypes: {
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
menuWidth: PropTypes.number,
menuHeight: PropTypes.number,
chevronOffset: PropTypes.number,
chevronFace: PropTypes.string, // top, bottom, left, right or none
// Function to be called on menu close
onFinished: PropTypes.func,
menuPaddingTop: PropTypes.number,
menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number,
zIndex: PropTypes.number,
// If true, insert an invisible screen-sized element behind the
// menu that when clicked will close it.
hasBackground: PropTypes.bool,
// The component to render as the context menu
elementClass: PropTypes.element.isRequired,
// on resize callback
windowResize: PropTypes.func,
// method to close menu
closeMenu: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
contextMenuRect: null,
};
this.onContextMenu = this.onContextMenu.bind(this);
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
}
collectContextMenuRect(element) {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
// For screen readers to find the thing
focusCapturedRef(element);
this.setState({
contextMenuRect: element.getBoundingClientRect(),
});
}
onContextMenu(e) {
if (this.props.closeMenu) {
this.props.closeMenu();
e.preventDefault();
const x = e.clientX;
const y = e.clientY;
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
// a context menu and its click-guard are up without completely rewriting how the context menus work.
setImmediate(() => {
const clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(
'contextmenu', true, true, window, 0,
0, 0, x, y, false, false,
false, false, 0, null,
);
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
});
}
}
_onKeyDown = (ev) => {
if (ev.keyCode === KeyCode.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.closeMenu();
}
};
render() {
const position = {};
let chevronFace = null;
const props = this.props;
if (props.top) {
position.top = props.top;
} else {
position.bottom = props.bottom;
}
if (props.left) {
position.left = props.left;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const contextMenuRect = this.state.contextMenuRect || null;
const padding = 10;
const chevronOffset = {};
if (props.chevronFace) {
chevronFace = props.chevronFace;
}
const hasChevron = chevronFace && chevronFace !== "none";
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
const target = position.top;
// By default, no adjustment is made
let adjusted = target;
// If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window.
if (contextMenuRect) {
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
}
position.top = adjusted;
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
}
const chevron = hasChevron ?
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
undefined;
const className = 'mx_ContextualMenu_wrapper';
const menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': !hasChevron && position.left,
'mx_ContextualMenu_right': !hasChevron && position.right,
'mx_ContextualMenu_top': !hasChevron && position.top,
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
});
const menuStyle = {};
if (props.menuWidth) {
menuStyle.width = props.menuWidth;
}
if (props.menuHeight) {
menuStyle.height = props.menuHeight;
}
if (!isNaN(Number(props.menuPaddingTop))) {
menuStyle["paddingTop"] = props.menuPaddingTop;
}
if (!isNaN(Number(props.menuPaddingLeft))) {
menuStyle["paddingLeft"] = props.menuPaddingLeft;
}
if (!isNaN(Number(props.menuPaddingBottom))) {
menuStyle["paddingBottom"] = props.menuPaddingBottom;
}
if (!isNaN(Number(props.menuPaddingRight))) {
menuStyle["paddingRight"] = props.menuPaddingRight;
}
const wrapperStyle = {};
if (!isNaN(Number(props.zIndex))) {
menuStyle["zIndex"] = props.zIndex + 1;
wrapperStyle["zIndex"] = props.zIndex;
}
const ElementClass = props.elementClass;
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
return <div className={className} style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}>
{ chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
</div>
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
</div>;
}
}
export function createMenu(ElementClass, props, hasBackground=true) {
const closeMenu = function(...args) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
if (props && props.onFinished) {
props.onFinished.apply(null, args);
}
};
// We only reference closeMenu once per call to createMenu
const menu = <ContextualMenu
hasBackground={hasBackground}
{...props}
elementClass={ElementClass}
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
/>;
ReactDOM.render(menu, getOrCreateContainer());
return {close: closeMenu};
}

View file

@ -1214,25 +1214,25 @@ export default createReactClass({
const EditableText = sdk.getComponent("elements.EditableText");
nameNode = <EditableText ref="nameEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t('Community Name')}
blurToCancel={false}
initialValue={this.state.profileForm.name}
onValueChanged={this._onNameChange}
tabIndex="0"
dir="auto" />;
nameNode = <EditableText
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t('Community Name')}
blurToCancel={false}
initialValue={this.state.profileForm.name}
onValueChanged={this._onNameChange}
tabIndex="0"
dir="auto" />;
shortDescNode = <EditableText ref="descriptionEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t("Description")}
blurToCancel={false}
initialValue={this.state.profileForm.short_description}
onValueChanged={this._onShortDescChange}
tabIndex="0"
dir="auto" />;
shortDescNode = <EditableText
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t("Description")}
blurToCancel={false}
initialValue={this.state.profileForm.short_description}
onValueChanged={this._onShortDescChange}
tabIndex="0"
dir="auto" />;
} else {
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;

View file

@ -18,7 +18,7 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
@ -129,6 +129,8 @@ export default createReactClass({
this._authLogic.poll();
}, 2000);
}
this._stageComponent = createRef();
},
componentWillUnmount: function() {
@ -153,8 +155,8 @@ export default createReactClass({
},
tryContinue: function() {
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
this.refs.stageComponent.tryContinue();
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
this._stageComponent.current.tryContinue();
}
},
@ -192,8 +194,8 @@ export default createReactClass({
},
_setFocus: function() {
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
this.refs.stageComponent.focus();
if (this._stageComponent.current && this._stageComponent.current.focus) {
this._stageComponent.current.focus();
}
},
@ -214,7 +216,8 @@ export default createReactClass({
const StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent ref="stageComponent"
<StageComponent
ref={this._stageComponent}
loginType={stage}
matrixClient={this.props.matrixClient}
authSessionId={this._authLogic.getSessionId()}

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import { MatrixClient } from 'matrix-js-sdk';
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
@ -129,6 +129,8 @@ const LoggedInView = createReactClass({
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
fixupColorFonts();
this._roomView = createRef();
},
componentDidUpdate(prevProps) {
@ -165,10 +167,10 @@ const LoggedInView = createReactClass({
},
canResetTimelineInRoom: function(roomId) {
if (!this.refs.roomView) {
if (!this._roomView.current) {
return true;
}
return this.refs.roomView.canResetTimeline();
return this._roomView.current.canResetTimeline();
},
_setStateFromSessionStore() {
@ -401,6 +403,11 @@ const LoggedInView = createReactClass({
const isClickShortcut = ev.target !== document.body &&
(ev.key === Key.SPACE || ev.key === Key.ENTER);
// Do not capture the context menu key to improve keyboard accessibility
if (ev.key === Key.CONTEXT_MENU) {
return;
}
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
// If using Slate, consume the Backspace without first focusing as it causes an implosion
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
@ -423,8 +430,8 @@ const LoggedInView = createReactClass({
* @param {Object} ev The key event
*/
_onScrollKeyPressed: function(ev) {
if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev);
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
},
@ -538,7 +545,7 @@ const LoggedInView = createReactClass({
switch (this.props.page_type) {
case PageTypes.RoomView:
pageElement = <RoomView
ref='roomView'
ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}

View file

@ -24,6 +24,8 @@ import Matrix from "matrix-js-sdk";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible';
// what-input helps improve keyboard accessibility
import 'what-input';
import Analytics from "../../Analytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -159,6 +159,10 @@ export default class MessagePanel extends React.Component {
SettingsStore.getValue("showHiddenEventsInTimeline");
this._isMounted = false;
this._readMarkerNode = createRef();
this._whoIsTyping = createRef();
this._scrollPanel = createRef();
}
componentDidMount() {
@ -191,8 +195,7 @@ export default class MessagePanel extends React.Component {
/* return true if the content is fully scrolled down right now; else false.
*/
isAtBottom() {
return this.refs.scrollPanel
&& this.refs.scrollPanel.isAtBottom();
return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@ -201,8 +204,7 @@ export default class MessagePanel extends React.Component {
* returns null if we are not mounted.
*/
getScrollState() {
if (!this.refs.scrollPanel) { return null; }
return this.refs.scrollPanel.getScrollState();
return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
}
// returns one of:
@ -212,8 +214,8 @@ export default class MessagePanel extends React.Component {
// 0: read marker is within the window
// +1: read marker is below the window
getReadMarkerPosition() {
const readMarker = this.refs.readMarkerNode;
const messageWrapper = this.refs.scrollPanel;
const readMarker = this._readMarkerNode.current;
const messageWrapper = this._scrollPanel.current;
if (!readMarker || !messageWrapper) {
return null;
@ -236,16 +238,16 @@ export default class MessagePanel extends React.Component {
/* jump to the top of the content.
*/
scrollToTop() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToTop();
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToTop();
}
}
/* jump to the bottom of the content.
*/
scrollToBottom() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToBottom();
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToBottom();
}
}
@ -255,8 +257,8 @@ export default class MessagePanel extends React.Component {
* @param {number} mult: -1 to page up, +1 to page down
*/
scrollRelative(mult) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollRelative(mult);
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollRelative(mult);
}
}
@ -266,8 +268,8 @@ export default class MessagePanel extends React.Component {
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
handleScrollKey(ev) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.handleScrollKey(ev);
if (this._scrollPanel.current) {
this._scrollPanel.current.handleScrollKey(ev);
}
}
@ -282,8 +284,8 @@ export default class MessagePanel extends React.Component {
* defaults to 0.
*/
scrollToEvent(eventId, pixelOffset, offsetBase) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
if (this._scrollPanel.current) {
this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
}
}
@ -297,8 +299,8 @@ export default class MessagePanel extends React.Component {
/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.checkFillState();
if (this._scrollPanel.current) {
this._scrollPanel.current.checkFillState();
}
}
@ -345,7 +347,7 @@ export default class MessagePanel extends React.Component {
}
return (
<li key={"readMarker_"+eventId} ref="readMarkerNode"
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
@ -693,6 +695,10 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId];
// Dev note: `this._isUnmounting.bind(this)` is important - it ensures that
// the function is run in the context of this class and not EventTile, therefore
// ensuring the right `this._mounted` variable is used by read receipts (which
// don't update their position if we, the MessagePanel, is unmounting).
ret.push(
<li key={eventId}
ref={this._collectEventNode.bind(this, eventId)}
@ -707,7 +713,7 @@ export default class MessagePanel extends React.Component {
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
checkUnmounting={this._isUnmounting.bind(this)}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
@ -825,14 +831,14 @@ export default class MessagePanel extends React.Component {
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
_onHeightChanged = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
_onTypingShown = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
@ -841,7 +847,7 @@ export default class MessagePanel extends React.Component {
};
_onTypingHidden = () => {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
@ -854,11 +860,11 @@ export default class MessagePanel extends React.Component {
};
updateTimelineMinHeight() {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
const isAtBottom = scrollPanel.isAtBottom();
const whoIsTyping = this.refs.whoIsTyping;
const whoIsTyping = this._whoIsTyping.current;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
@ -871,7 +877,7 @@ export default class MessagePanel extends React.Component {
}
onTimelineReset() {
const scrollPanel = this.refs.scrollPanel;
const scrollPanel = this._scrollPanel.current;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
@ -905,19 +911,22 @@ export default class MessagePanel extends React.Component {
room={this.props.room}
onShown={this._onTypingShown}
onHidden={this._onTypingHidden}
ref="whoIsTyping" />
ref={this._whoIsTyping} />
);
}
return (
<ScrollPanel ref="scrollPanel" className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}>
<ScrollPanel
ref={this._scrollPanel}
className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}
>
{ topSpinner }
{ this._getEventTiles() }
{ whoIsTyping }

View file

@ -572,7 +572,7 @@ module.exports = createReactClass({
if (rows.length === 0 && !this.state.loading) {
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
} else {
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
scrollpanel_content = <table className="mx_RoomDirectory_table">
<tbody>
{ rows }
</tbody>

View file

@ -18,7 +18,6 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
@ -36,12 +35,11 @@ import {_t} from "../../languageHandler";
// turn this on for drop & drag console debugging galore
const debug = false;
const RoomSubList = createReactClass({
displayName: 'RoomSubList',
export default class RoomSubList extends React.PureComponent {
static displayName = 'RoomSubList';
static debug = debug;
debug: debug,
propTypes: {
static propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
@ -59,10 +57,26 @@ const RoomSubList = createReactClass({
incomingCall: PropTypes.object,
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
forceExpand: PropTypes.bool,
},
};
getInitialState: function() {
static defaultProps = {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
static getDerivedStateFromProps(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
}
constructor(props) {
super(props);
this.state = {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
@ -71,47 +85,33 @@ const RoomSubList = createReactClass({
// we have to store the length of the list here so we can see if it's changed or not...
listLength: null,
};
},
getDefaultProps: function() {
return {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
},
componentDidMount: function() {
this._header = createRef();
this._subList = createRef();
this._scroller = createRef();
this._headerButton = createRef();
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
},
}
statics: {
getDerivedStateFromProps: function(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
},
},
componentWillUnmount: function() {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
},
}
// The header is collapsible if it is hidden or not stuck
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
isCollapsibleOnClick: function() {
const stuck = this.refs.header.dataset.stuck;
isCollapsibleOnClick() {
const stuck = this._header.current.dataset.stuck;
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
return true;
} else {
return false;
}
},
}
onAction: function(payload) {
onAction = (payload) => {
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
@ -124,9 +124,9 @@ const RoomSubList = createReactClass({
) {
this.forceUpdate();
}
},
};
onClick: function(ev) {
onClick = (ev) => {
if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
@ -135,11 +135,11 @@ const RoomSubList = createReactClass({
});
} else {
// The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
}
},
};
onHeaderKeyDown: function(ev) {
onHeaderKeyDown = (ev) => {
switch (ev.key) {
case Key.TAB:
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
@ -159,7 +159,7 @@ const RoomSubList = createReactClass({
this.onClick();
} else if (!this.props.forceExpand) {
// sublist is expanded, go to first room
const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile");
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
if (element) {
element.focus();
}
@ -167,9 +167,9 @@ const RoomSubList = createReactClass({
break;
}
}
},
};
onKeyDown: function(ev) {
onKeyDown = (ev) => {
switch (ev.key) {
// On ARROW_LEFT go to the sublist header
case Key.ARROW_LEFT:
@ -180,24 +180,24 @@ const RoomSubList = createReactClass({
case Key.ARROW_RIGHT:
ev.stopPropagation();
}
},
};
onRoomTileClick(roomId, ev) {
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
});
},
};
_updateSubListCount: function() {
_updateSubListCount = () => {
// Force an update by setting the state to the current state
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
// method is honoured
this.setState(this.state);
},
};
makeRoomTile: function(room) {
makeRoomTile = (room) => {
return <RoomTile
room={room}
roomSubList={this}
@ -212,9 +212,9 @@ const RoomSubList = createReactClass({
incomingCall={null}
onClick={this.onRoomTileClick}
/>;
},
};
_onNotifBadgeClick: function(e) {
_onNotifBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
@ -225,9 +225,9 @@ const RoomSubList = createReactClass({
room_id: room.roomId,
});
}
},
};
_onInviteBadgeClick: function(e) {
_onInviteBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
@ -247,14 +247,14 @@ const RoomSubList = createReactClass({
});
}
}
},
};
onAddRoom: function(e) {
onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
},
};
_getHeaderJsx: function(isCollapsed) {
_getHeaderJsx(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const subListNotifications = !this.props.isInvite ?
@ -328,7 +328,7 @@ const RoomSubList = createReactClass({
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref="header" onKeyDown={this.onHeaderKeyDown}>
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onClick={this.onClick}
className="mx_RoomSubList_label"
@ -346,36 +346,36 @@ const RoomSubList = createReactClass({
{ addRoomButton }
</div>
);
},
}
checkOverflow: function() {
if (this.refs.scroller) {
this.refs.scroller.checkOverflow();
checkOverflow = () => {
if (this._scroller.current) {
this._scroller.current.checkOverflow();
}
},
};
setHeight: function(height) {
if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`;
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = `${height}px`;
}
this._updateLazyRenderHeight(height);
},
};
_updateLazyRenderHeight: function(height) {
_updateLazyRenderHeight(height) {
this.setState({scrollerHeight: height});
},
}
_onScroll: function() {
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
},
_onScroll = () => {
this.setState({scrollTop: this._scroller.current.getScrollTop()});
};
_canUseLazyListRendering() {
// for now disable lazy rendering as they are already rendered tiles
// not rooms like props.list we pass to LazyRenderList
return !this.props.extraTiles || !this.props.extraTiles.length;
},
}
render: function() {
render() {
const len = this.props.list.length + this.props.extraTiles.length;
const isCollapsed = this.state.hidden && !this.props.forceExpand;
@ -391,7 +391,7 @@ const RoomSubList = createReactClass({
// no body
} else if (this._canUseLazyListRendering()) {
content = (
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
@ -404,7 +404,7 @@ const RoomSubList = createReactClass({
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
const tiles = roomTiles.concat(this.props.extraTiles);
content = (
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
{ tiles }
</IndicatorScrollbar>
);
@ -418,7 +418,7 @@ const RoomSubList = createReactClass({
return (
<div
ref="subList"
ref={this._subList}
className={subListClasses}
role="group"
aria-label={this.props.label}
@ -428,7 +428,5 @@ const RoomSubList = createReactClass({
{ content }
</div>
);
},
});
module.exports = RoomSubList;
}
}

View file

@ -23,7 +23,7 @@ limitations under the License.
import shouldHideEvent from '../../shouldHideEvent';
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
@ -207,6 +207,9 @@ module.exports = createReactClass({
this._onCiderUpdated();
this._ciderWatcherRef = SettingsStore.watchSetting(
"useCiderComposer", null, this._onCiderUpdated);
this._roomView = createRef();
this._searchResultsPanel = createRef();
},
_onCiderUpdated: function() {
@ -459,8 +462,8 @@ module.exports = createReactClass({
},
componentDidUpdate: function() {
if (this.refs.roomView) {
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
if (this._roomView.current) {
const roomView = ReactDOM.findDOMNode(this._roomView.current);
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
@ -474,10 +477,10 @@ module.exports = createReactClass({
// in render() prevents the ref from being set on first mount, so we try and
// catch the messagePanel when it does mount. Because we only want the ref once,
// we use a boolean flag to avoid duplicate work.
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) {
this.setState({
atEndOfLiveTimelineInit: true,
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(),
});
}
},
@ -499,12 +502,12 @@ module.exports = createReactClass({
// stop tracking room changes to format permalinks
this._stopAllPermalinkCreators();
if (this.refs.roomView) {
if (this._roomView.current) {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
const roomView = ReactDOM.findDOMNode(this._roomView.current);
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
@ -701,10 +704,10 @@ module.exports = createReactClass({
},
canResetTimeline: function() {
if (!this.refs.messagePanel) {
if (!this._messagePanel) {
return true;
}
return this.refs.messagePanel.canResetTimeline();
return this._messagePanel.canResetTimeline();
},
// called when state.room is first initialised (either at initial load,
@ -1046,7 +1049,7 @@ module.exports = createReactClass({
},
onMessageListScroll: function(ev) {
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
if (this._messagePanel.isAtEndOfLiveTimeline()) {
this.setState({
numUnreadMessages: 0,
atEndOfLiveTimeline: true,
@ -1119,8 +1122,8 @@ module.exports = createReactClass({
// if we already have a search panel, we need to tell it to forget
// about its scroll state.
if (this.refs.searchResultsPanel) {
this.refs.searchResultsPanel.resetScrollState();
if (this._searchResultsPanel.current) {
this._searchResultsPanel.current.resetScrollState();
}
// make sure that we don't end up showing results from
@ -1225,7 +1228,7 @@ module.exports = createReactClass({
// once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets.
const onHeightChanged = () => {
const scrollPanel = this.refs.searchResultsPanel;
const scrollPanel = this._searchResultsPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
@ -1370,28 +1373,28 @@ module.exports = createReactClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this.refs.messagePanel.jumpToLiveTimeline();
this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'});
},
// jump up to wherever our read marker is
jumpToReadMarker: function() {
this.refs.messagePanel.jumpToReadMarker();
this._messagePanel.jumpToReadMarker();
},
// update the read marker to match the read-receipt
forgetReadMarker: function(ev) {
ev.stopPropagation();
this.refs.messagePanel.forgetReadMarker();
this._messagePanel.forgetReadMarker();
},
// decide whether or not the top 'unread messages' bar should be shown
_updateTopUnreadMessagesBar: function() {
if (!this.refs.messagePanel) {
if (!this._messagePanel) {
return;
}
const showBar = this.refs.messagePanel.canJumpToReadMarker();
const showBar = this._messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar});
}
@ -1401,7 +1404,7 @@ module.exports = createReactClass({
// restored when we switch back to it.
//
_getScrollState: function() {
const messagePanel = this.refs.messagePanel;
const messagePanel = this._messagePanel;
if (!messagePanel) return null;
// if we're following the live timeline, we want to return null; that
@ -1506,10 +1509,10 @@ module.exports = createReactClass({
*/
handleScrollKey: function(ev) {
let panel;
if (this.refs.searchResultsPanel) {
panel = this.refs.searchResultsPanel;
} else if (this.refs.messagePanel) {
panel = this.refs.messagePanel;
if (this._searchResultsPanel.current) {
panel = this._searchResultsPanel.current;
} else if (this._messagePanel) {
panel = this._messagePanel;
}
if (panel) {
@ -1530,7 +1533,7 @@ module.exports = createReactClass({
// this has to be a proper method rather than an unnamed function,
// otherwise react calls it with null on each update.
_gatherTimelinePanelRef: function(r) {
this.refs.messagePanel = r;
this._messagePanel = r;
if (r) {
console.log("updateTint from RoomView._gatherTimelinePanelRef");
this.updateTint();
@ -1719,7 +1722,7 @@ module.exports = createReactClass({
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
aux = <SearchBar searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
@ -1775,7 +1778,7 @@ module.exports = createReactClass({
}
const auxPanel = (
<AuxPanel ref="auxPanel" room={this.state.room}
<AuxPanel room={this.state.room}
fullHeight={false}
userId={MatrixClientPeg.get().credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
@ -1875,7 +1878,7 @@ module.exports = createReactClass({
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
} else {
searchResultsPanel = (
<ScrollPanel ref="searchResultsPanel"
<ScrollPanel ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
onFillRequest={this.onSearchResultsFillRequest}
resizeNotifier={this.props.resizeNotifier}
@ -1898,7 +1901,8 @@ module.exports = createReactClass({
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
<TimelinePanel
ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
manageReadReceipts={!this.state.isPeeking}
@ -1952,9 +1956,11 @@ module.exports = createReactClass({
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
<ErrorBoundary>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
<RoomHeader
room={this.state.room}
searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
collapsedRhs={collapsedRhs}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, {createRef} from "react";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard';
@ -166,6 +166,8 @@ module.exports = createReactClass({
}
this.resetScrollState();
this._itemlist = createRef();
},
componentDidMount: function() {
@ -328,7 +330,7 @@ module.exports = createReactClass({
this._isFilling = true;
}
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const firstTile = itemlist && itemlist.firstElementChild;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@ -373,7 +375,7 @@ module.exports = createReactClass({
const origExcessHeight = excessHeight;
const tiles = this.refs.itemlist.children;
const tiles = this._itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@ -602,7 +604,7 @@ module.exports = createReactClass({
const scrollNode = this._getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const messages = itemlist.children;
let node = null;
@ -644,7 +646,7 @@ module.exports = createReactClass({
const sn = this._getScrollNode();
sn.scrollTop = sn.scrollHeight;
} else if (scrollState.trackedScrollToken) {
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const trackedNode = this._getTrackedNode();
if (trackedNode) {
const newBottomOffset = this._topFromBottom(trackedNode);
@ -682,7 +684,7 @@ module.exports = createReactClass({
}
const sn = this._getScrollNode();
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const contentHeight = this._getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
@ -724,7 +726,7 @@ module.exports = createReactClass({
if (!trackedNode || !trackedNode.parentElement) {
let node;
const messages = this.refs.itemlist.children;
const messages = this._itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
@ -756,7 +758,7 @@ module.exports = createReactClass({
},
_getMessagesHeight() {
const itemlist = this.refs.itemlist;
const itemlist = this._itemlist.current;
const lastNode = itemlist.lastElementChild;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
@ -765,7 +767,7 @@ module.exports = createReactClass({
},
_topFromBottom(node) {
return this.refs.itemlist.clientHeight - node.offsetTop;
return this._itemlist.current.clientHeight - node.offsetTop;
},
/* get the DOM node which has the scrollTop property we care about for our
@ -797,7 +799,7 @@ module.exports = createReactClass({
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
preventShrinking: function() {
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
@ -824,7 +826,7 @@ module.exports = createReactClass({
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking: function() {
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@ -843,7 +845,7 @@ module.exports = createReactClass({
if (this.preventShrinkingState) {
const sn = this._getScrollNode();
const scrollState = this.scrollState;
const messageList = this.refs.itemlist;
const messageList = this._itemlist.current;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@ -879,7 +881,7 @@ module.exports = createReactClass({
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
{ this.props.children }
</ol>
</div>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard';
@ -53,6 +53,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._search = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
@ -66,26 +70,26 @@ module.exports = createReactClass({
switch (payload.action) {
case 'view_room':
if (this.refs.search && payload.clear_search) {
if (this._search.current && payload.clear_search) {
this._clearSearch();
}
break;
case 'focus_room_filter':
if (this.refs.search) {
this.refs.search.focus();
if (this._search.current) {
this._search.current.focus();
}
break;
}
},
onChange: function() {
if (!this.refs.search) return;
this.setState({ searchTerm: this.refs.search.value });
if (!this._search.current) return;
this.setState({ searchTerm: this._search.current.value });
this.onSearch();
},
onSearch: throttle(function() {
this.props.onSearch(this.refs.search.value);
this.props.onSearch(this._search.current.value);
}, 200, {trailing: true, leading: true}),
_onKeyDown: function(ev) {
@ -113,7 +117,7 @@ module.exports = createReactClass({
},
_clearSearch: function(source) {
this.refs.search.value = "";
this._search.current.value = "";
this.onChange();
if (this.props.onCleared) {
this.props.onCleared(source);
@ -146,7 +150,7 @@ module.exports = createReactClass({
<input
key="searchfield"
type="text"
ref="search"
ref={this._search}
className={"mx_textinput_icon mx_textinput_search " + className}
value={ this.state.searchTerm }
onFocus={ this._onFocus }

View file

@ -19,7 +19,7 @@ limitations under the License.
import SettingsStore from "../../settings/SettingsStore";
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
@ -203,6 +203,8 @@ const TimelinePanel = createReactClass({
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this._messagePanel = createRef();
if (this.props.manageReadReceipts) {
this.updateReadReceiptOnUserActivity();
}
@ -425,8 +427,8 @@ const TimelinePanel = createReactClass({
if (payload.action === "edit_event") {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => {
if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded(
if (payload.event && this._messagePanel.current) {
this._messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({
}
this.setState(updatedState, () => {
this.refs.messagePanel.updateTimelineMinHeight();
this._messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated();
}
@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({
onRoomTimelineReset: function(room, timelineSet) {
if (timelineSet !== this.props.timelineSet) return;
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
this._loadTimeline();
}
},
canResetTimeline: function() {
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
},
onRoomRedaction: function(ev, room) {
@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({
sendReadReceipt: function() {
if (SettingsStore.getValue("lowBandwidth")) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
this._loadTimeline();
} else {
if (this.refs.messagePanel) {
this.refs.messagePanel.scrollToBottom();
if (this._messagePanel.current) {
this._messagePanel.current.scrollToBottom();
}
}
},
@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({
*/
jumpToReadMarker: function() {
if (!this.props.manageReadMarkers) return;
if (!this.refs.messagePanel) return;
if (!this._messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
@ -835,11 +837,11 @@ const TimelinePanel = createReactClass({
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
const ret = this.refs.messagePanel.getReadMarkerPosition();
const ret = this._messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
return;
}
@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({
* at the end of the live timeline.
*/
isAtEndOfLiveTimeline: function() {
return this.refs.messagePanel
&& this.refs.messagePanel.isAtBottom()
return this._messagePanel.current
&& this._messagePanel.current.isAtBottom()
&& this._timelineWindow
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
},
@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({
* returns null if we are not mounted.
*/
getScrollState: function() {
if (!this.refs.messagePanel) { return null; }
return this.refs.messagePanel.getScrollState();
if (!this._messagePanel.current) { return null; }
return this._messagePanel.current.getScrollState();
},
// returns one of:
@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({
// +1: read marker is below the window
getReadMarkerPosition: function() {
if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
if (!this._messagePanel.current) return null;
const ret = this.refs.messagePanel.getReadMarkerPosition();
const ret = this._messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@ -936,7 +938,7 @@ const TimelinePanel = createReactClass({
* We pass it down to the scroll panel.
*/
handleScrollKey: function(ev) {
if (!this.refs.messagePanel) { return; }
if (!this._messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
@ -944,7 +946,7 @@ const TimelinePanel = createReactClass({
ev.keyCode == KeyCode.END) {
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
this._messagePanel.current.handleScrollKey(ev);
}
},
@ -986,8 +988,8 @@ const TimelinePanel = createReactClass({
const onLoaded = () => {
// clear the timeline min-height when
// (re)loading the timeline
if (this.refs.messagePanel) {
this.refs.messagePanel.onTimelineReset();
if (this._messagePanel.current) {
this._messagePanel.current.onTimelineReset();
}
this._reloadEvents();
@ -1002,7 +1004,7 @@ const TimelinePanel = createReactClass({
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
if (!this.refs.messagePanel) {
if (!this._messagePanel.current) {
// this shouldn't happen - we know we're mounted because
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
@ -1012,10 +1014,10 @@ const TimelinePanel = createReactClass({
return;
}
if (eventId) {
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
this.refs.messagePanel.scrollToBottom();
this._messagePanel.current.scrollToBottom();
}
this.sendReadReceipt();
@ -1134,7 +1136,7 @@ const TimelinePanel = createReactClass({
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
const messagePanel = this.refs.messagePanel;
const messagePanel = this._messagePanel.current;
if (messagePanel === undefined) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
@ -1313,7 +1315,8 @@ const TimelinePanel = createReactClass({
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
);
return (
<MessagePanel ref="messagePanel"
<MessagePanel
ref={this._messagePanel}
room={this.props.timelineSet.room}
permalinkCreator={this.props.permalinkCreator}
hidden={this.props.hidden}

View file

@ -17,15 +17,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as ContextualMenu from './ContextualMenu';
import {TopLeftMenu} from '../views/context_menus/TopLeftMenu';
import AccessibleButton from '../views/elements/AccessibleButton';
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";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
const AVATAR_SIZE = 28;
@ -40,11 +38,8 @@ export default class TopLeftMenuButton extends React.Component {
super();
this.state = {
menuDisplayed: false,
menuFunctions: null, // should be { close: fn }
profileInfo: null,
};
this.onToggleMenu = this.onToggleMenu.bind(this);
}
async _getProfileInfo() {
@ -95,7 +90,21 @@ export default class TopLeftMenuButton extends React.Component {
}
}
openMenu = (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({ menuDisplayed: true });
};
closeMenu = () => {
this.setState({
menuDisplayed: false,
});
};
render() {
const cli = MatrixClientPeg.get().getUserId();
const name = this._getDisplayName();
let nameElement;
let chevronElement;
@ -106,14 +115,29 @@ export default class TopLeftMenuButton extends React.Component {
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
}
return (
<AccessibleButton
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._buttonRef.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.closeMenu}
>
<TopLeftMenu displayName={name} userId={cli} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className="mx_TopLeftMenuButton"
onClick={this.onToggleMenu}
onClick={this.openMenu}
inputRef={(r) => this._buttonRef = r}
aria-label={_t("Your profile")}
aria-haspopup={true}
aria-expanded={this.state.menuDisplayed}
label={_t("Your profile")}
isExpanded={this.state.menuDisplayed}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
@ -125,34 +149,9 @@ export default class TopLeftMenuButton extends React.Component {
/>
{ nameElement }
{ chevronElement }
</AccessibleButton>
);
}
</ContextMenuButton>
onToggleMenu(e) {
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;
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, menuFunctions: null });
},
});
this.setState({ menuDisplayed: true, menuFunctions });
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
@ -48,6 +48,8 @@ module.exports = createReactClass({
componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();
},
componentDidMount: function() {
@ -67,7 +69,7 @@ module.exports = createReactClass({
scriptTag.setAttribute(
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
);
this.refs.recaptchaContainer.appendChild(scriptTag);
this._recaptchaContainer.current.appendChild(scriptTag);
}
},
@ -124,11 +126,11 @@ module.exports = createReactClass({
}
return (
<div ref="recaptchaContainer">
<div ref={this._recaptchaContainer}>
<p>{_t(
"This homeserver would like to make sure you are not a robot.",
)}</p>
<div id={DIV_ID}></div>
<div id={DIV_ID} />
{ error }
</div>
);

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import url from 'url';
@ -581,6 +581,8 @@ export const FallbackAuthEntry = createReactClass({
// the popup if we open it immediately.
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this._fallbackButton = createRef();
},
componentWillUnmount: function() {
@ -591,8 +593,8 @@ export const FallbackAuthEntry = createReactClass({
},
focus: function() {
if (this.refs.fallbackButton) {
this.refs.fallbackButton.focus();
if (this._fallbackButton.current) {
this._fallbackButton.current.focus();
}
},
@ -624,7 +626,7 @@ export const FallbackAuthEntry = createReactClass({
}
return (
<div>
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import {_t} from "../../../languageHandler";
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import * as ContextualMenu from "../../structures/ContextualMenu";
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = {
@ -43,7 +43,10 @@ export default class MemberStatusMessageAvatar extends React.Component {
this.state = {
hasStatus: this.hasStatus,
menuDisplayed: false,
};
this._button = createRef();
}
componentWillMount() {
@ -86,25 +89,12 @@ export default class MemberStatusMessageAvatar extends React.Component {
});
};
_onClick = (e) => {
e.stopPropagation();
openMenu = () => {
this.setState({ menuDisplayed: true });
};
const elementRect = e.target.getBoundingClientRect();
const x = (elementRect.left + window.pageXOffset);
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
const chevronOffset = (elementRect.width - chevronWidth) / 2;
const chevronMargin = 1; // Add some spacing away from target
const y = elementRect.top + window.pageYOffset - chevronMargin;
ContextualMenu.createMenu(StatusMessageContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 226,
user: this.props.member.user,
});
closeMenu = () => {
this.setState({ menuDisplayed: false });
};
render() {
@ -124,10 +114,39 @@ export default class MemberStatusMessageAvatar extends React.Component {
"mx_MemberStatusMessageAvatar_hasStatus": this.state.hasStatus,
});
return <AccessibleButton className={classes}
element="div" onClick={this._onClick}
>
{avatar}
</AccessibleButton>;
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._button.current.getBoundingClientRect();
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
const chevronMargin = 1; // Add some spacing away from target
contextMenu = (
<ContextMenu
chevronOffset={(elementRect.width - chevronWidth) / 2}
chevronFace="bottom"
left={elementRect.left + window.pageXOffset}
top={elementRect.top + window.pageYOffset - chevronMargin}
menuWidth={226}
onFinished={this.closeMenu}
>
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className={classes}
inputRef={this._button}
onClick={this.openMenu}
isExpanded={this.state.menuDisplayed}
label={_t("User Status")}
>
{avatar}
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk';
import GroupStore from "../../../stores/GroupStore";
import {MenuItem} from "../../structures/ContextMenu";
export default class GroupInviteTileContextMenu extends React.Component {
static propTypes = {
@ -36,7 +37,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
this._onClickReject = this._onClickReject.bind(this);
}
componentWillMount() {
componentDidMount() {
this._unmounted = false;
}
@ -78,12 +79,11 @@ export default class GroupInviteTileContextMenu extends React.Component {
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div>
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ _t('Reject') }
</AccessibleButton>
</MenuItem>
</div>;
}
}

View file

@ -31,6 +31,7 @@ import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import {MenuItem} from "../../structures/ContextMenu";
function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -289,8 +290,6 @@ module.exports = createReactClass({
},
render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
@ -322,89 +321,89 @@ module.exports = createReactClass({
if (!mxEvent.isRedacted()) {
if (eventStatus === EventStatus.NOT_SENT) {
resendButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
{ _t('Resend') }
</AccessibleButton>
</MenuItem>
);
}
if (editStatus === EventStatus.NOT_SENT) {
resendEditButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
{ _t('Resend edit') }
</AccessibleButton>
</MenuItem>
);
}
if (unsentReactionsCount !== 0) {
resendReactionsButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
</AccessibleButton>
</MenuItem>
);
}
}
if (redactStatus === EventStatus.NOT_SENT) {
resendRedactionButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
{ _t('Resend removal') }
</AccessibleButton>
</MenuItem>
);
}
if (isSent && this.state.canRedact) {
redactButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
{ _t('Remove') }
</AccessibleButton>
</MenuItem>
);
}
if (allowCancel) {
cancelButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
{ _t('Cancel Sending') }
</AccessibleButton>
</MenuItem>
);
}
if (isContentActionable(mxEvent)) {
forwardButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
{ _t('Forward Message') }
</AccessibleButton>
</MenuItem>
);
if (this.state.canPin) {
pinButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</AccessibleButton>
</MenuItem>
);
}
}
const viewSourceButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
{ _t('View Source') }
</AccessibleButton>
</MenuItem>
);
if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
{ _t('View Decrypted Source') }
</AccessibleButton>
</MenuItem>
);
}
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
{ _t('Unhide Preview') }
</AccessibleButton>
</MenuItem>
);
}
}
@ -415,19 +414,19 @@ module.exports = createReactClass({
}
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
<AccessibleButton className="mx_MessageContextMenu_field">
<MenuItem className="mx_MessageContextMenu_field">
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</a>
</AccessibleButton>
</MenuItem>
);
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
quoteButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
{ _t('Quote') }
</AccessibleButton>
</MenuItem>
);
}
@ -437,7 +436,7 @@ module.exports = createReactClass({
isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
<AccessibleButton className="mx_MessageContextMenu_field">
<MenuItem className="mx_MessageContextMenu_field">
<a
href={mxEvent.event.content.external_url}
target="_blank"
@ -447,33 +446,33 @@ module.exports = createReactClass({
>
{ _t('Source URL') }
</a>
</AccessibleButton>
</MenuItem>
);
}
if (this.props.collapseReplyThread) {
collapseReplyThread = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
{ _t('Collapse Reply Thread') }
</AccessibleButton>
</MenuItem>
);
}
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</AccessibleButton>
</MenuItem>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
{ _t('Report Content') }
</AccessibleButton>
</MenuItem>
);
}

View file

@ -32,6 +32,36 @@ import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
const classes = classNames('mx_RoomTileContextMenu_tag_field', {
'mx_RoomTileContextMenu_tag_fieldSet': active,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
return (
<MenuItemCheckbox className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_tag_icon" src={src} width="15" height="15" alt="" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={srcSet} width="15" height="15" alt="" />
{ label }
</MenuItemCheckbox>
);
};
const NotifOption = ({active, onClick, src, label}) => {
const classes = classNames('mx_RoomTileContextMenu_notif_field', {
'mx_RoomTileContextMenu_notif_fieldSet': active,
});
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" alt="" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={src} width="16" height="12" alt="" />
{ label }
</MenuItemRadio>
);
};
module.exports = createReactClass({
displayName: 'RoomTileContextMenu',
@ -228,53 +258,36 @@ module.exports = createReactClass({
},
_renderNotifMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const alertMeClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
});
const allNotifsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
});
const mentionsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
});
const muteNotifsClasses = classNames({
'mx_RoomTileContextMenu_notif_field': true,
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
});
return (
<div className="mx_RoomTileContextMenu">
<div className="mx_RoomTileContextMenu_notif_picker">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" />
<div className="mx_RoomTileContextMenu" role="group" aria-label={_t("Notification settings")}>
<div className="mx_RoomTileContextMenu_notif_picker" role="presentation">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" alt="" />
</div>
<AccessibleButton className={alertMeClasses} onClick={this._onClickAlertMe}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
{ _t('All messages (noisy)') }
</AccessibleButton>
<AccessibleButton className={allNotifsClasses} onClick={this._onClickAllNotifs}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
{ _t('All messages') }
</AccessibleButton>
<AccessibleButton className={mentionsClasses} onClick={this._onClickMentions}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
{ _t('Mentions only') }
</AccessibleButton>
<AccessibleButton className={muteNotifsClasses} onClick={this._onClickMute}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
{ _t('Mute') }
</AccessibleButton>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES_LOUD}
label={_t('All messages (noisy)')}
onClick={this._onClickAlertMe}
src={require("../../../../res/img/icon-context-mute-off-copy.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES}
label={_t('All messages')}
onClick={this._onClickAllNotifs}
src={require("../../../../res/img/icon-context-mute-off.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MENTIONS_ONLY}
label={_t('Mentions only')}
onClick={this._onClickMentions}
src={require("../../../../res/img/icon-context-mute-mentions.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MUTE}
label={_t('Mute')}
onClick={this._onClickMute}
src={require("../../../../res/img/icon-context-mute.svg")}
/>
</div>
);
},
@ -290,13 +303,12 @@ module.exports = createReactClass({
},
_renderSettingsMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div>
<AccessibleButton className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" alt="" />
{ _t('Settings') }
</AccessibleButton>
</MenuItem>
</div>
);
},
@ -306,8 +318,6 @@ module.exports = createReactClass({
return null;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let leaveClickHandler = null;
let leaveText = null;
@ -329,52 +339,38 @@ module.exports = createReactClass({
return (
<div>
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ leaveText }
</AccessibleButton>
</MenuItem>
</div>
);
},
_renderRoomTagMenu: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const favouriteClasses = classNames({
'mx_RoomTileContextMenu_tag_field': true,
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
const lowPriorityClasses = classNames({
'mx_RoomTileContextMenu_tag_field': true,
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isLowPriority,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
const dmClasses = classNames({
'mx_RoomTileContextMenu_tag_field': true,
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isDirectMessage,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
return (
<div>
<AccessibleButton className={favouriteClasses} onClick={this._onClickFavourite} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
{ _t('Favourite') }
</AccessibleButton>
<AccessibleButton className={lowPriorityClasses} onClick={this._onClickLowPriority} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
{ _t('Low Priority') }
</AccessibleButton>
<AccessibleButton className={dmClasses} onClick={this._onClickDM} >
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
{ _t('Direct Chat') }
</AccessibleButton>
<RoomTagOption
active={this.state.isFavourite}
label={_t('Favourite')}
onClick={this._onClickFavourite}
src={require("../../../../res/img/icon_context_fave.svg")}
srcSet={require("../../../../res/img/icon_context_fave_on.svg")}
/>
<RoomTagOption
active={this.state.isLowPriority}
label={_t('Low Priority')}
onClick={this._onClickLowPriority}
src={require("../../../../res/img/icon_context_low.svg")}
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
/>
<RoomTagOption
active={this.state.isDirectMessage}
label={_t('Direct Chat')}
onClick={this._onClickDM}
src={require("../../../../res/img/icon_context_person.svg")}
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
/>
</div>
);
},
@ -386,11 +382,11 @@ module.exports = createReactClass({
case 'join':
return <div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" />
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
case 'invite':
@ -400,7 +396,7 @@ module.exports = createReactClass({
default:
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" />
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
}

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.
@ -16,11 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu";
export default class TagTileContextMenu extends React.Component {
static propTypes = {
@ -29,6 +31,10 @@ export default class TagTileContextMenu extends React.Component {
onFinished: PropTypes.func.isRequired,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() {
super();
@ -45,18 +51,15 @@ export default class TagTileContextMenu extends React.Component {
}
_onRemoveClick() {
dis.dispatch(TagOrderActions.removeTag(
// XXX: Context menus don't have a MatrixClient context
MatrixClientPeg.get(),
this.props.tag,
));
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
this.props.onFinished();
}
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return <div>
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick}>
<TintableSvg
className="mx_TagTileContextMenu_item_icon"
src={require("../../../../res/img/icons-groups.svg")}
@ -64,12 +67,12 @@ export default class TagTileContextMenu extends React.Component {
height="15"
/>
{ _t('View Community') }
</div>
<hr className="mx_TagTileContextMenu_separator" />
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
</MenuItem>
<hr className="mx_TagTileContextMenu_separator" role="separator" />
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick}>
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ _t('Hide') }
</div>
</MenuItem>
</div>;
}
}

View file

@ -24,7 +24,7 @@ import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from "../../../index";
import {MenuItem} from "../../structures/ContextMenu";
export class TopLeftMenu extends React.Component {
static propTypes = {
@ -58,8 +58,6 @@ export class TopLeftMenu extends React.Component {
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
@ -69,10 +67,10 @@ export class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
@ -81,40 +79,40 @@ export class TopLeftMenu extends React.Component {
let homePageItem = null;
if (this.hasHomePage()) {
homePageItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</AccessibleButton>
</MenuItem>
);
}
let signInOutItem;
if (isGuest) {
signInOutItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
<MenuItem className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
{_t("Sign in")}
</AccessibleButton>
</MenuItem>
);
} else {
signInOutItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
<MenuItem className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
{_t("Sign out")}
</AccessibleButton>
</MenuItem>
);
}
const settingsItem = (
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
</AccessibleButton>
</MenuItem>
);
return <div className="mx_TopLeftMenu" ref={this.props.containerRef}>
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
<ul className="mx_TopLeftMenu_section_withIcon">
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{signInOutItem}

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler';
import {MenuItem} from "../../structures/ContextMenu";
export default class WidgetContextMenu extends React.Component {
static propTypes = {
@ -71,50 +71,45 @@ export default class WidgetContextMenu extends React.Component {
};
render() {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const options = [];
if (this.props.onEditClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
{_t("Edit")}
</AccessibleButton>,
</MenuItem>,
);
}
if (this.props.onReloadClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
key='reload'>
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
{_t("Reload")}
</AccessibleButton>,
</MenuItem>,
);
}
if (this.props.onSnapshotClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
key='snap'>
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked} key='snap'>
{_t("Take picture")}
</AccessibleButton>,
</MenuItem>,
);
}
if (this.props.onDeleteClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
key='delete'>
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked} key='delete'>
{_t("Remove for everyone")}
</AccessibleButton>,
</MenuItem>,
);
}
// Push this last so it appears last. It's always present.
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</AccessibleButton>,
</MenuItem>,
);
// Put separators between the options

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -106,10 +106,14 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
this._textinput.current.value = this.props.value;
}
},
@ -126,8 +130,8 @@ module.exports = createReactClass({
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
if (this.refs.textinput.value !== '') {
selectedList = this._addAddressesToList([this.refs.textinput.value]);
if (this._textinput.current.value !== '') {
selectedList = this._addAddressesToList([this._textinput.current.value]);
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
@ -154,23 +158,23 @@ module.exports = createReactClass({
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
} else if (this._textinput.current.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.selectedList.length - 1)();
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
if (this.refs.textinput.value === '') {
if (this._textinput.current.value === '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addAddressesToList([this.refs.textinput.value]);
this._addAddressesToList([this._textinput.current.value]);
}
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
e.stopPropagation();
e.preventDefault();
this._addAddressesToList([this.refs.textinput.value]);
this._addAddressesToList([this._textinput.current.value]);
}
},
@ -647,7 +651,7 @@ module.exports = createReactClass({
onPaste={this._onPaste}
rows="1"
id="textinput"
ref="textinput"
ref={this._textinput}
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.getPlaceholder()}

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.
@ -21,15 +22,12 @@ import sdk from '../../../index';
import Analytics from '../../../Analytics';
import MatrixClientPeg from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import Velocity from 'velocity-animate';
import { _t } from '../../../languageHandler';
export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) {
super(props, context);
this._passwordField = null;
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
@ -78,7 +76,6 @@ export default class DeactivateAccountDialog extends React.Component {
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
Velocity(this._passwordField, "callout.shake", 300);
}
this.setState({
busy: false,
@ -181,7 +178,6 @@ export default class DeactivateAccountDialog extends React.Component {
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass}
/>
</div>

View file

@ -0,0 +1,128 @@
/*
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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
export default class RoomUpgradeWarningDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired,
targetVersion: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
this.state = {
currentVersion: room ? room.getVersion() : "1",
isPrivate,
inviteUsersToNewRoom: true,
};
}
_onContinue = () => {
this.props.onFinished({continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom});
};
_onCancel = () => {
this.props.onFinished({continue: false, invite: false});
};
_onInviteUsersToggle = (newVal) => {
this.setState({inviteUsersToNewRoom: newVal});
};
_openBugReportDialog = (e) => {
e.preventDefault();
e.stopPropagation();
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let inviteToggle = null;
if (this.state.isPrivate) {
inviteToggle = (
<LabelledToggleSwitch
value={this.state.inviteUsersToNewRoom}
onChange={this._onInviteUsersToggle}
label={_t("Automatically invite users")} />
);
}
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
return (
<BaseDialog
className='mx_RoomUpgradeWarningDialog'
hasCancel={true}
fixedWidth={false}
onFinished={this.props.onFinished}
title={title}
>
<div>
<p>
{_t(
"Upgrading a room is an advanced action and is usually recommended when a room " +
"is unstable due to bugs, missing features or security vulnerabilities.",
)}
</p>
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your Riot, please <a>report a bug</a>.",
{}, {
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},
},
)}
</p>
<p>
{_t(
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
{},
{
oldVersion: () => <code>{this.state.currentVersion}</code>,
newVersion: () => <code>{this.props.targetVersion}</code>,
},
)}
</p>
{inviteToggle}
</div>
<DialogButtons
primaryButton={_t("Upgrade")}
onPrimaryButtonClick={this._onContinue}
cancelButton={_t("Cancel")}
onCancel={this._onCancel}
/>
</BaseDialog>
);
}
}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@ -62,8 +62,13 @@ export default createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._input_value = createRef();
this._uiAuth = createRef();
},
componentDidMount: function() {
this.refs.input_value.select();
this._input_value.current.select();
this._matrixClient = MatrixClientPeg.get();
},
@ -102,8 +107,8 @@ export default createReactClass({
},
onSubmit: function(ev) {
if (this.refs.uiAuth) {
this.refs.uiAuth.tryContinue();
if (this._uiAuth.current) {
this._uiAuth.current.tryContinue();
}
this.setState({
doingUIAuth: true,
@ -215,7 +220,7 @@ export default createReactClass({
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
ref="uiAuth"
ref={this._uiAuth}
continueIsManaged={true}
/>;
}
@ -257,7 +262,7 @@ export default createReactClass({
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
<input type="text" ref={this._input_value} value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}

View file

@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextualMenu from "../../structures/ContextualMenu";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
const socials = [
{
@ -73,6 +74,8 @@ export default class ShareDialog extends React.Component {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
};
this._link = createRef();
}
static _selectText(target) {
@ -93,7 +96,7 @@ export default class ShareDialog extends React.Component {
onCopyClick(e) {
e.preventDefault();
ShareDialog._selectText(this.refs.link);
ShareDialog._selectText(this._link.current);
let successful;
try {
@ -102,18 +105,12 @@ export default class ShareDialog extends React.Component {
console.error('Failed to copy: ', err);
}
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10,
left: x,
top: y,
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false);
});
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
}
@ -200,7 +197,7 @@ export default class ShareDialog extends React.Component {
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a ref="link"
<a ref={this._link}
href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@ -42,15 +42,19 @@ export default createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
this._textinput.current.value = this.props.value;
}
},
onOk: function() {
this.props.onFinished(true, this.refs.textinput.value);
this.props.onFinished(true, this._textinput.current.value);
},
onCancel: function() {
@ -70,7 +74,13 @@ export default createReactClass({
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
<input
id="textinput"
ref={this._textinput}
className="mx_TextInputDialog_input"
defaultValue={this.props.value}
autoFocus={this.props.focus}
size="64" />
</div>
</div>
</form>

View file

@ -18,7 +18,7 @@ limitations under the License.
import url from 'url';
import qs from 'querystring';
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import WidgetMessaging from '../../../WidgetMessaging';
@ -35,7 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {createMenu} from "../../structures/ContextualMenu";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
@ -62,6 +62,10 @@ export default class AppTile extends React.Component {
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
this._contextMenuButton = createRef();
this._appFrame = createRef();
this._menu_bar = createRef();
}
/**
@ -89,6 +93,7 @@ export default class AppTile extends React.Component {
error: null,
deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
menuDisplayed: false,
};
}
@ -334,14 +339,14 @@ export default class AppTile extends React.Component {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this.refs.appFrame) {
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this.refs.appFrame.src = 'about:blank';
this._appFrame.current.src = 'about:blank';
}
WidgetUtils.setRoomWidget(
@ -386,7 +391,7 @@ export default class AppTile extends React.Component {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
@ -493,7 +498,7 @@ export default class AppTile extends React.Component {
ev.preventDefault();
// Ignore clicks on menu bar children
if (ev.target !== this.refs.menu_bar) {
if (ev.target !== this._menu_bar.current) {
return;
}
@ -552,48 +557,15 @@ export default class AppTile extends React.Component {
_onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
this._appFrame.current.src = this._appFrame.current.src;
}
_getMenuOptions(ev) {
// TODO: This block of code gets copy/pasted a lot. We should make that happen less.
const menuOptions = {};
const buttonRect = ev.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const buttonLeft = buttonRect.left + window.pageXOffset;
const buttonTop = buttonRect.top + window.pageYOffset;
// Align the right edge of the menu to the left edge of the button
menuOptions.right = window.innerWidth - buttonLeft;
// Align the menu vertically on whichever side of the button has more
// space available.
if (buttonTop < window.innerHeight / 2) {
menuOptions.top = buttonTop;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
return menuOptions;
}
_onContextMenuClick = () => {
this.setState({ menuDisplayed: true });
};
_onContextMenuClick = (ev) => {
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
const menuOptions = {
...this._getMenuOptions(ev),
// A revoke handler is always required
onRevokeClicked: this._onRevokeClicked,
};
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
if (showEditButton) menuOptions.onEditClicked = this._onEditClick;
if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick;
if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick;
if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick;
createMenu(WidgetContextMenu, menuOptions);
_closeContextMenu = () => {
this.setState({ menuDisplayed: false });
};
render() {
@ -601,7 +573,7 @@ export default class AppTile extends React.Component {
// Don't render widget if it is in the process of being deleted
if (this.state.deleting) {
return <div></div>;
return <div />;
}
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
@ -656,7 +628,7 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement }
<iframe
allow={iframeFeatures}
ref="appFrame"
ref={this._appFrame}
src={this._getSafeUrl()}
allowFullScreen={true}
sandbox={sandboxFlags}
@ -697,10 +669,34 @@ export default class AppTile extends React.Component {
mx_AppTileMenuBar_expanded: this.props.show,
});
return (
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
contextMenu = (
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
<WidgetContextMenu
onRevokeClicked={this._onRevokeClicked}
onEditClicked={showEditButton ? this._onEditClick : undefined}
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
onSnapshotClicked={showPictureSnapshotButton ? this._onSnapshotClick : undefined}
onReloadClicked={this.props.showReload ? this._onReloadWidgetClick : undefined}
onFinished={this._closeContextMenu}
/>
</ContextMenu>
);
}
return <React.Fragment>
<div className={appTileClass} id={this.props.id}>
{ this.props.showMenubar &&
<div ref="menu_bar" className={menuBarClasses} onClick={this.onClickMenuBar}>
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
{ /* Minimise widget */ }
{ showMinimiseButton && <AccessibleButton
@ -725,20 +721,24 @@ export default class AppTile extends React.Component {
onClick={this._onPopoutWidgetClick}
/> }
{ /* Context menu */ }
{ <AccessibleButton
{ <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
title={_t('More options')}
label={_t('More options')}
isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton}
onClick={this._onContextMenuClick}
/> }
</span>
</div> }
{ appTileBody }
</div>
);
{ contextMenu }
</React.Fragment>;
}
}
AppTile.displayName ='AppTile';
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {Key} from "../../../Keyboard";
@ -65,7 +65,7 @@ module.exports = createReactClass({
componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this.refs.editable_div) {
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
}
@ -76,24 +76,27 @@ module.exports = createReactClass({
// as React doesn't play nice with contentEditable.
this.value = '';
this.placeholder = false;
this._editable_div = createRef();
},
componentDidMount: function() {
this.value = this.props.initialValue;
if (this.refs.editable_div) {
if (this._editable_div.current) {
this.showPlaceholder(!this.value);
}
},
showPlaceholder: function(show) {
if (show) {
this.refs.editable_div.textContent = this.props.placeholder;
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
this._editable_div.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName);
this.placeholder = true;
this.value = '';
} else {
this.refs.editable_div.textContent = this.value;
this.refs.editable_div.setAttribute("class", this.props.className);
this._editable_div.current.textContent = this.value;
this._editable_div.current.setAttribute("class", this.props.className);
this.placeholder = false;
}
},
@ -120,7 +123,7 @@ module.exports = createReactClass({
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false);
this.refs.editable_div.blur();
this._editable_div.current.blur();
},
onValueChanged: function(shouldSubmit) {
@ -219,7 +222,7 @@ module.exports = createReactClass({
</div>;
} else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div ref="editable_div"
editableEl = <div ref={this._editable_div}
contentEditable={true}
className={className}
onKeyDown={this.onKeyDown}

View file

@ -90,7 +90,7 @@ function isInLowerLeftHalf(x, y, rect) {
* tooltip along one edge of the target.
*/
export default class InteractiveTooltip extends React.Component {
propTypes: {
static propTypes = {
// Content to show in the tooltip
content: PropTypes.node.isRequired,
// Function to call when visibility of the tooltip changes

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
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,20 +16,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import {_t} from '../../../languageHandler';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import * as ContextualMenu from '../../structures/ContextualMenu';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -58,6 +60,8 @@ export default createReactClass({
},
componentDidMount() {
this._contextMenuButton = createRef();
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
@ -107,56 +111,35 @@ export default createReactClass({
}
},
_openContextMenu: function(x, y, chevronOffset) {
// Hide the (...) immediately
this.setState({ hover: false });
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
ContextualMenu.createMenu(TagTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
top: y,
tag: this.props.tag,
onFinished: () => {
this.setState({ menuDisplayed: false });
},
});
this.setState({ menuDisplayed: true });
},
onContextButtonClick: function(e) {
e.preventDefault();
e.stopPropagation();
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
this._openContextMenu(x, y, chevronOffset);
},
onContextMenu: function(e) {
e.preventDefault();
const chevronOffset = 12;
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
},
onMouseOver: function() {
console.log("DEBUG onMouseOver");
this.setState({hover: true});
},
onMouseOut: function() {
console.log("DEBUG onMouseOut");
this.setState({hover: false});
},
openMenu: function(e) {
// Prevent the TagTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this.setState({
menuDisplayed: true,
hover: false,
});
},
closeMenu: function() {
this.setState({
menuDisplayed: false,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Tooltip = sdk.getComponent('elements.Tooltip');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
@ -184,23 +167,46 @@ export default createReactClass({
const tip = this.state.hover ?
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
{ "\u00B7\u00B7\u00B7" }
</div> : <div />;
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
/>
{ tip }
{ contextButton }
{ badgeElement }
</div>
</AccessibleButton>;
</div> : <div ref={this._contextMenuButton} />;
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
<TagTileContextMenu tag={this.props.tag} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className={className}
onClick={this.onClick}
onContextMenu={this.openMenu}
label={_t("Options")}
isExpanded={this.state.menuDisplayed}
>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
/>
{ tip }
{ contextButton }
{ badgeElement }
</div>
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
},
});

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
@ -34,6 +34,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._user_id_input = createRef();
},
addUser: function(user_id) {
if (this.props.selected_users.indexOf(user_id == -1)) {
this.props.onChange(this.props.selected_users.concat([user_id]));
@ -47,20 +51,20 @@ module.exports = createReactClass({
},
onAddUserId: function() {
this.addUser(this.refs.user_id_input.value);
this.refs.user_id_input.value = "";
this.addUser(this._user_id_input.current.value);
this._user_id_input.current.value = "";
},
render: function() {
const self = this;
return (
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
<ul className="mx_UserSelector_UserIdList">
{ this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
}) }
</ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<input type="text" ref={this._user_id_input} defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
{ _t("Add User") }
</button>

View file

@ -62,7 +62,7 @@ EMOJIBASE.forEach(emoji => {
DATA_BY_CATEGORY[categoryId].push(emoji);
}
// This is used as the string to match the query against when filtering emojis.
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`;
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
});
export const CATEGORY_HEADER_HEIGHT = 22;
@ -201,6 +201,7 @@ class EmojiPicker extends React.Component {
}
onChangeFilter(filter) {
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
for (const cat of this.categories) {
let emojis;
// If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.

View file

@ -23,7 +23,6 @@ class ReactionPicker extends React.Component {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
closeMenu: PropTypes.func.isRequired,
reactions: PropTypes.object,
};
@ -89,7 +88,6 @@ class ReactionPicker extends React.Component {
onChoose(reaction) {
this.componentWillUnmount();
this.props.closeMenu();
this.props.onFinished();
const myReactions = this.getReactions();
if (myReactions.hasOwnProperty(reaction)) {

View file

@ -1,6 +1,7 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
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,11 +22,12 @@ import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import {_t} from '../../../languageHandler';
import classNames from 'classnames';
import MatrixClientPeg from "../../../MatrixClientPeg";
import {createMenu} from "../../structures/ContextualMenu";
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
// XXX this class copies a lot from RoomTile.js
export default createReactClass({
displayName: 'GroupInviteTile',
@ -69,54 +71,49 @@ export default createReactClass({
});
},
_showContextMenu: function(x, y, chevronOffset) {
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
createMenu(GroupInviteTileContextMenu, {
chevronOffset,
left: x,
top: y,
group: this.props.group,
onFinished: () => {
this.setState({ menuDisplayed: false });
},
});
this.setState({ menuDisplayed: true });
},
onContextMenu: function(e) {
// Prevent the RoomTile onClick event firing as well
e.preventDefault();
_showContextMenu: function(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const chevronOffset = 12;
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
},
onBadgeClicked: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const state = {
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
state.hover = false;
}
const elementRect = e.target.getBoundingClientRect();
this.setState(state);
},
// The window X and Y offsets are to adjust position when zoomed in to page
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
onContextMenuButtonClick: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(x, y, chevronOffset);
this._showContextMenu(e.target.getBoundingClientRect());
},
onContextMenu: function(e) {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
},
closeMenu: function() {
this.setState({
contextMenuPosition: null,
});
},
render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const groupName = this.props.group.name || this.props.group.groupId;
@ -125,21 +122,31 @@ export default createReactClass({
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
});
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
{ groupName }
</div>;
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
const badgeEllipsis = this.state.badgeHover || isMenuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
'mx_RoomTile_badgeButton': badgeEllipsis,
});
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
const badge = (
<ContextMenuButton
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
>
{ badgeContent }
</ContextMenuButton>
);
let tooltip;
if (this.props.collapsed && this.state.hover) {
@ -148,17 +155,28 @@ export default createReactClass({
}
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_selected': this.state.selected,
'mx_GroupInviteTile': true,
});
return (
<AccessibleButton className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
let contextMenu;
if (isMenuDisplayed) {
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<GroupInviteTileContextMenu group={this.props.group} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<AccessibleButton
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
>
<div className="mx_RoomTile_avatar">
{ av }
@ -169,6 +187,8 @@ export default createReactClass({
</div>
{ tooltip }
</AccessibleButton>
);
{ contextMenu }
</React.Fragment>;
},
});

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
@ -51,6 +51,8 @@ export default class EditHistoryMessage extends React.PureComponent {
}
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
this._content = createRef();
}
_onAssociatedStatusChanged = () => {
@ -78,8 +80,8 @@ export default class EditHistoryMessage extends React.PureComponent {
pillifyLinks() {
// not present for redacted events
if (this.refs.content) {
pillifyLinks(this.refs.content.children, this.props.mxEvent);
if (this._content.current) {
pillifyLinks(this._content.current.children, this.props.mxEvent);
}
}
@ -102,9 +104,9 @@ export default class EditHistoryMessage extends React.PureComponent {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
// hide the button when already redacted
let redactButton;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent) {
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = (
<AccessibleButton onClick={this._onRedactClick} disabled={!this.state.canRedact}>
<AccessibleButton onClick={this._onRedactClick}>
{_t("Remove")}
</AccessibleButton>
);
@ -140,13 +142,13 @@ export default class EditHistoryMessage extends React.PureComponent {
if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
contentContainer = (
<div className="mx_EventTile_content" ref="content">*&nbsp;
<div className="mx_EventTile_content" ref={this._content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{contentElements}
</div>
);
} else {
contentContainer = <div className="mx_EventTile_content" ref="content">{contentElements}</div>;
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{contentElements}</div>;
}
}

View file

@ -80,7 +80,7 @@ export default class MAudioBody extends React.Component {
if (this.state.error !== null) {
return (
<span className="mx_MAudioBody" ref="body">
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting audio") }
</span>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import filesize from 'filesize';
@ -251,6 +251,12 @@ module.exports = createReactClass({
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
},
UNSAFE_componentWillMount: function() {
this._iframe = createRef();
this._dummyLink = createRef();
this._downloadImage = createRef();
},
componentDidMount: function() {
// Add this to the list of mounted components to receive notifications
// when the tint changes.
@ -272,17 +278,17 @@ module.exports = createReactClass({
tint: function() {
// Update our tinted copy of require("../../../../res/img/download.svg")
if (this.refs.downloadImage) {
this.refs.downloadImage.src = tintedDownloadImageURL;
if (this._downloadImage.current) {
this._downloadImage.current.src = tintedDownloadImageURL;
}
if (this.refs.iframe) {
if (this._iframe.current) {
// If the attachment is encrypted then the download image
// will be inside the iframe so we wont be able to update
// it directly.
this.refs.iframe.contentWindow.postMessage({
this._iframe.current.contentWindow.postMessage({
code: remoteSetTint.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this.refs.dummyLink),
style: computedStyle(this._dummyLink.current),
}, "*");
}
},
@ -325,7 +331,7 @@ module.exports = createReactClass({
};
return (
<span className="mx_MFileBody" ref="body">
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
@ -340,7 +346,7 @@ module.exports = createReactClass({
ev.target.contentWindow.postMessage({
code: remoteRender.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this.refs.dummyLink),
style: computedStyle(this._dummyLink.current),
blob: this.state.decryptedBlob,
// Set a download attribute for encrypted files so that the file
// will have the correct name when the user tries to download it.
@ -367,9 +373,9 @@ module.exports = createReactClass({
* We'll use it to learn how the download link
* would have been styled if it was rendered inline.
*/ }
<a ref="dummyLink" />
<a ref={this._dummyLink} />
</div>
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe" />
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
</div>
</span>
);
@ -439,7 +445,7 @@ module.exports = createReactClass({
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a {...downloadProps}>
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
{ _t("Download %(text)s", { text: text }) }
</a>
</div>

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
@ -65,6 +65,8 @@ export default class MImageBody extends React.Component {
hover: false,
showImage: SettingsStore.getValue("showImages"),
};
this._image = createRef();
}
componentWillMount() {
@ -158,8 +160,8 @@ export default class MImageBody extends React.Component {
let loadedImageDimensions;
if (this.refs.image) {
const { naturalWidth, naturalHeight } = this.refs.image;
if (this._image.current) {
const { naturalWidth, naturalHeight } = this._image.current;
// this is only used as a fallback in case content.info.w/h is missing
loadedImageDimensions = { naturalWidth, naturalHeight };
}
@ -342,7 +344,7 @@ export default class MImageBody extends React.Component {
imageElement = <HiddenImagePlaceholder />;
} else {
imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref="image"
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
@ -385,7 +387,7 @@ export default class MImageBody extends React.Component {
// which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size
img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
style={{ maxWidth: maxWidth + "px" }}
alt={content.body}
onError={this.onImageError}
@ -459,7 +461,7 @@ export default class MImageBody extends React.Component {
if (this.state.error !== null) {
return (
<span className="mx_MImageBody" ref="body">
<span className="mx_MImageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting image") }
</span>
@ -477,7 +479,7 @@ export default class MImageBody extends React.Component {
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody();
return <span className="mx_MImageBody" ref="body">
return <span className="mx_MImageBody">
{ thumbnail }
{ fileBody }
</span>;

View file

@ -132,7 +132,7 @@ module.exports = createReactClass({
if (this.state.error !== null) {
return (
<span className="mx_MVideoBody" ref="body">
<span className="mx_MVideoBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting video") }
</span>
@ -144,8 +144,8 @@ module.exports = createReactClass({
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a spinner.
return (
<span className="mx_MVideoBody" ref="body">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
<span className="mx_MVideoBody">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
</div>
</span>

View file

@ -1,6 +1,7 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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,17 +16,96 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import { createMenu } from '../../structures/ContextualMenu';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import {RoomContext} from "../../structures/RoomView";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
let contextMenu;
if (menuDisplayed) {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
const onCryptoClick = () => {
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event: mxEvent},
);
};
let e2eInfoCallback = null;
if (mxEvent.isEncrypted()) {
e2eInfoCallback = onCryptoClick;
}
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<MessageContextMenu
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
e2eInfoCallback={e2eInfoCallback}
onFinished={closeMenu}
/>
</ContextMenu>;
}
return <React.Fragment>
<ContextMenuButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
label={_t("Options")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
};
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
}
return <React.Fragment>
<ContextMenuButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
label={_t("React")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
};
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
@ -62,14 +142,6 @@ export default class MessageActionBar extends React.PureComponent {
this.props.onFocusChange(focused);
};
onCryptoClick = () => {
const event = this.props.mxEvent;
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event},
);
};
onReplyClick = (ev) => {
dis.dispatch({
action: 'reply_to_event',
@ -84,71 +156,6 @@ export default class MessageActionBar extends React.PureComponent {
});
};
getMenuOptions = (ev) => {
const menuOptions = {};
const buttonRect = ev.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const buttonRight = buttonRect.right + window.pageXOffset;
const buttonBottom = buttonRect.bottom + window.pageYOffset;
const buttonTop = buttonRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more
// space available.
if (buttonBottom < window.innerHeight / 2) {
menuOptions.top = buttonBottom;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
return menuOptions;
};
onReactClick = (ev) => {
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
const menuOptions = {
...this.getMenuOptions(ev),
mxEvent: this.props.mxEvent,
reactions: this.props.reactions,
chevronFace: "none",
onFinished: () => this.onFocusChange(false),
};
createMenu(ReactionPicker, menuOptions);
this.onFocusChange(true);
};
onOptionsClick = (ev) => {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const { getTile, getReplyThread } = this.props;
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
let e2eInfoCallback = null;
if (this.props.mxEvent.isEncrypted()) {
e2eInfoCallback = () => this.onCryptoClick();
}
const menuOptions = {
...this.getMenuOptions(ev),
mxEvent: this.props.mxEvent,
chevronFace: "none",
permalinkCreator: this.props.permalinkCreator,
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
e2eInfoCallback: e2eInfoCallback,
onFinished: () => {
this.onFocusChange(false);
},
};
createMenu(MessageContextMenu, menuOptions);
this.onFocusChange(true);
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -158,11 +165,9 @@ export default class MessageActionBar extends React.PureComponent {
if (isContentActionable(this.props.mxEvent)) {
if (this.context.room.canReact) {
reactButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
title={_t("React")}
onClick={this.onReactClick}
/>;
reactButton = (
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
);
}
if (this.context.room.canReply) {
replyButton = <AccessibleButton
@ -185,11 +190,12 @@ export default class MessageActionBar extends React.PureComponent {
{reactButton}
{replyButton}
{editButton}
<AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={this.onOptionsClick}
aria-haspopup={true}
<OptionsButton
mxEvent={this.props.mxEvent}
getReplyThread={this.props.getReplyThread}
getTile={this.props.getTile}
permalinkCreator={this.props.permalinkCreator}
onFocusChange={this.onFocusChange}
/>
</div>;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
@ -47,8 +47,12 @@ module.exports = createReactClass({
maxImageHeight: PropTypes.number,
},
UNSAFE_componentWillMount: function() {
this._body = createRef();
},
getEventTileOps: function() {
return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null;
},
onTileUpdate: function() {
@ -103,7 +107,8 @@ module.exports = createReactClass({
}
return <BodyType
ref="body" mxEvent={this.props.mxEvent}
ref={this._body}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}

View file

@ -149,7 +149,11 @@ export default class ReactionsRow extends React.PureComponent {
</a>;
}
return <div className="mx_ReactionsRow">
return <div
className="mx_ReactionsRow"
role="toolbar"
aria-label={_t("Reactions")}
>
{items}
{showAllButton}
</div>;

View file

@ -20,6 +20,8 @@ import classNames from 'classnames';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
@ -79,7 +81,7 @@ export default class ReactionsRowButton extends React.PureComponent {
render() {
const ReactionsRowButtonTooltip =
sdk.getComponent('messages.ReactionsRowButtonTooltip');
const { content, count, reactionEvents, myReactionEvent } = this.props;
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
const classes = classNames({
mx_ReactionsRowButton: true,
@ -96,18 +98,48 @@ export default class ReactionsRowButton extends React.PureComponent {
/>;
}
return <span className={classes}
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
let label;
if (room) {
const senders = [];
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender());
const name = member ? member.name : reactionEvent.getSender();
senders.push(name);
}
label = _t(
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
{
content,
},
{
reactors: () => {
return formatCommaSeparatedList(senders, 6);
},
reactedWith: (sub) => {
if (!content) {
return null;
}
return sub;
},
},
);
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton className={classes}
aria-label={label}
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
<span className="mx_ReactionsRowButton_content">
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}
</span>
<span className="mx_ReactionsRowButton_count">
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
</span>
{tooltip}
</span>;
</AccessibleButton>;
}
}

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -27,12 +27,13 @@ import sdk from '../../../index';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import * as ContextualMenu from '../../structures/ContextualMenu';
import * as ContextMenu from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
module.exports = createReactClass({
displayName: 'TextualBody',
@ -85,6 +86,10 @@ module.exports = createReactClass({
return successful;
},
UNSAFE_componentWillMount: function() {
this._content = createRef();
},
componentDidMount: function() {
this._unmounted = false;
if (!this.props.editState) {
@ -93,13 +98,13 @@ module.exports = createReactClass({
},
_applyFormatting() {
this.activateSpoilers(this.refs.content.children);
this.activateSpoilers(this._content.current.children);
// 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.
pillifyLinks(this.refs.content.children, this.props.mxEvent);
HtmlUtils.linkifyElement(this.refs.content);
pillifyLinks(this._content.current.children, this.props.mxEvent);
HtmlUtils.linkifyElement(this._content.current);
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
@ -162,7 +167,7 @@ module.exports = createReactClass({
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);
let links = this.findLinks(this._content.current.children);
if (links.length) {
// de-dup the links (but preserve ordering)
const seen = new Set();
@ -272,18 +277,12 @@ module.exports = createReactClass({
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent);
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10,
left: x,
top: y,
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false);
});
e.target.onmouseleave = close;
};
@ -332,7 +331,7 @@ module.exports = createReactClass({
},
getInnerText: () => {
return this.refs.content.innerText;
return this._content.current.innerText;
},
};
},
@ -406,13 +405,18 @@ module.exports = createReactClass({
label={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
/>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div
key="editedMarker" className="mx_EventTile_edited"
<AccessibleButton
key="editedMarker"
className="mx_EventTile_edited"
onClick={this._openHistoryDialog}
onMouseEnter={this._onMouseEnterEditedMarker}
onMouseLeave={this._onMouseLeaveEditedMarker}
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
>
{ editedTooltip }<span>{`(${_t("edited")})`}</span>
</AccessibleButton>
);
},
@ -457,7 +461,7 @@ module.exports = createReactClass({
case "m.emote":
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
<span ref={this._content} className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<span
className="mx_MEmoteBody_sender"
@ -472,14 +476,14 @@ module.exports = createReactClass({
);
case "m.notice":
return (
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
<span ref={this._content} className="mx_MNoticeBody mx_EventTile_content">
{ body }
{ widgets }
</span>
);
default: // including "m.text"
return (
<span ref="content" className="mx_MTextBody mx_EventTile_content">
<span ref={this._content} className="mx_MTextBody mx_EventTile_content">
{ body }
{ widgets }
</span>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg";
@ -58,13 +58,15 @@ export default class RoomProfileSettings extends React.Component {
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
};
this._avatarUpload = createRef();
}
_uploadAvatar = (e) => {
e.stopPropagation();
e.preventDefault();
this.refs.avatarUpload.click();
this._avatarUpload.current.click();
};
_saveProfile = async (e) => {
@ -178,7 +180,7 @@ export default class RoomProfileSettings extends React.Component {
return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">

View file

@ -188,14 +188,15 @@ module.exports = createReactClass({
}
const callView = (
<CallView ref="callView" room={this.props.room}
<CallView
room={this.props.room}
ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize}
maxVideoHeight={this.props.maxHeight}
/>
);
const appsDrawer = <AppsDrawer ref="appsDrawer"
const appsDrawer = <AppsDrawer
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}

View file

@ -19,7 +19,7 @@ limitations under the License.
import ReplyThread from "../elements/ReplyThread";
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const classNames = require("classnames");
@ -224,6 +224,9 @@ module.exports = createReactClass({
// don't do RR animations until we are mounted
this._suppressReadReceiptAnimation = true;
this._verifyEvent(this.props.mxEvent);
this._tile = createRef();
this._replyThread = createRef();
},
componentDidMount: function() {
@ -494,6 +497,9 @@ module.exports = createReactClass({
if (ev.status === EventStatus.NOT_SENT) {
return;
}
if (ev.isState()) {
return; // we expect this to be unencrypted
}
// if the event is not encrypted, but it's an e2e room, show the open padlock
return <E2ePadlockUnencrypted {...props} />;
}
@ -509,11 +515,11 @@ module.exports = createReactClass({
},
getTile() {
return this.refs.tile;
return this._tile.current;
},
getReplyThread() {
return this.refs.replyThread;
return this._replyThread.current;
},
getReactions() {
@ -745,7 +751,7 @@ module.exports = createReactClass({
</a>
</div>
<div className="mx_EventTile_line">
<EventTileType ref="tile"
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
@ -759,7 +765,7 @@ module.exports = createReactClass({
return (
<div className={classes}>
<div className="mx_EventTile_line">
<EventTileType ref="tile"
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
@ -789,7 +795,7 @@ module.exports = createReactClass({
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
'replyThread',
this._replyThread,
);
}
return (
@ -802,7 +808,7 @@ module.exports = createReactClass({
</a>
{ !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
<EventTileType ref="tile"
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
@ -817,7 +823,7 @@ module.exports = createReactClass({
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
'replyThread',
this._replyThread,
);
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
@ -836,7 +842,7 @@ module.exports = createReactClass({
</a>
{ !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
<EventTileType ref="tile"
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
@ -887,7 +893,7 @@ module.exports.haveTileForEvent = function(e) {
function E2ePadlockUndecryptable(props) {
return (
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" {...props} />
<E2ePadlock title={_t("This message cannot be decrypted")} icon="undecryptable" {...props} />
);
}
@ -899,19 +905,57 @@ function E2ePadlockUnverified(props) {
function E2ePadlockUnencrypted(props) {
return (
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" {...props} />
<E2ePadlock title={_t("Unencrypted")} icon="unencrypted" {...props} />
);
}
function E2ePadlock(props) {
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
return (<div
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
title={props.title} onClick={props.onClick} />);
} else {
return (<div
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden mx_EventTile_e2eIcon_${props.icon}`}
onClick={props.onClick} />);
class E2ePadlock extends React.Component {
static propTypes = {
icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
constructor() {
super();
this.state = {
hover: false,
};
}
onClick = (e) => {
if (this.props.onClick) this.props.onClick(e);
};
onHoverStart = () => {
this.setState({hover: true});
};
onHoverEnd = () => {
this.setState({hover: false});
};
render() {
let tooltip = null;
if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
}
let classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
if (!SettingsStore.getValue("alwaysShowEncryptionIcons")) {
classes += ' mx_EventTile_e2eIcon_hidden';
}
return (
<div
className={classes}
onClick={this.onClick}
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
>{tooltip}</div>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { linkifyElement } from '../../../HtmlUtils';
@ -54,17 +54,19 @@ module.exports = createReactClass({
}, (error)=>{
console.error("Failed to get URL preview: " + error);
});
this._description = createRef();
},
componentDidMount: function() {
if (this.refs.description) {
linkifyElement(this.refs.description);
if (this._description.current) {
linkifyElement(this._description.current);
}
},
componentDidUpdate: function() {
if (this.refs.description) {
linkifyElement(this.refs.description);
if (this._description.current) {
linkifyElement(this._description.current);
}
},
@ -129,7 +131,7 @@ module.exports = createReactClass({
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref="description">
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
{ p["og:description"] }
</div>
</div>

View file

@ -14,7 +14,7 @@ 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 React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
@ -111,6 +111,8 @@ class UploadButton extends React.Component {
super(props, context);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
this._uploadInput = createRef();
}
onUploadClick(ev) {
@ -118,7 +120,7 @@ class UploadButton extends React.Component {
dis.dispatch({action: 'require_registration'});
return;
}
this.refs.uploadInput.click();
this._uploadInput.current.click();
}
onUploadFileInputChange(ev) {
@ -150,7 +152,9 @@ class UploadButton extends React.Component {
onClick={this.onUploadClick}
title={_t('Upload file')}
>
<input ref="uploadInput" type="file"
<input
ref={this._uploadInput}
type="file"
style={uploadInputStyle}
multiple
onChange={this.onUploadFileInputChange}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, {createRef} from "react";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -45,6 +45,8 @@ export default class RoomBreadcrumbs extends React.Component {
// The room IDs we're waiting to come down the Room handler and when we
// started waiting for them. Used to track a room over an upgrade/autojoin.
this._waitingRoomQueue = [/* { roomId, addedTs } */];
this._scroller = createRef();
}
componentWillMount() {
@ -284,8 +286,8 @@ export default class RoomBreadcrumbs extends React.Component {
}
this.setState({rooms});
if (this.refs.scroller) {
this.refs.scroller.moveToOrigin();
if (this._scroller.current) {
this._scroller.current.moveToOrigin();
}
// We don't track room aesthetics (badges, membership, etc) over the wire so we
@ -390,7 +392,7 @@ export default class RoomBreadcrumbs extends React.Component {
return (
<div role="toolbar" aria-label={_t("Recent rooms")}>
<IndicatorScrollbar
ref="scroller"
ref={this._scroller}
className="mx_RoomBreadcrumbs"
trackHorizontalOverflow={true}
verticalScrollsHorizontally={true}

View file

@ -55,7 +55,7 @@ export default createReactClass({
if (rows.length === 0) {
rooms = <i>{ _t('No rooms to show') }</i>;
} else {
rooms = <table ref="directory_table" className="mx_RoomDirectory_table">
rooms = <table className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
</tbody>

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import sdk from '../../../index';
import React from 'react';
import React, {createRef} from 'react';
import { _t } from '../../../languageHandler';
import { linkifyElement } from '../../../HtmlUtils';
import { ContentRepo } from 'matrix-js-sdk';
@ -49,11 +49,15 @@ export default createReactClass({
},
_linkifyTopic: function() {
if (this.refs.topic) {
linkifyElement(this.refs.topic);
if (this._topic.current) {
linkifyElement(this._topic.current);
}
},
UNSAFE_componentWillMount: function() {
this._topic = createRef();
},
componentDidMount: function() {
this._linkifyTopic();
},
@ -104,7 +108,7 @@ export default createReactClass({
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic" ref="topic" onClick={this.onTopicClick}>
<div className="mx_RoomDirectory_topic" ref={this._topic} onClick={this.onTopicClick}>
{ room.topic }
</div>
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
@ -56,6 +56,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._topic = createRef();
},
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
@ -70,8 +74,8 @@ module.exports = createReactClass({
},
componentDidUpdate: function() {
if (this.refs.topic) {
linkifyElement(this.refs.topic);
if (this._topic.current) {
linkifyElement(this._topic.current);
}
},
@ -204,7 +208,7 @@ module.exports = createReactClass({
}
}
const topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
<div className="mx_RoomHeader_topic" ref={this._topic} title={topic} dir="auto">{ topic }</div>;
const avatarSize = 28;
let roomAvatar;
if (this.props.room) {

View file

@ -65,14 +65,14 @@ module.exports = createReactClass({
return (
<div className="mx_RoomHeader_name">
<EditableText ref="editor"
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={this._placeholderName}
blurToCancel={false}
initialValue={this.state.name}
onValueChanged={this._onValueChanged}
dir="auto" />
<EditableText
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={this._placeholderName}
blurToCancel={false}
initialValue={this.state.name}
onValueChanged={this._onValueChanged}
dir="auto" />
</div>
);
},

View file

@ -451,16 +451,21 @@ module.exports = createReactClass({
if (isDM) {
title = _t("Do you want to chat with %(user)s?",
{ user: inviteMember.name });
subTitle = [
avatar,
_t("<userName/> wants to chat", {}, {userName: () => inviterElement}),
];
primaryActionLabel = _t("Start chatting");
} else {
title = _t("Do you want to join %(roomName)s?",
{ roomName: this._roomName() });
subTitle = [
avatar,
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
];
primaryActionLabel = _t("Accept");
}
subTitle = [
avatar,
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
];
primaryActionLabel = _t("Accept");
primaryActionHandler = this.props.onJoinClick;
secondaryActionLabel = _t("Reject");
secondaryActionHandler = this.props.onRejectClick;

View file

@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -26,10 +25,9 @@ import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import sdk from '../../../index';
import {createMenu} from '../../structures/ContextualMenu';
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
@ -61,7 +59,7 @@ module.exports = createReactClass({
return ({
hover: false,
badgeHover: false,
menuDisplayed: false,
contextMenuPosition: null, // DOM bounding box, null if non-shown
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
@ -229,32 +227,6 @@ module.exports = createReactClass({
this.badgeOnMouseLeave();
},
_showContextMenu: function(x, y, chevronOffset) {
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
createMenu(RoomTileContextMenu, {
chevronOffset,
left: x,
top: y,
room: this.props.room,
onFinished: () => {
this.setState({ menuDisplayed: false });
this.props.refreshSubList();
},
});
this.setState({ menuDisplayed: true });
},
onContextMenu: function(e) {
// Prevent the RoomTile onClick event firing as well
e.preventDefault();
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const chevronOffset = 12;
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
},
badgeOnMouseEnter: function() {
// Only allow non-guests to access the context menu
// and only change it if it needs to change
@ -267,26 +239,46 @@ module.exports = createReactClass({
this.setState( { badgeHover: false } );
},
onOpenMenu: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
_showContextMenu: function(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const state = {
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
state.hover = false;
}
const elementRect = e.target.getBoundingClientRect();
this.setState(state);
},
// The window X and Y offsets are to adjust position when zoomed in to page
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
onContextMenuButtonClick: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(x, y, chevronOffset);
this._showContextMenu(e.target.getBoundingClientRect());
},
onContextMenu: function(e) {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
},
closeMenu: function() {
this.setState({
contextMenuPosition: null,
});
this.props.refreshSubList();
},
render: function() {
@ -303,6 +295,8 @@ module.exports = createReactClass({
subtext = this.state.statusMessage;
}
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
@ -310,7 +304,7 @@ module.exports = createReactClass({
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': isInvite,
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
@ -322,7 +316,7 @@ module.exports = createReactClass({
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
});
let name = this.state.roomName;
@ -344,7 +338,7 @@ module.exports = createReactClass({
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
@ -360,9 +354,17 @@ module.exports = createReactClass({
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
//}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let contextMenuButton;
if (!MatrixClientPeg.get().isGuest()) {
contextMenuButton = <AccessibleButton className="mx_RoomTile_menuButton" onClick={this.onOpenMenu} />;
contextMenuButton = (
<ContextMenuButton
className="mx_RoomTile_menuButton"
label={_t("Options")}
isExpanded={isMenuDisplayed}
onClick={this.onContextMenuButtonClick} />
);
}
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
@ -393,32 +395,47 @@ module.exports = createReactClass({
ariaLabel += " " + _t("Unread messages.");
}
return <AccessibleButton tabIndex="0"
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
let contextMenu;
if (isMenuDisplayed) {
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<AccessibleButton
tabIndex="0"
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
</div>
</div>
</div>
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ contextMenuButton }
{ badge }
</div>
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>;
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
{ contextMenu }
</React.Fragment>;
},
});

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
const classNames = require('classnames');
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
@ -29,6 +29,10 @@ module.exports = createReactClass({
});
},
UNSAFE_componentWillMount: function() {
this._search_term = createRef();
},
onThisRoomClick: function() {
this.setState({ scope: 'Room' }, () => this._searchIfQuery());
},
@ -47,29 +51,41 @@ module.exports = createReactClass({
},
_searchIfQuery: function() {
if (this.refs.search_term.value) {
if (this._search_term.current.value) {
this.onSearch();
}
},
onSearch: function() {
this.props.onSearch(this.refs.search_term.value, this.state.scope);
this.props.onSearch(this._search_term.current.value, this.state.scope);
},
render: function() {
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
mx_SearchBar_searching: this.props.searchInProgress,
});
const thisRoomClasses = classNames("mx_SearchBar_button", {
mx_SearchBar_unselected: this.state.scope !== 'Room',
});
const allRoomsClasses = classNames("mx_SearchBar_button", {
mx_SearchBar_unselected: this.state.scope !== 'All',
});
return (
<div className="mx_SearchBar">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
<div className="mx_SearchBar_input mx_textinput">
<input ref="search_term" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}></AccessibleButton>
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
{_t("This Room")}
</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
{_t("All Rooms")}
</AccessibleButton>
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}></AccessibleButton>
<div className="mx_SearchBar_input mx_textinput">
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>
);
},

View file

@ -14,12 +14,11 @@ 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 React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
@ -27,7 +26,6 @@ import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
import E2EIcon from './E2EIcon';
@ -143,6 +141,8 @@ class UploadButton extends React.Component {
super(props, context);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
this._uploadInput = createRef();
}
onUploadClick(ev) {
@ -150,7 +150,7 @@ class UploadButton extends React.Component {
dis.dispatch({action: 'require_registration'});
return;
}
this.refs.uploadInput.click();
this._uploadInput.current.click();
}
onUploadFileInputChange(ev) {
@ -182,7 +182,7 @@ class UploadButton extends React.Component {
onClick={this.onUploadClick}
title={_t('Upload file')}
>
<input ref="uploadInput" type="file"
<input ref={this._uploadInput} type="file"
style={uploadInputStyle}
multiple
onChange={this.onUploadFileInputChange}

View file

@ -25,6 +25,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
const widgetType = 'm.stickerpicker';
@ -371,26 +372,8 @@ export default class Stickerpicker extends React.Component {
}
render() {
const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
let stickerPicker;
let stickersButton;
const stickerPicker = <ContextualMenu
elementClass={GenericElementContextMenu}
chevronOffset={this.state.stickerPickerChevronOffset}
chevronFace={'bottom'}
left={this.state.stickerPickerX}
top={this.state.stickerPickerY}
menuWidth={this.popoverWidth}
menuHeight={this.popoverHeight}
element={this._getStickerpickerContent()}
onFinished={this._onFinished}
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
/>;
if (this.state.showStickers) {
// Show hide-stickers button
stickersButton =
@ -402,6 +385,23 @@ export default class Stickerpicker extends React.Component {
title={_t("Hide Stickers")}
>
</AccessibleButton>;
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
stickerPicker = <ContextMenu
chevronOffset={this.state.stickerPickerChevronOffset}
chevronFace="bottom"
left={this.state.stickerPickerX}
top={this.state.stickerPickerY}
menuWidth={this.popoverWidth}
menuHeight={this.popoverHeight}
onFinished={this._onFinished}
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
>
<GenericElementContextMenu element={this._getStickerpickerContent()} onResize={this._onFinished} />
</ContextMenu>;
} else {
// Show show-stickers button
stickersButton =
@ -415,8 +415,8 @@ export default class Stickerpicker extends React.Component {
</AccessibleButton>;
}
return <React.Fragment>
{stickersButton}
{this.state.showStickers && stickerPicker}
{ stickersButton }
{ stickerPicker }
</React.Fragment>;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
@ -48,13 +48,15 @@ export default class ProfileSettings extends React.Component {
avatarFile: null,
enableProfileSave: false,
};
this._avatarUpload = createRef();
}
_uploadAvatar = (e) => {
e.stopPropagation();
e.preventDefault();
this.refs.avatarUpload.click();
this._avatarUpload.current.click();
};
_saveProfile = async (e) => {
@ -156,7 +158,7 @@ export default class ProfileSettings extends React.Component {
return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
@ -44,13 +44,15 @@ export default class NotificationsSettingsTab extends React.Component {
}
this.setState({currentSound: soundData.name || soundData.url});
});
this._soundUpload = createRef();
}
async _triggerUploader(e) {
e.stopPropagation();
e.preventDefault();
this.refs.soundUpload.click();
this._soundUpload.current.click();
}
async _onSoundUploadChanged(e) {
@ -157,7 +159,7 @@ export default class NotificationsSettingsTab extends React.Component {
<div>
<h3>{_t("Set a new custom sound")}</h3>
<form autoComplete="off" noValidate={true}>
<input ref="soundUpload" className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
</form>
{currentUploadedFile}

View file

@ -49,8 +49,6 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = {
language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false,
@ -62,6 +60,7 @@ export default class GeneralUserSettingsTab extends React.Component {
},
emails: [],
msisdns: [],
...this._calculateThemeState(),
};
this.dispatcherRef = dis.register(this._onAction);
@ -80,6 +79,39 @@ export default class GeneralUserSettingsTab extends React.Component {
dis.unregister(this.dispatcherRef);
}
_calculateThemeState() {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that.
if (systemThemeExplicit) {
return {
theme: themeChoice,
useSystemTheme: true,
};
}
// If the user has set a theme explicitly, use that (no system theme matching)
if (themeExplicit) {
return {
theme: themeChoice,
useSystemTheme: false,
};
}
// Otherwise assume the defaults for the settings
return {
theme: themeChoice,
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
};
}
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
@ -89,11 +121,11 @@ export default class GeneralUserSettingsTab extends React.Component {
_onEmailsChange = (emails) => {
this.setState({ emails });
}
};
_onMsisdnsChange = (msisdns) => {
this.setState({ msisdns });
}
};
async _getThreepidState() {
const cli = MatrixClientPeg.get();
@ -193,9 +225,9 @@ export default class GeneralUserSettingsTab extends React.Component {
_onUseSystemThemeChanged = (checked) => {
this.setState({useSystemTheme: checked});
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch({action: 'recheck_theme'});
}
};
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
@ -307,12 +339,15 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
const themeWatcher = new ThemeWatcher();
let systemThemeSection;
if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div>
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE}
<LabelledToggleSwitch
value={this.state.useSystemTheme}
label={SettingsStore.getDisplayName("use_system_theme")}
onChange={this._onUseSystemThemeChanged}
/>
</div>;

View file

@ -13,7 +13,7 @@ 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 React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
@ -56,6 +56,10 @@ module.exports = createReactClass({
};
},
UNSAFE_componentWillMount: function() {
this._video = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.showCall();
@ -128,7 +132,7 @@ module.exports = createReactClass({
},
getVideoView: function() {
return this.refs.video;
return this._video.current;
},
render: function() {
@ -147,7 +151,9 @@ module.exports = createReactClass({
return (
<div>
<VideoView ref="video" onClick={this.props.onClick}
<VideoView
ref={this._video}
onClick={this.props.onClick}
onResize={this.props.onResize}
maxHeight={this.props.maxVideoHeight}
/>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -30,12 +30,16 @@ module.exports = createReactClass({
onResize: PropTypes.func,
},
UNSAFE_componentWillMount() {
this._vid = createRef();
},
componentDidMount() {
this.refs.vid.addEventListener('resize', this.onResize);
this._vid.current.addEventListener('resize', this.onResize);
},
componentWillUnmount() {
this.refs.vid.removeEventListener('resize', this.onResize);
this._vid.current.removeEventListener('resize', this.onResize);
},
onResize: function(e) {
@ -46,7 +50,7 @@ module.exports = createReactClass({
render: function() {
return (
<video ref="vid" style={{maxHeight: this.props.maxHeight}}>
<video ref={this._vid} style={{maxHeight: this.props.maxHeight}}>
</video>
);
},

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -49,6 +49,11 @@ module.exports = createReactClass({
onResize: PropTypes.func,
},
UNSAFE_componentWillMount: function() {
this._local = createRef();
this._remote = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
@ -58,7 +63,7 @@ module.exports = createReactClass({
},
getRemoteVideoElement: function() {
return ReactDOM.findDOMNode(this.refs.remote);
return ReactDOM.findDOMNode(this._remote.current);
},
getRemoteAudioElement: function() {
@ -74,7 +79,7 @@ module.exports = createReactClass({
},
getLocalVideoElement: function() {
return ReactDOM.findDOMNode(this.refs.local);
return ReactDOM.findDOMNode(this._local.current);
},
setContainer: function(c) {
@ -125,11 +130,11 @@ module.exports = createReactClass({
return (
<div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
<div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote" onResize={this.props.onResize}
<VideoFeed ref={this._remote} onResize={this.props.onResize}
maxHeight={maxVideoHeight} />
</div>
<div className={localVideoFeedClasses}>
<VideoFeed ref="local" />
<VideoFeed ref={this._local} />
</div>
</div>
);

View file

@ -18,7 +18,6 @@
"This phone number is already in use": "رقم الهاتف هذا مستخدم بالفعل",
"Failed to verify email address: make sure you clicked the link in the email": "فشل تأكيد عنوان البريد الإلكتروني: تحقق من نقر الرابط في البريد",
"The version of Riot.im": "إصدارة Riot.im",
"Whether or not you're logged in (we don't record your user name)": "فيما إذا كنت والجا أم لا (لا نسجّل اسم المستخدم)",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "فيما إذا كنت تستخدم وضع النص الغني لمحرر النصوص الغني أم لا",
"Your homeserver's URL": "عنوان خادوم المنزل",
"Your identity server's URL": "عنوان خادوم التعريف",
@ -39,7 +38,6 @@
"Update": "تحديث",
"What's New": "آخِر المُستجدّات",
"Toolbox": "علبة الأدوات",
"Add an email address above to configure email notifications": "أضف بريداً إلكترونياً أعلاه من أجل ضبط الإشعارات عبر البريد الإلكتروني",
"Collecting logs": "تجميع السجلات",
"No update available.": "لا يوجد هناك أي تحديث.",
"An error occurred whilst saving your email notification preferences.": "حدث خطأ ما أثناء عملية حفظ إعدادات الإشعارات عبر البريد الإلكتروني.",
@ -49,7 +47,6 @@
"Send Account Data": "إرسال بيانات الحساب",
"Waiting for response from server": "في انتظار الرد مِن الخادوم",
"This will allow you to return to your account after signing out, and sign in on other devices.": "سيسمح لك هذا بالعودة إلى حسابك بعد الخروج، وتسجيل الدخول على الأجهزة الأخرى.",
"Directory": "الدليل",
"Send logs": "إرسال السِجلات",
"Download this file": "تنزيل هذا الملف",
"Thank you!": "شكرًا !",
@ -63,7 +60,6 @@
"What's new?": "ما الجديد ؟",
"You have successfully set a password and an email address!": "لقد قمت بتعيين كلمة سرية و إدخال عنوان للبريد الإلكتروني بنجاح !",
"Cancel Sending": "إلغاء الإرسال",
"Collapse panel": "طي الجدول",
"Set Password": "تعيين كلمة سرية",
"Checking for an update...": "البحث عن تحديث …",
"powered by Matrix": "مشغل بواسطة Matrix",

View file

@ -4,7 +4,6 @@
"Uploading report": "Hesabatın göndərilməsi",
"Waiting for response from server": "Serverdən cavabın gözlənməsi",
"Messages containing my display name": "Mənim adımı özündə saxlayan mesajlar",
"Messages containing my user name": "Mənim istifadəçinin adımı özündə saxlayan mesaj",
"Messages in one-to-one chats": "Fərdi çatlarda mesajlar",
"Messages in group chats": "Qrup çatlarında mesajlar",
"When I'm invited to a room": "Nə vaxt ki, məni otağa dəvət edirlər",
@ -24,11 +23,9 @@
"Notify me for anything else": "Bütün qalan hadisələrdə xəbər vermək",
"Enable notifications for this account": "Bu hesab üçün xəbərdarlıqları qoşmaq",
"All notifications are currently disabled for all targets.": "Bütün qurğular üçün bütün bildirişlər kəsilmişdir.",
"Add an email address above to configure email notifications": "Yuxarı email-i xəbərdarlıqların qurması üçün əlavə edin",
"Failed to verify email address: make sure you clicked the link in the email": "Email-i yoxlamağı bacarmadı: əmin olun ki, siz məktubda istinaddakı ünvana keçdiniz",
"The platform you're on": "İstifadə edilən platforma",
"The version of Riot.im": "Riot.im versiyası",
"Whether or not you're logged in (we don't record your user name)": "Siz sistemə girdiniz ya yox (biz sizin istifadəçinin adınızı saxlamırıq)",
"Your language of choice": "Seçilmiş dil",
"Which officially provided instance you are using, if any": "Hansı rəsmən dəstəklənən müştəri tərəfindən siz istifadə edirsiniz ( əgər istifadə edirsinizsə)",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Siz Rich Text Editor redaktorunda Richtext rejimindən istifadə edirsinizmi",
@ -47,12 +44,7 @@
"VoIP is unsupported": "Zənglər dəstəklənmir",
"You cannot place VoIP calls in this browser.": "Zənglər bu brauzerdə dəstəklənmir.",
"You cannot place a call with yourself.": "Siz özünə zəng vura bilmirsiniz.",
"Conference calls are not supported in encrypted rooms": "Konfrans-əlaqə şifrlənmiş otaqlarda dəstəklənmir",
"Conference calls are not supported in this client": "Bu müştəridə konfrans-əlaqə dəstəklənmir",
"Warning!": "Diqqət!",
"Conference calling is in development and may not be reliable.": "Konfrans-əlaqə hazırlamadadır və işləməyə bilər.",
"Failed to set up conference call": "Konfrans-zəngi etməyi bacarmadı",
"Conference call failed.": "Konfrans-zəngin nasazlığı.",
"Upload Failed": "Faylın göndərilməsinin nasazlığı",
"Failure to create room": "Otağı yaratmağı bacarmadı",
"Sun": "Baz",
@ -83,10 +75,8 @@
"Admin": "Administrator",
"Start a chat": "Danışığa başlamaq",
"Who would you like to communicate with?": "Kimlə siz əlaqə saxlamaq istəyirdiniz?",
"Email, name or matrix ID": "Email, ad və ya ZT-ID",
"Start Chat": "Danışığa başlamaq",
"Invite new room members": "Yeni iştirakçıların otağına dəvət etmək",
"Who would you like to add to this room?": "Bu otaqa kimi dəvət etmək istərdiniz?",
"You need to be logged in.": "Siz sistemə girməlisiniz.",
"You need to be able to invite users to do that.": "Bunun üçün siz istifadəçiləri dəvət etmək imkanına malik olmalısınız.",
"Failed to send request.": "Sorğunu göndərməyi bacarmadı.",
@ -131,7 +121,6 @@
"(not supported by this browser)": "(bu brauzerlə dəstəklənmir)",
"%(senderName)s answered the call.": "%(senderName)s zəngə cavab verdi.",
"%(senderName)s ended the call.": "%(senderName)s zəng qurtardı.",
"%(senderName)s placed a %(callType)s call.": "%(senderName)s ) %(callType)s-zəng başladı.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s dəvət edilmiş iştirakçılar üçün danışıqların tarixini açdı.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s girmiş iştirakçılar üçün danışıqların tarixini açdı.",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s iştirakçılar üçün danışıqların tarixini açdı.",
@ -140,23 +129,15 @@
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s включил(а) в комнате сквозное шифрование (алгоритм %(algorithm)s).",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s üçün %(fromPowerLevel)s-dan %(toPowerLevel)s-lə",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s hüquqların səviyyələrini dəyişdirdi %(powerLevelDiffText)s.",
"%(displayName)s is typing": "%(displayName)s çap edir",
"%(names)s and %(lastPerson)s are typing": "%(names)s və %(lastPerson)s çap edirlər",
"Failed to join room": "Otağa girməyi bacarmadı",
"Disable Emoji suggestions while typing": "Mətnin yığılması vaxtı Emoji-i təklif etməmək",
"Hide read receipts": "Oxuma haqqında nişanları gizlətmək",
"Always show message timestamps": "Həmişə mesajların göndərilməsi vaxtını göstərmək",
"Autoplay GIFs and videos": "GIF animasiyalarını və videolarını avtomatik olaraq oynayır",
"Don't send typing notifications": "Nə vaxt ki, mən çap edirəm, o haqda bildirişləri göndərməmək",
"Never send encrypted messages to unverified devices from this device": "Heç vaxt (bu qurğudan) yoxlanılmamış qurğulara şifrlənmiş mesajları göndərməmək",
"Never send encrypted messages to unverified devices in this room from this device": "Heç vaxt (bu otaqda, bu qurğudan) yoxlanılmamış qurğulara şifrlənmiş mesajları göndərməmək",
"Accept": "Qəbul etmək",
"Error": "Səhv",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Mətn mesajı +%(msisdn)s-a göndərilmişdi. Mesajdan yoxlama kodunu daxil edin",
"Incorrect verification code": "Təsdiq etmənin səhv kodu",
"Enter Code": "Kodu daxil etmək",
"Phone": "Telefon",
"Add phone number": "Telefon nömrəsini əlavə etmək",
"New passwords don't match": "Yeni şifrlər uyğun gəlmir",
"Passwords can't be empty": "Şifrələr boş ola bilməz",
"Continue": "Davam etmək",
@ -173,10 +154,7 @@
"On": "Qoşmaq",
"Invalid alias format": "Adının yolverilməz formatı",
"'%(alias)s' is not a valid format for an alias": "Ad '%(alias)s' yolverilməz formata malikdir",
"Invalid address format": "Ünvanın yolverilməz formatı",
"'%(alias)s' is not a valid format for an address": "Ünvan '%(alias)s' yolverilməz formata malikdir",
"not specified": "qeyd edilmədi",
"not set": "qeyd edilmədi",
"Local addresses for this room:": "Sizin serverinizdə bu otağın ünvanları:",
"New address (e.g. #foo:%(localDomain)s)": "Yeni ünvan (məsələn, #nəsə:%(localDomain)s)",
"Blacklisted": "Qara siyahıda",
@ -196,14 +174,9 @@
"Ignore": "Bloklamaq",
"User Options": "Hərəkətlər",
"Direct chats": "Şəxsi çatlar",
"Level:": "Səviyyə:",
"Invited": "Dəvət edilmişdir",
"Filter room members": "İştirakçılara görə axtarış",
"Attachment": "Əlavə",
"Upload Files": "Faylların göndərilməsi",
"Are you sure you want to upload the following files?": "Siz əminsiniz ki, siz bu faylları göndərmək istəyirsiniz?",
"Encrypted room": "Şifrlənmiş otaq",
"Unencrypted room": "Şifrələnməyən otaq",
"Hangup": "Bitirmək",
"Voice call": "Səs çağırış",
"Video call": "Video çağırış",
@ -217,30 +190,15 @@
"Upload avatar": "Avatar-ı yükləmək",
"Settings": "Qurmalar",
"Forget room": "Otağı unutmaq",
"Drop here to tag %(section)s": "Bura daşıyın %(section)s nişan qoymaq üçün",
"Invites": "Dəvətlər",
"Favourites": "Seçilmişlər",
"People": "İnsanlar",
"Low priority": "Əhəmiyyətsizlər",
"Historical": "Arxiv",
"Rejoin": "Yenidən girmək",
"You are trying to access %(roomName)s.": "Siz %(roomName)s-a girməyə çalışırsınız.",
"You are trying to access a room.": "Siz otağa girməyə çalışırsınız.",
"<a>Click here</a> to join the discussion!": "<a>Qoşulmaq üçün</a> buraya basın!",
"Failed to unban": "Blokdan çıxarmağı bacarmadı",
"Banned by %(displayName)s": "%(displayName)s bloklanıb",
"Changes to who can read history will only apply to future messages in this room": "Tarixə girişin qaydalarının dəyişikliyi yalnız bu otaqda gələcək mesajlara tətbiq ediləcək",
"unknown error code": "naməlum səhv kodu",
"Failed to forget room %(errCode)s": "Otağı unutmağı bacarmadı: %(errCode)s",
"End-to-end encryption is in beta and may not be reliable": "İki tərəfi açıq şifrləmə indi beta-testdə və işləməyə bilər",
"You should not yet trust it to secure data": "Hal-hazırda yazışmalarınızın şifrələnəcəyinə etibar etməməlisiniz",
"Devices will not yet be able to decrypt history from before they joined the room": "Qurğular otağa girişinin anına qədər mesajların tarixinin şifrini aça bilməyəcək",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Otaqda şifrləmənin qoşmasından sonra siz o yenidən söndürə bilməyəcəksiniz (müvəqqəti)",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Şifrlənmiş mesajlar daha iki tərəfi açıq şifrləməni dəstəkləməyən müştərilərdə görülməyəcək",
"Enable encryption": "Şifrləməni qoşmaq",
"(warning: cannot be disabled again!)": "(xəbərdarlıq: dəyişdirmək mümkün olmayacaq!)",
"To send messages, you must be a": "Mesajların göndərilməsi üçün, olmaq lazımdır",
"To invite users into the room, you must be a": "Otağa iştirakçıları dəvət etmək üçün, olmaq lazımdır",
"No users have specific privileges in this room": "Heç bir istifadəçi bu otaqda xüsusi hüquqlara malik deyil",
"Banned users": "Bloklanmış istifadəçilər",
"Favourite": "Seçilmiş",
@ -259,14 +217,7 @@
"Decrypt %(text)s": "Şifrini açmaq %(text)s",
"Download %(text)s": "Yükləmək %(text)s",
"Message removed by %(userId)s": "%(userId)s mesajı silinmişdir",
"Password:": "Şifrə:",
"Username on %(hs)s": "İstifadəçinin adı %(hs)s",
"User name": "İstifadəçinin adı",
"Mobile phone number": "Mobil telefonun nömrəsi",
"Forgot your password?": "Şifrənizi unutmusunuz?",
"Sign in with": "Seçmək",
"Email address (optional)": "Email (qeyri-məcburi)",
"Mobile phone number (optional)": "Mobil telefonun (qeyri-məcburi) nömrəsi",
"Register": "Qeydiyyatdan keçmək",
"Remove": "Silmək",
"You are not receiving desktop notifications": "Siz sistem xəbərdarlıqlarını almırsınız",
@ -274,7 +225,6 @@
"Update": "Yeniləmək",
"Create new room": "Otağı yaratmaq",
"No results": "Nəticə yoxdur",
"Delete": "Silmək",
"Home": "Başlanğıc",
"Could not connect to the integration server": "İnteqrasiyanın serverinə qoşulmağ mümkün deyil",
"Manage Integrations": "İnteqrasiyaları idarə etmə",
@ -282,9 +232,6 @@
"Room directory": "Otaqların kataloqu",
"Start chat": "Çata başlamaq",
"Create Room": "Otağı yaratmaq",
"Advanced options": "Daha çox seçim",
"Block users on other matrix homeservers from joining this room": "Başqa serverlərdən bu otağa daxil olan istifadəçiləri bloklamaq",
"This setting cannot be changed later!": "Bu seçim sonra dəyişdirmək olmaz!",
"Deactivate Account": "Hesabı bağlamaq",
"Send Account Data": "Hesabın məlumatlarını göndərmək",
"An error has occurred.": "Səhv oldu.",
@ -293,7 +240,6 @@
"Please check your email and click on the link it contains. Once this is done, click continue.": "Öz elektron poçtunu yoxlayın və olan istinadı basın. Bundan sonra düyməni Davam etməyə basın.",
"Unable to add email address": "Email-i əlavə etməyə müvəffəq olmur",
"Unable to verify email address.": "Email-i yoxlamağı bacarmadı.",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "İstifadəçilərin adları yalnız hərfləri, rəqəmləri, nöqtələri, defisləri və altından xətt çəkmənin simvollarını özündə saxlaya bilər.",
"Username not available": "İstifadəçi adı mövcud deyil",
"An error occurred: %(error_string)s": "Səhv baş verdi: %(error_string)s",
"Username available": "İstifadəçi adı mövcuddur",
@ -301,13 +247,8 @@
"Reject invitation": "Dəvəti rədd etmək",
"Are you sure you want to reject the invitation?": "Siz əminsiniz ki, siz dəvəti rədd etmək istəyirsiniz?",
"Name": "Ad",
"Topic": "Mövzu",
"Make this room private": "Bu otağı bağlanmış etmək",
"Share message history with new users": "Mesajların tarixinə girişi yeni istifadəçilərə icazə vermək",
"Encrypt room": "Otağın şifrələnməsi",
"There are no visible files in this room": "Bu otaqda görülən fayl yoxdur",
"Featured Users:": "Seçilmiş istifadəçilər:",
"Couldn't load home page": "Ana səhifəni yükləməyi bacarmadı",
"Failed to reject invitation": "Dəvəti rədd etməyi bacarmadı",
"Failed to leave room": "Otaqdan çıxmağı bacarmadı",
"For security, this session has been signed out. Please sign in again.": "Təhlükəsizliyin təmin olunması üçün sizin sessiyanız başa çatmışdır idi. Zəhmət olmasa, yenidən girin.",
@ -315,45 +256,27 @@
"You have no visible notifications": "Görülən xəbərdarlıq yoxdur",
"Files": "Fayllar",
"Notifications": "Xəbərdarlıqlar",
"Hide panel": "Paneli gizlətmək",
"#example": "#misal",
"Connectivity to the server has been lost.": "Serverlə əlaqə itirilmişdir.",
"Sent messages will be stored until your connection has returned.": "Hələ ki serverlə əlaqə bərpa olmayacaq, göndərilmiş mesajlar saxlanacaq.",
"Active call": "Aktiv çağırış",
"Failed to upload file": "Faylı göndərməyi bacarmadı",
"No more results": "Daha çox nəticə yoxdur",
"Failed to save settings": "Qurmaları saxlamağı bacarmadı",
"Failed to reject invite": "Dəvəti rədd etməyi bacarmadı",
"Fill screen": "Ekranı doldurmaq",
"Click to unmute video": "Klikləyin, videonu qoşmaq üçün",
"Click to mute video": "Klikləyin, videonu söndürmək üçün",
"Click to unmute audio": "Klikləyin, səsi qoşmaq üçün",
"Click to mute audio": "Klikləyin, səsi söndürmək üçün",
"Expand panel": "Paneli açmaq",
"Filter room names": "Otaqlar üzrə axtarış",
"Failed to load timeline position": "Xronologiyadan nişanı yükləməyi bacarmadı",
"Can't load user settings": "İstifadəçi qurmalarını yükləmək mümkün deyil",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Şifrə uğurla dəyişdirildi. Təkrar avtorizasiyaya qədər siz başqa cihazlarda push-xəbərdarlıqları almayacaqsınız",
"Remove Contact Information?": "Əlaqə məlumatı silinsin?",
"Unable to remove contact information": "Əlaqə məlumatlarının silməyi bacarmadı",
"Interface Language": "İnterfeysin dili",
"User Interface": "İstifadəçi interfeysi",
"<not supported>": "<dəstəklənmir>",
"Import E2E room keys": "Şifrləmənin açarlarının idxalı",
"Cryptography": "Kriptoqrafiya",
"Ignored Users": "Bloklanan istifadəçilər",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Məxfilik bizim üçün əhəmiyyətlidir, buna görə biz bizim analitikamız üçün heç bir şəxsi və ya müəyyən edən məlumat yığmırıq.",
"Learn more about how we use analytics.": "O haqda daha ətraflı, necə biz analitikadan istifadə edirik.",
"Labs": "Laboratoriya",
"Use with caution": "Ehtiyatlılıqla istifadə etmək",
"Deactivate my account": "Mənim hesabımı bağlamaq",
"Clear Cache": "Keşi təmizləmək",
"Clear Cache and Reload": "Keşi təmizləmək və yenidən yükləmək",
"Bulk Options": "Qrup parametrləri",
"Email": "E-poçt",
"Add email address": "Email-i əlavə etmək",
"Profile": "Profil",
"Display name": "Göstərilən ad",
"Account": "Hesab",
"Access Token:": "Girişin token-i:",
"click to reveal": "açılış üçün basın",
@ -365,23 +288,11 @@
"A new password must be entered.": "Yeni parolu daxil edin.",
"New passwords must match each other.": "Yeni şifrələr uyğun olmalıdır.",
"I have verified my email address": "Mən öz email-i təsdiq etdim",
"Your password has been reset": "Sizin şifrə sıfırlandı",
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Siz bütün qurğulardan çıxdınız və push-xəbərdarlıqları almayacaqsınız. Xəbərdarlıq aktivləşdirmək üçün hər cihaza yenidən daxil olun",
"Return to login screen": "Girişin ekranına qayıtmaq",
"New password": "Yeni şifrə",
"Confirm your new password": "Yeni Şifrə təsdiq edin",
"Send Reset Email": "Şifrənizi sıfırlamaq üçün istinadla məktubu göndərmək",
"Create an account": "Hesabı yaratmaq",
"Set a display name:": "Görünüş adını daxil edin:",
"Upload an avatar:": "Avatar yüklə:",
"This server does not support authentication with a phone number.": "Bu server telefon nömrəsinin köməyi ilə müəyyənləşdirilməni dəstəkləmir.",
"Missing password.": "Şifrə yoxdur.",
"Passwords don't match.": "Şifrələr uyğun gəlmir.",
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Şifrə çox qısa (min. %(MIN_PASSWORD_LENGTH)s).",
"This doesn't look like a valid email address.": "Bu etibarlı bir e-poçt kimi görünmür.",
"This doesn't look like a valid phone number.": "Yanlış telefon nömrəsi.",
"An unknown error occurred.": "Bilinməyən bir səhv baş verdi.",
"I already have an account": "Məndə hesab var",
"Commands": "Komandalar",
"Emoji": "Smaylar",
"Users": "İstifadəçilər",
@ -449,7 +360,6 @@
"Restricted": "Məhduddur",
"Email, name or Matrix ID": "E-poçt, ad və ya Matrix ID",
"Send Invites": "Dəvət göndərin",
"Failed to invite user": "İstifadəçi dəvət olunmadı",
"Failed to invite": "Dəvət alınmadı",
"Failed to invite users to the room:": "İstifadəçiləri otağa dəvət etmək alınmadı:",
"Unable to create widget.": "Widjet yaratmaq olmur.",
@ -462,11 +372,9 @@
"Upgrades a room to a new version": "Bir otağı yeni bir versiyaya yüksəldir",
"Room upgrade confirmation": "Otaq yenilənməsi quraşdırılması",
"Upgrading a room can be destructive and isn't always necessary.": "Bir otağı təkmilləşdirməsi dağıdıcı ola bilər və həmişə lazım deyil.",
"Upgrade": "Yeniləmə",
"Changes your display nickname in the current room only": "Yalnız cari otaqda ekran ləqəbinizi dəyişdirir",
"Changes your avatar in this current room only": "Avatarınızı yalnız bu cari otaqda dəyişir",
"Changes your avatar in all rooms": "Bütün otaqlarda avatarınızı dəyişdirir",
"Changes colour scheme of current room": "Cari otağın rəng sxemini dəyişdirir",
"Gets or sets the room topic": "Otaq mövzusunu alır və ya təyin edir",
"This room has no topic.": "Bu otağın mövzusu yoxdur.",
"Sets the room name": "Otaq adını təyin edir",

View file

@ -1,6 +1,5 @@
{
"Couldn't find a matching Matrix room": "Не атрымалася знайсці адпаведны пакой Matrix",
"Enable audible notifications in web client": "Ўключыць гукавыя апавяшчэнні ў вэб-кліенце",
"All messages (noisy)": "Усе паведамленні (гучна)",
"Reject": "Адхіліць",
"Failed to forget room %(errCode)s": "Не атрымалася забыць пакой %(errCode)s",
@ -12,7 +11,6 @@
"Enable them now": "Уключыць іх зараз",
"Notification targets": "Мэты апавяшчэння",
"Failed to set direct chat tag": "Не ўдалося ўсталяваць тэг прамога чата",
"Failed to get protocol list from Home Server": "Не ўдалося атрымаць спіс пратаколаў ад хатняга сервера",
"Failed to set Direct Message status of room": "Не ўдалося ўсталяваць статут прамога паведамлення пакою",
"Favourite": "Улюбёнае",
"Quote": "Цытата",
@ -26,7 +24,6 @@
"Riot does not know how to join a room on this network": "Riot не ведае, як увайсці ў пакой у гэтай сетке",
"Members": "Удзельнікі",
"Can't update user notification settings": "Немагчыма абнавіць налады апавяшчэнняў карыстальніка",
"Add an email address above to configure email notifications": "Дадайце адрас электроннай пошты вышэй, каб наладзіць апавяшчэнні",
"Failed to change settings": "Не атрымалася змяніць налады",
"Noisy": "Шумна",
"Resend": "Паўторна",
@ -34,29 +31,22 @@
"remove %(name)s from the directory.": "выдаліць %(name)s з каталога.",
"Off": "Выключыць",
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Выдаліць псеўданім пакоя %(alias)s і выдаліць %(name)s з каталога?",
"Filter room names": "Фільтр iмёнаў пакояў",
"Invite to this room": "Запрасіць у гэты пакой",
"Notifications on the following keywords follow rules which cant be displayed here:": "Апавяшчэнні па наступных ключавых словах прытрымліваюцца правілаў, якія не могуць быць адлюстраваны тут:",
"#example": "#прыклад",
"Mentions only": "Толькі згадкі",
"Failed to get public room list": "Не ўдалося атрымаць спіс агульных пакояў",
"Remove": "Выдалiць",
"Failed to remove tag %(tagName)s from room": "Не ўдалося выдаліць %(tagName)s з пакоя",
"Leave": "Пакінуць",
"Enable notifications for this account": "Ўключыць апавяшчэнні для гэтага ўліковага запісу",
"Enable desktop notifications": "Ўключыць апавяшчэнні на працоўным стале",
"Error": "Памылка",
"Directory": "Каталог",
"No rooms to show": "Няма пакояў для паказу",
"Download this file": "Спампаваць гэты файл",
"Search for a room": "Пошук па пакоі",
"Operation failed": "Не атрымалася выканаць аперацыю",
"delete the alias.": "выдаліць псеўданім.",
"Forget": "Забыць",
"Mute": "Без гуку",
"Error saving email notification preferences": "Памылка захавання налад апавяшчэнняў па электроннай пошце",
"Enter keywords separated by a comma:": "Калі ласка, увядзіце ключавыя словы, падзеленыя коскамі:",
"The Home Server may be too old to support third party networks": "Хатні сервер можа быць занадта стары для падтрымкі іншых сетак",
"powered by Matrix": "працуе на Matrix",
"Custom Server Options": "Карыстальніцкія параметры сервера",
"Remove %(name)s from the directory?": "Выдаліць %(name)s з каталога?",

File diff suppressed because it is too large Load diff

View file

@ -2,14 +2,12 @@
"People": "Gent",
"Add a widget": "Afegeix un giny",
"Account": "Compte",
"VoIP": "Veu IP",
"No Microphones detected": "No s'ha detectat cap micròfon",
"No Webcams detected": "No s'ha detectat cap càmera web",
"Microphone": "Micròfon",
"Camera": "Càmera",
"Advanced": "Avançat",
"Algorithm": "Algoritme",
"Hide removed messages": "Amaga els missatges esborrats",
"Always show message timestamps": "Mostra sempre la marca de temps del missatge",
"Alias (optional)": "Àlies (opcional)",
"Cancel": "Cancel·la",
@ -30,7 +28,6 @@
"Remove": "Elimina",
"unknown error code": "codi d'error desconegut",
"OK": "D'acord",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "S'ha enviat un missatge de text a +%(msisdn)s. Entreu si us plau el codi de verificació que conté",
"Operation failed": "No s'ha pogut realitzar l'operació",
"Search": "Cerca",
"powered by Matrix": "amb tecnologia de Matrix",
@ -58,14 +55,7 @@
"VoIP is unsupported": "El VoIP no és compatible",
"You cannot place VoIP calls in this browser.": "No es poden fer trucades VoIP amb aquest navegador.",
"You cannot place a call with yourself.": "No és possible trucar-se a un mateix.",
"Conference calls are not supported in this client": "Aquest client no és compatible amb les trucades de conferència",
"Conference calls are not supported in encrypted rooms": "Les trucades de conferència no es poden fer dins les sales encriptades",
"Warning!": "Avís!",
"Conference calling is in development and may not be reliable.": "Les conferències estan en desenvolupament i podrien no ser fiables.",
"Failed to set up conference call": "No s'ha pogut realitzar la trucada de conferència",
"Conference call failed.": "No s´ha pogut realitzar la trucada de conferència.",
"The file '%(fileName)s' failed to upload": "No s'ha pogut pujar el fitxer '%(fileName)s'",
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "El fitxer «%(fileName)s» supera el límit de mida per a les pujades d'aquest servidor",
"Upload Failed": "No s'ha pogut realitzar la pujada",
"Sun": "dg.",
"Mon": "dl.",
@ -95,7 +85,6 @@
"Who would you like to add to this community?": "A qui voleu afegir a aquesta comunitat?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Avís: les persones que afegiu a aquesta comunitat seran visibles públicament per a qualsevol que conegui l'ID de la comunitat",
"Invite new community members": "Convida nous membres a unir-se a la comunitat",
"Name or matrix ID": "Nom o ID de Matrix",
"Invite to Community": "Convida a la comunitat",
"Which rooms would you like to add to this community?": "Quines sales voleu afegir a aquesta comunitat?",
"Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales als que no son membres a la pàgina de la comunitat i a la llista de sales?",
@ -117,12 +106,9 @@
"Admin": "Administrador",
"Start a chat": "Comença un xat",
"Who would you like to communicate with?": "Amb qui us voleu comunicar?",
"Email, name or matrix ID": "Correu electrònic, nom o ID de Matrix",
"Start Chat": "Comença un xat",
"Invite new room members": "Convida a nous membres a la sala",
"Who would you like to add to this room?": "A qui vol afegir a aquesta sala?",
"Send Invites": "Envia invitacions",
"Failed to invite user": "No s'ha pogut convidar al usuari",
"Failed to invite": "No s'ha pogut tramitar la invitació",
"Failed to invite the following users to the %(roomName)s room:": "No s'ha pogut convidar a la sala %(roomName)s els següents usuaris:",
"You need to be logged in.": "És necessari estar autenticat.",
@ -182,7 +168,6 @@
"(no answer)": "(sense resposta)",
"(unknown failure: %(reason)s)": "(error desconegut: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s ha penjat.",
"%(senderName)s placed a %(callType)s call.": "%(senderName)s ha fet una trucada de %(callType)s.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha convidat a %(targetDisplayName)s a entrar a la sala.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha fet visible l'històric futur de la sala per a tots els membres, a partir de que hi són convidats.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha fet visible l'històric futur de la sala a tots els membres, des de que entren a la sala.",
@ -196,10 +181,6 @@
"%(widgetName)s widget modified by %(senderName)s": "%(senderName)s ha modificat el giny %(widgetName)s",
"%(widgetName)s widget added by %(senderName)s": "%(senderName)s ha afegit el giny %(widgetName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(senderName)s ha eliminat el giny %(widgetName)s",
"%(displayName)s is typing": "%(displayName)s està escrivint",
"%(names)s and %(count)s others are typing|other": "%(names)s i %(count)s més estan escrivint",
"%(names)s and %(count)s others are typing|one": "%(names)s i algú altre està escrivint",
"%(names)s and %(lastPerson)s are typing": "%(names)s i %(lastPerson)s estan escrivint",
"Failure to create room": "No s'ha pogut crear la sala",
"Server may be unavailable, overloaded, or you hit a bug.": "És possible que el servidor no estigui disponible, amb sobrecàrrega o que s'hagi trobat un error.",
"Send anyway": "Envia de totes maneres",
@ -210,19 +191,11 @@
"Authentication check failed: incorrect password?": "Ha fallat l'autenticació: heu introduït correctament la contrasenya?",
"Failed to join room": "No s'ha pogut entrar a la sala",
"Message Pinning": "Fixació de missatges",
"Disable Emoji suggestions while typing": "Desactiva els suggeriments d'Emoji mentre s'escriu",
"Use compact timeline layout": "Utilitza el disseny compacte de la línia de temps",
"Hide join/leave messages (invites/kicks/bans unaffected)": "Amaga els missatges d'entrada i sortida (no afecta a les invitacions, expulsions o prohibicions)",
"Hide avatar changes": "Amaga els canvis de foto de perfil",
"Hide display name changes": "Amaga els canvis de nom visible",
"Hide read receipts": "Amaga els rebuts llegits",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostra les marques de temps en format de 12 hores (per exemple, 2:30pm)",
"Autoplay GIFs and videos": "Reprodueix de forma automàtica els GIF i vídeos",
"Enable automatic language detection for syntax highlighting": "Activa la detecció automàtica d'idiomes per al ressaltat de sintaxi",
"Disable big emoji in chat": "Desactiva els grans emoji al xat",
"Don't send typing notifications": "No enviïs notificacions d'escriptura",
"Automatically replace plain text Emoji": "Substitueix automàticament Emoji de text pla",
"Disable Peer-to-Peer for 1:1 calls": "Desactiva el Peer-to-Peer per a trucades entre dos",
"Never send encrypted messages to unverified devices in this room from this device": "No enviïs mai missatges xifrats a dispositius no verificats en aquesta sala des d'aquest dispositiu",
"Enable inline URL previews by default": "Activa per defecte la vista prèvia d'URL en línia",
"Enable URL previews for this room (only affects you)": "Activa la vista prèvia d'URL d'aquesta sala (no afecta altres usuaris)",
@ -231,17 +204,14 @@
"Active call (%(roomName)s)": "Trucada activa (%(roomName)s)",
"unknown caller": "trucada d'un desconegut",
"Incoming voice call from %(name)s": "Trucada de veu entrant de %(name)s",
"Hide avatars in user and room mentions": "Amaga els avatars a les mencions de l'usuari i de la sala",
"Never send encrypted messages to unverified devices from this device": "No enviïs mai missatges xifrats a dispositius no verificats des d'aquest dispositiu",
"Incoming video call from %(name)s": "Trucada de vídeo entrant de %(name)s",
"Incoming call from %(name)s": "Trucada entrant de %(name)s",
"Decline": "Declina",
"Accept": "Accepta",
"Incorrect verification code": "El codi de verificació és incorrecte",
"Enter Code": "Introduïu el codi",
"Submit": "Envia",
"Phone": "Telèfon",
"Add phone number": "Afegiu un número de telèfon",
"Add": "Afegeix",
"Failed to upload profile picture!": "No s'ha pogut pujar la imatge!",
"Upload new:": "Puja un nou:",
@ -256,15 +226,12 @@
"New Password": "Nova contrasenya",
"Confirm password": "Confirma la contrasenya",
"Change Password": "Canvia la contrasenya",
"Your home server does not support device management.": "El seu servidor amfitrió no és compatible amb la gestió de dispositius.",
"Unable to load device list": "No s'ha pogut carregar la llista de dispositius",
"Authentication": "Autenticació",
"Delete %(count)s devices|other": "Suprimeix %(count)s dispositius",
"Delete %(count)s devices|one": "Suprimeix el dispositiu",
"Device ID": "ID del dispositiu",
"Device Name": "Nom del dispositiu",
"Last seen": "Vist per última vegada",
"Select devices": "Selecciona els dispositius",
"Failed to set display name": "No s'ha pogut establir el nom visible",
"Disable Notifications": "Desactiva les notificacions",
"Enable Notifications": "Activa les notificacions",
@ -280,13 +247,10 @@
"%(senderName)s uploaded a file": "%(senderName)s ha pujat un fitxer",
"Options": "Opcions",
"Undecryptable": "Indesxifrable",
"Encrypted by a verified device": "Xifrat per un dispositiu verificat",
"Encrypted by an unverified device": "Xifrat per un dispositiu no verificat",
"Unencrypted message": "Missatge no xifrat",
"Please select the destination room for this message": "Si us plau, seleccioneu la sala destinatària per a aquest missatge",
"Blacklisted": "Llista negre",
"Verified": "Verificat",
"Unverified": "No verificat",
"device id: ": "ID del dispositiu: ",
"Disinvite": "Descarta la invitació",
"Kick": "Fes fora",
@ -318,17 +282,12 @@
"Revoke Moderator": "Revoca el moderador",
"Make Moderator": "Fes-lo moderador",
"Admin Tools": "Eines d'administració",
"Level:": "Nivell:",
"and %(count)s others...|other": "i %(count)s altres...",
"and %(count)s others...|one": "i un altre...",
"Invited": "Convidat",
"Filter room members": "Filtra els membres de la sala",
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (poder %(powerLevelNumber)s)",
"Attachment": "Adjunt",
"Upload Files": "Puja fitxers",
"Are you sure you want to upload the following files?": "Confirmeu que voleu pujar els següents fitxers?",
"Encrypted room": "Sala xifrada",
"Unencrypted room": "Sala no xifrada",
"Hangup": "Penja",
"Voice call": "Trucada de veu",
"Video call": "Trucada de vídeo",
@ -339,8 +298,6 @@
"Send an encrypted message…": "Envia un missatge xifrat…",
"Send a message (unencrypted)…": "Envia un missatge (sense xifrar)…",
"You do not have permission to post to this room": "No teniu el permís per escriure en aquesta sala",
"Turn Markdown on": "Activa el Markdown",
"Turn Markdown off": "Desactiva el Markdown",
"Hide Text Formatting Toolbar": "Amaga la barra d'eines de format de text",
"Server error": "S'ha produït un error al servidor",
"Mirror local video feed": "Mostra el vídeo local com un mirall",
@ -348,12 +305,6 @@
"Command error": "S'ha produït un error en l'ordre",
"bold": "negreta",
"italic": "cursiva",
"strike": "tatxat",
"underline": "subratllat",
"code": "codi",
"quote": "cita",
"bullet": "pic",
"numbullet": "pic numerat",
"Markdown is disabled": "El Markdown està desactivat",
"Markdown is enabled": "El Markdown està activat",
"Jump to message": "Salta al missatge",
@ -376,73 +327,27 @@
"Seen by %(userName)s at %(dateTime)s": "Vist per %(userName)s a les %(dateTime)s",
"No rooms to show": "No hi ha cap sala per a mostrar",
"Unnamed room": "Sala sense nom",
"Failed to set avatar.": "No s'ha pogut establir l'avatar.",
"Save": "Desa",
"(~%(count)s results)|other": "(~%(count)s resultats)",
"(~%(count)s results)|one": "(~%(count)s resultat)",
"Join Room": "Entra a la sala",
"Upload avatar": "Puja l'avatar",
"Remove avatar": "Elimina l'avatar",
"Forget room": "Oblida la sala",
"Show panel": "Mostra el plafó",
"Drop here to favourite": "Deixa-ho anar aquí per a marcar-ho com a favorit",
"Drop here to tag direct chat": "Deixa'l anar aquí per posar-li la etiqueta de xat directe",
"Drop here to restore": "Deixa'l anar aquí per a restaurar-lo",
"Drop here to demote": "Deixa'l anar aquí per a baixar-lo de grau",
"Drop here to tag %(section)s": "Deixa´l anar aquí per posar-li la etiqueta de %(section)s",
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "Encara no ha entrat a cap sala. Premeu <CreateRoomButton>per crear una sala o bé <RoomDirectoryButton> per fullejar el directori",
"Community Invites": "Invitacions de les comunitats",
"Invites": "Invitacions",
"Favourites": "Preferits",
"Low priority": "Baixa prioritat",
"Historical": "Històric",
"Unable to ascertain that the address this invite was sent to matches one associated with your account.": "No s'ha pogut determinar que s'hagi enviat la invitació a una adreça associada al vostre compte.",
"This invitation was sent to an email address which is not associated with this account:": "Aquesta invitació s'ha enviat a una adreça de correu electrònic que no està associada a aquest compte:",
"Press <StartChatButton> to start a chat with someone": "Prem <StartChatButton> per a començar un xat amb algú",
"You may wish to login with a different account, or add this email to this account.": "És possible que vulgueu iniciar la sessió amb un altre compte o bé afegir aquest correu electrònic a aquest compte.",
"You have been invited to join this room by %(inviterName)s": "Heu sigut convidat a aquesta sala per %(inviterName)s",
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Voleu <acceptText>accept</acceptText> o bé <declineText>decline</declineText> aquesta invitació?",
"Reason: %(reasonText)s": "Raó: %(reasonText)s",
"Rejoin": "Trona a entrar",
"You have been kicked from %(roomName)s by %(userName)s.": "%(userName)s us ha fet fora de la sala %(roomName)s.",
"You have been kicked from this room by %(userName)s.": "%(userName)s vos ha fet fora de la sala.",
"You have been banned from %(roomName)s by %(userName)s.": "%(userName)s vos ha expulsat de la sala %(roomName)s.",
"You have been banned from this room by %(userName)s.": "Heu sigut expulsat d'aquesta sala per %(userName)s.",
"This room": "Aquesta sala",
"%(roomName)s does not exist.": "La sala %(roomName)s no existeix.",
"%(roomName)s is not accessible at this time.": "La sala %(roomName)s no és accessible en aquest moment.",
"You are trying to access %(roomName)s.": "Esteu intentant accedir a la sala %(roomName)s.",
"You are trying to access a room.": "Esteu intentant accedir a una sala.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> per a entrar al debat!",
"This is a preview of this room. Room interactions have been disabled": "Aquesta és una vista prèvia de la sala. No s'hi pot interactuar des d'aquí",
"To change the room's avatar, you must be a": "Per canviar l'avatar de la sala, heu de ser",
"To change the room's name, you must be a": "Per canviar el nom de la sala, heu de ser",
"To change the room's main address, you must be a": "Per canviar l'adreça principal de la sala, heu de ser",
"To change the room's history visibility, you must be a": "Per canviar la visibilitat de l'historial de la sala, heu de ser",
"To change the permissions in the room, you must be a": "Per canviar els permisos a la sala, heu de ser",
"To change the topic, you must be a": "Per canviar el tema, heu de ser",
"To modify widgets in the room, you must be a": "Per modificar els ginys de la sala, has de ser",
"Failed to unban": "No s'ha pogut expulsar",
"Banned by %(displayName)s": "Expulsat per %(displayName)s",
"Privacy warning": "Avís de privadesa",
"Changes to who can read history will only apply to future messages in this room": "Els canvis de qui pot llegir l'historial només s'aplicaran als missatges futurs d'aquesta sala",
"The visibility of existing history will be unchanged": "La visibilitat de l'historial existent no es modificarà",
"End-to-end encryption is in beta and may not be reliable": "El xifratge d'extrem a extrem està en fase beta i pot ser que no sigui fiable",
"You should not yet trust it to secure data": "Encara no hi heu de confiar per a comunicacions segures",
"Devices will not yet be able to decrypt history from before they joined the room": "Els dispositius encara no podran desxifrar l'historial d'abans d'entrar a la sala",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Quan s'activa el xifratge en una sala, no es pot desactivar posteriorment (per ara)",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Els missatges xifrats no es podran veure en els clients que encara no tinguin implementat el xifratge",
"Enable encryption": "El xifratge està activat",
"(warning: cannot be disabled again!)": "(avís: no es pot desactivar de nou!)",
"Encryption is enabled in this room": "El xifratge està activat en aquesta sala",
"Encryption is not enabled in this room": "El xifratge no està activat en aquesta sala",
"Privileged Users": "Usuaris amb privilegis",
"No users have specific privileges in this room": "Cap usuari té privilegis específics en aquesta sala",
"Banned users": "Usuaris expulsats",
"This room is not accessible by remote Matrix servers": "Aquesta sala no és accessible per a servidors de Matrix remots",
"Leave room": "Surt de la sala",
"Tagged as: ": "Etiquetats com: ",
"To link to a room it must have <a>an address</a>.": "Per poder fer un enllaç a una sala aquesta ha de tenir <a>an address</a>.",
"Guests cannot join this room even if explicitly invited.": "Els usuaris d'altres xarxes no poden entrar a la sala d'aquest esdeveniment encara que hi hagin sigut convidats de forma explícita.",
"Click here to fix": "Feu clic aquí per corregir-ho",
"Who can access this room?": "Qui pot entrar a aquesta sala?",
@ -455,28 +360,13 @@
"Members only (since they were invited)": "Només els membres (a partir del punt en què hi són convidats)",
"Members only (since they joined)": "Només els membres (a partir del punt en què entrin a la sala)",
"Permissions": "Permisos",
"The default role for new room members is": "El rol per defecte dels nous membres de la sala és",
"To send messages, you must be a": "Per poder enviar missatges, heu de ser",
"To invite users into the room, you must be a": "Per poder convidar a usuaris a aquesta sala, heu de ser",
"To configure the room, you must be a": "Per poder configurar aquesta sala, heu de ser",
"To kick users, you must be a": "Per poder fer fora a usuaris, heu de ser",
"To ban users, you must be a": "Per expulsar a usuaris, heu de ser",
"To remove other users' messages, you must be a": "Per eliminar missatges d'altres usuaris, heu de ser",
"To send events of type <eventType/>, you must be a": "Per poder enviar esdeveniments de tipus <eventType/>, heu de ser",
"This room's internal ID is": "L'ID d'aquesta sala és",
"Add a topic": "Afegeix un tema",
"Scroll to unread messages": "Desplaça't fins els missatges no llegits",
"Jump to first unread message.": "Salta al primer missatge no llegit.",
"Invalid alias format": "El format de l'àlies no és vàlid",
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' no és un format d'àlies vàlid",
"Invalid address format": "L'adreça no té un format vàlid",
"Anyone who knows the room's link, including guests": "Qualsevol que conegui l'enllaç de la sala, inclosos els usuaris d'altres xarxes",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' no és un format vàlid per a una adreça",
"not specified": "sense especificar",
"not set": "sense definir",
"Remote addresses for this room:": "Adreces remotes per a aquesta sala:",
"Addresses": "Adreces",
"The main address for this room is": "L'adreça principal d'aquesta sala és",
"Local addresses for this room:": "Adreces locals d'aquesta sala:",
"This room has no local addresses": "Aquesta sala no té adreces locals",
"New address (e.g. #foo:%(localDomain)s)": "Nova adreça (per exemple #foo:%(localDomain)s)",
@ -504,35 +394,17 @@
"Removed or unknown message type": "El tipus de missatge ha sigut eliminat o és desconegut",
"Message removed by %(userId)s": "El missatge ha sigut eliminat per l'usuari %(userId)s",
"Message removed": "S'ha eliminat el missatge",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Actualment, la verificació del robot no està disponible a l'escriptori: utilitzeu un <a>web browser</a>",
"Sign in with CAS": "Inicieu sessió amb CAS",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Podeu utilitzar les opcions de servidor personalitzades per a iniciar sessió en altres servidors Matrix, especificant un altre URL de servidor principal.",
"This allows you to use this app with an existing Matrix account on a different home server.": "Això us permet utilitzar aquesta aplicació amb un compte de Matrix existent en un altre servidor.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "També podeu establir un servidor d'identitat personalitzat, però normalment això evitarà la interacció basada en l'adreça de correu electrònic amb altres usuaris.",
"To continue, please enter your password.": "Per poder continuar, si us plau, introduïu una contrasenya.",
"Password:": "Contrasenya:",
"An email has been sent to %(emailAddress)s": "S'ha enviat un correu electrònic a %(emailAddress)s",
"Please check your email to continue registration.": "Reviseu el vostre correu electrònic per a poder continuar amb el registre.",
"Token incorrect": "El testimoni és incorrecte",
"A text message has been sent to %(msisdn)s": "S'ha enviat un missatge de text a %(msisdn)s",
"Please enter the code it contains:": "Introduïu el codi que conté:",
"Start authentication": "Inicia l'autenticació",
"Username on %(hs)s": "Nom d'usuari a %(hs)s",
"User name": "Nom d'usuari",
"Mobile phone number": "Número de telèfon mòbil",
"Forgot your password?": "Heu oblidat la vostra contrasenya?",
"%(serverName)s Matrix ID": "ID de Matrix de %(serverName)s",
"Sign in with": "Inicieu sessió amb",
"Email address": "Correu electrònic",
"Sign in": "Inicia sessió",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Si no especifiqueu una adreça de correu electrònic, no podreu restablir la vostra contrasenya. N'esteu segur?",
"Email address (optional)": "Correu electrònic (opcional)",
"You are registering with %(SelectedTeamName)s": "Esteu registrant-vos amb %(SelectedTeamName)s",
"Mobile phone number (optional)": "Número de telèfon mòbil (opcional)",
"Default server": "Servidor per defecte",
"Custom server": "Servidor personalitzat",
"Identity server URL": "URL del servidor d'identitat",
"What does this mean?": "Què vol dir això?",
"Remove from community": "Elimina de la comunitat",
"Disinvite this user from community?": "Voleu retirar la invitació de aquest usuari a la comunitat?",
"Remove this user from community?": "Voleu eliminar de la comunitat a aquest usuari?",
@ -540,7 +412,6 @@
"Failed to remove user from community": "No s'ha pogut treure l'usuari de la comunitat",
"Filter community members": "Filtra els membres de la comunitat",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Esteu segur que voleu treure l'usuari '%(roomName)s' del grup %(groupId)s?",
"Home server URL": "URL del servidor d'origen",
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
"Removing a room from the community will also remove it from the community page.": "L'eliminació d'una sala de la comunitat també l'eliminarà de la pàgina de la comunitat.",
"Failed to remove room from community": "No s'ha pogut eliminar la sala de la comunitat",
@ -554,27 +425,21 @@
"Something went wrong when trying to get your communities.": "S'ha produït un error en intentar obtenir les vostres comunitats.",
"You're not currently a member of any communities.": "Actualment no sou membre de cap comunitat.",
"Unknown Address": "Adreça desconeguda",
"NOTE: Apps are not end-to-end encrypted": "NOTA: Les aplicacions no estan xifrades d'extrem a extrem",
"Do you want to load widget from URL:": "Carregar ginys des de l'URL:",
"Allow": "Permetre",
"Delete Widget": "Suprimeix el giny",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "La supressió d'un giny l'elimina per a tots els usuaris d'aquesta sala. Esteu segur que voleu eliminar aquest giny?",
"Delete widget": "Suprimeix el giny",
"Revoke widget access": "Revoca l'accés del giny",
"Minimize apps": "Minimitza les aplicacions",
"Blacklist": "Llista negre",
"Unverify": "Sense verificar",
"Verify...": "Verificar...",
"No results": "Sense resultats",
"Delete": "Esborra",
"Communities": "Comunitats",
"Home": "Inici",
"Integrations Error": "S'ha produït un error d'integració",
"Could not connect to the integration server": "No s'ha pogut connectar amb el servidor d'integració",
"Manage Integrations": "Gestiona les integracions",
"%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s han entrat",
"Guest access is disabled on this Home Server.": "L'accés a usuaris d'altres xarxes no està permès en aquest servidor.",
"Unblacklist": "Treure de la llista negre",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s s'ha unit",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",
@ -624,7 +489,6 @@
"%(items)s and %(lastItem)s": "%(items)s i %(lastItem)s",
"collapse": "col·lapsa",
"expand": "expandeix",
"Custom of %(powerLevel)s": "Personalitzat de %(powerLevel)s",
"Custom level": "Nivell personalitzat",
"And %(count)s more...|other": "I %(count)s més...",
"ex. @bob:example.com": "per exemple @carles:exemple.cat",
@ -634,21 +498,12 @@
"email address": "correu electrònic",
"Try using one of the following valid address types: %(validTypesList)s.": "Proveu d'utilitzar un dels següents tipus d'adreça vàlids: %(validTypesList)s.",
"You have entered an invalid address.": "No heu introduït una adreça vàlida.",
"Create a new chat or reuse an existing one": "Creeu un xat nou o feu-ne servit un d'existent",
"Start new chat": "Inicia un xat nou",
"You already have existing direct chats with this user:": "Ja teniu xats directes amb aquest usuari:",
"Start chatting": "Comença a xerrar",
"Click on the button below to start chatting!": "Feu clic al botó de sota per començar a xerrar!",
"Start Chatting": "Comenceu a xatejar",
"Confirm Removal": "Confirmeu l'eliminació",
"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.": "Esteu segur que voleu eliminar (suprimir) aquest esdeveniment? Tingueu en compte que si suprimiu un nom sala o si feu un canvi de tema, desfaria el canvi.",
"This room is not showing flair for any communities": "Aquesta sala no mostra talent per a cap comunitat",
"Flair": "Talents",
"Showing flair for these communities:": "Mostra els talents d'aquestes comunitats:",
"Flair will appear if enabled in room settings": "Els talents es mostraran si són activats a la configuració de la sala",
"Flair will not appear": "Els talents no es mostraran",
"Display your community flair in rooms configured to show it.": "Mostra els talents de la vostra comunitat dins les sales configurades per a mostrar-los.",
"This Home Server would like to make sure you are not a robot": "Aquest servidor amfitrió es vol assegurar que no sou un robot",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s han entrat %(count)s vegades",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s ha entrat %(count)s vegades",
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s han sortit %(count)s vegades",
@ -663,9 +518,6 @@
"example": "exemple",
"Create": "Crea",
"Create Room": "Crea una sala",
"Room name (optional)": "Nom de la sala (opcional)",
"Advanced options": "Opcions avançades",
"This setting cannot be changed later!": "Aquests paràmetres no es poden canviar després!",
"Unknown error": "S'ha produït un error desconegut",
"Incorrect password": "Contrasenya incorrecta",
"Deactivate Account": "Desactivar el compte",
@ -673,7 +525,6 @@
"Device name": "Nom del dispositiu",
"Device key": "Clau del dispositiu",
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Si coincideix, premeu el botó de verificació de sota. Si no coincideix, algú més està interceptant aquest dispositiu i probablement voleu prémer el botó de llista negra.",
"In future this verification process will be more sophisticated.": "En un futur, aquest procés de verificació serà més sofisticat.",
"Verify device": "Verifica el dispositiu",
"I verify that the keys match": "Verifico que les claus coincideixen",
"An error has occurred.": "S'ha produït un error.",
@ -693,7 +544,6 @@
"Unable to verify email address.": "No s'ha pogut verificar el correu electrònic.",
"This will allow you to reset your password and receive notifications.": "Això us permetrà restablir la vostra contrasenya i rebre notificacions.",
"Skip": "Omet",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "Els noms d'usuari només poden contenir lletres, números, punts, guionets i guionets baixos.",
"Username not available": "Aquest nom d'usuari no està disponible",
"Username invalid: %(errMessage)s": "El nom d'usuari és invàlid: %(errMessage)s",
"An error occurred: %(error_string)s": "S'ha produït un error: %(error_string)s",
@ -701,7 +551,6 @@
"To get started, please pick a username!": "Per començar, seleccioneu un nom d'usuari!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Aquest serà el nom del seu compte al <span></span> servidor amfitrió, o bé trieu-ne un altre <a>different server</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Si ja teniu un compte a Matrix, podeu <a>log in</a>.",
"Block users on other matrix homeservers from joining this room": "Impedeix als usuaris d'altres servidors de Matrix d'entrar a aquesta sala",
"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.": "Si anteriorment heu utilitzat un versió de Riot més recent, la vostra sessió podría ser incompatible amb aquesta versió. Tanqueu aquesta finestra i torneu a la versió més recent.",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Actualment teniu a la llista negre els dispositius no verificats; per enviar missatges a aquests dispositius, els heu de verificar abans.",
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Recomanem que dugueu a terme el procès de verificació per a cada dispositiu per tal de confirmar que són del legítim propietari, però podeu enviar el missatge sense verificar-ho si ho preferiu.",
@ -712,10 +561,6 @@
"Public Chat": "Xat públic",
"Custom": "Personalitzat",
"Name": "Nom",
"Topic": "Tema",
"Make this room private": "Fes que aquesta sala sigui privada",
"Share message history with new users": "Comparteix l'historial dels missatges amb els nous usuaris",
"Encrypt room": "Sala xifrada",
"You must <a>register</a> to use this functionality": "Heu de <a>register</a> per utilitzar aquesta funcionalitat",
"You must join the room to see its files": "Heu d'entrar a la sala per poder-ne veure els fitxers",
"There are no visible files in this room": "No hi ha fitxers visibles en aquesta sala",
@ -752,7 +597,6 @@
"Long Description (HTML)": "Descripció llarga (HTML)",
"Description": "Descripció",
"Community %(groupId)s not found": "No s'ha pogut trobar la comunitat %(groupId)s",
"This Home server does not support communities": "Aquest servidor amfitrió no admet comunitats",
"Failed to load %(groupId)s": "No s'ha pogut carregar la comunitat %(groupId)s",
"Reject invitation": "Rebutja la invitació",
"Are you sure you want to reject the invitation?": "Esteu segur que voleu rebutjar la invitació?",
@ -775,19 +619,14 @@
"Warning": "Avís",
"Connectivity to the server has been lost.": "S'ha perdut la connectivitat amb el servidor.",
"Sent messages will be stored until your connection has returned.": "Els missatges enviats s'emmagatzemaran fins que la vostra connexió hagi tornat.",
"%(count)s new messages|other": "%(count)s nous missatges",
"%(count)s new messages|one": "%(count)s nou missatge",
"Active call": "Trucada activa",
"You seem to be uploading files, are you sure you want to quit?": "Sembla que s'està pujant fitxers, esteu segur que voleu sortir?",
"You seem to be in a call, are you sure you want to quit?": "Sembla que està en una trucada, estàs segur que vols sortir?",
"Failed to upload file": "No s'ha pogut pujar el fitxer",
"Server may be unavailable, overloaded, or the file too big": "El servidor pot estar no disponible, sobrecarregat o el fitxer és massa gran",
"Search failed": "No s'ha pogut cercar",
"Server may be unavailable, overloaded, or search timed out :(": "Pot ser que el servidor no estigui disponible, que estigui sobrecarregat o que s'ha esgotat el temps de cerca :(",
"No more results": "No hi ha més resultats",
"Unknown room %(roomId)s": "La sala %(roomId)s és desconeguda",
"Room": "Sala",
"Failed to save settings": "No s'ha pogut desar la configuració",
"Failed to reject invite": "No s'ha pogut rebutjar la invitació",
"Fill screen": "Emplena la pantalla",
"Click to unmute video": "Feu clic per activar el so de vídeo",
@ -806,26 +645,15 @@
"Uploading %(filename)s and %(count)s others|zero": "Pujant %(filename)s",
"Light theme": "Tema clar",
"Dark theme": "Tema fosc",
"Status.im theme": "Tema d'Status.im",
"Sign out": "Tancar sessió",
"Remove %(threePid)s?": "Esborrar %(threePid)s?",
"Interface Language": "Idioma de l'interfície",
"User Interface": "Interfície d'usuari",
"Import E2E room keys": "Importar claus E2E de sala",
"Cryptography": "Criptografia",
"Device ID:": "ID del dispositiu:",
"Device key:": "Clau del dispositiu:",
"Ignored Users": "Usuaris ignorats",
"Labs": "Laboraroris",
"Use with caution": "Utilitzar amb precaució",
"Deactivate my account": "Desactivar la meva compte",
"Updates": "Actualitzacions",
"matrix-react-sdk version:": "Versió de matrix-react-sdk:",
"riot-web version:": "Versió de riot-web:",
"olm version:": "Versió d'olm:",
"Your password has been reset": "La teva contrasenya s'ha reiniciat",
"New password": "Nova contrasenya",
"Confirm your new password": "Confirma la teva nova contrasenya",
"Incorrect username and/or password.": "Usuari i/o contrasenya incorrectes.",
"The phone number entered looks invalid": "El número de telèfon introduït sembla erroni",
"none": "cap",
@ -845,26 +673,21 @@
"Import": "Importa",
"The version of Riot.im": "La versió del Riot.im",
"Email": "Correu electrònic",
"Add email address": "Afegeix correu electrònic",
"I have verified my email address": "He verificat l'adreça de correu electrònic",
"Send Reset Email": "Envia email de reinici",
"Your homeserver's URL": "L'URL del vostre servidor personal",
"Your identity server's URL": "L'URL del vostre servidor d'identitat",
"Analytics": "Analítiques",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ha canviat el seu nom visible a %(displayName)s.",
"Server may be unavailable or overloaded": "El servidor pot estar inaccessible o sobrecarregat",
"Display name": "Nom visible",
"Identity Server is": "El servidor d'identitat és",
"Submit debug logs": "Enviar logs de depuració",
"The platform you're on": "La plataforma a la que estàs",
"Whether or not you're logged in (we don't record your user name)": "Si estàs identificat o no (no desem el teu nom d'usuari)",
"Your language of choice": "El teu idioma preferit",
"Which officially provided instance you are using, if any": "Quina instància oficial estàs utilitzant, si escau",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Si estàs utilitzant el mode Richtext del Rich Text Editor o no",
"The information being sent to us to help make Riot.im better includes:": "La informació enviada a Riot.im per ajudar-nos a millorar inclou:",
"Fetching third party location failed": "S'ha produït un error en obtenir la ubicació de tercers",
"A new version of Riot is available.": "Hi ha una versió nova del Riot disponible.",
"Couldn't load home page": "No s'ha pogut carregar la pàgina d'inici",
"Send Account Data": "Envia les dades del compte",
"Advanced notification settings": "Paràmetres avançats de notificacions",
"Uploading report": "S'està enviant l'informe",
@ -878,11 +701,7 @@
"Friday": "Divendres",
"Update": "Actualització",
"What's New": "Novetats",
"Add an email address above to configure email notifications": "Afegiu més amunt un correu electrònic per a configurar les notificacions",
"Expand panel": "Expandeix el panel",
"On": "Engegat",
"%(count)s Members|other": "%(count)s membres",
"Filter room names": "Filtra els noms de les sales",
"Changelog": "Registre de canvis",
"Waiting for response from server": "S'està esperant una resposta del servidor",
"Uploaded on %(date)s by %(user)s": "Pujat el %(date)s per l'usuari %(user)s",
@ -892,12 +711,9 @@
"delete the alias.": "esborra l'alies.",
"To return to your account in future you need to <u>set a password</u>": "Per poder tornar al vostre compte en un futur, heu de <u>set a password</u>",
"Forget": "Oblida",
"#example": "#exemple",
"Hide panel": "Amaga el panel",
"You cannot delete this image. (%(code)s)": "No podeu eliminar aquesta imatge. (%(code)s)",
"Cancel Sending": "Cancel·la l'enviament",
"This Room": "Aquesta sala",
"The Home Server may be too old to support third party networks": "El servidor local pot ser massa antic per a ser compatible amb xarxes de tercers",
"Resend": "Reenvia",
"Room not found": "No s'ha trobat la sala",
"Messages containing my display name": "Missatges que contenen el meu nom visible",
@ -906,10 +722,8 @@
"Error saving email notification preferences": "No s'han pogut desar les preferències de les notificacions a causa d'un error",
"View Decrypted Source": "Mostra el codi desxifrat",
"Failed to update keywords": "No s'han pogut actualitzar les paraules clau",
"Notes:": "Notes:",
"remove %(name)s from the directory.": "elimina %(name)s del directori.",
"Notifications on the following keywords follow rules which cant be displayed here:": "Les notificacions sobre les següents paraules clau segueixen regles que no es poden mostrar aquí:",
"<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.": "<safariLink>Safari</safariLink> i <operaLink>Opera</operaLink> també fan la feina.",
"Please set a password!": "Si us plau, establiu una contrasenya",
"You have successfully set a password!": "Heu establert correctament la contrasenya",
"An error occurred whilst saving your email notification preferences.": "S'ha produït un error en desar les vostres preferències de notificació per correu electrònic.",
@ -920,14 +734,11 @@
"Members": "Membres",
"No update available.": "No hi ha cap actualització disponible.",
"Noisy": "Sorollós",
"Failed to get protocol list from Home Server": "No s'ha pogut obtenir la llista de protocols del servidor local",
"Collecting app version information": "S'està recollint la informació de la versió de l'aplicació",
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Voleu esborrar de la sala l'alies %(alias)s i retirar %(name)s del directori?",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Això farà possible que pugueu tronar al vostre compte des de qualsevol dispositiu.",
"Enable notifications for this account": "Habilita les notificacions per aquest compte",
"Directory": "Directori",
"Invite to this community": "Convida a aquesta comunitat",
"Search for a room": "Busca una sala",
"Search…": "Cerca…",
"Messages containing <span>keywords</span>": "Missatges que contenen <span>keywords</span>",
"When I'm invited to a room": "Quan sóc convidat a una sala",
@ -949,17 +760,12 @@
"Monday": "Dilluns",
"All messages (noisy)": "Tots els missatges (sorollós)",
"Enable them now": "Habilita-ho ara",
"Messages containing my user name": "Missatges que contenen el meu nom d'usuari",
"Toolbox": "Caixa d'eines",
"Collecting logs": "S'estan recopilant els registres",
"more": "més",
"GitHub issue link:": "Enllaç de l'issue de GitHub:",
"Failed to get public room list": "No s'ha pogut obtenir el llistat de sales públiques",
"You must specify an event type!": "Heu d'especificar un tipus d'esdeveniment",
"(HTTP status %(httpStatus)s)": "(Estat de l´HTTP %(httpStatus)s)",
"All Rooms": "Totes les sales",
"State Key": "Clau d'estat",
"Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.": "Si us plau, instal·leu <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> per gaudir d'una millor experiència.",
"Wednesday": "Dimecres",
"Send logs": "Envia els registres",
"All messages": "Tots els missatges",
@ -992,28 +798,23 @@
"Messages in group chats": "Missatges en xats de grup",
"Yesterday": "Ahir",
"Error encountered (%(errorDetail)s).": "S'ha trobat un error (%(errorDetail)s).",
"Login": "Inicia sessió",
"Low Priority": "Baixa prioritat",
"Unable to fetch notification target list": "No s'ha pogut obtenir la llista d'objectius de les notificacions",
"Set Password": "Establiu una contrasenya",
"Enable audible notifications in web client": "Habilita les notificacions d'àudio al client web",
"Off": "Apagat",
"Riot does not know how to join a room on this network": "El Riot no sap com unir-se a una sala en aquesta xarxa",
"Mentions only": "Només mencions",
"Failed to remove tag %(tagName)s from room": "No s'ha pogut esborrar l'etiqueta %(tagName)s de la sala",
"You can now return to your account after signing out, and sign in on other devices.": "Ara podreu tornar a entrar al vostre compte des de altres dispositius.",
"Enable desktop notifications": "Habilita les notificacions d'escriptori",
"Enable email notifications": "Habilita les notificacions per correu electrònic",
"Event Type": "Tipus d'esdeveniment",
"Download this file": "Descarrega aquest fitxer",
"Pin Message": "Enganxa el missatge",
"Failed to change settings": "No s'han pogut modificar els paràmetres",
"View Community": "Mira la communitat",
"%(count)s Members|one": "%(count)s membre",
"Event sent!": "S'ha enviat l'esdeveniment",
"Event Content": "Contingut de l'esdeveniment",
"Thank you!": "Gràcies!",
"Collapse panel": "Col·lapsa el tauler",
"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!": "Amb el vostre navegador actual, l'aparença de l'aplicació pot ser completament incorrecta i algunes o totes les funcions poden no funcionar correctament. Si voleu provar-ho de totes maneres, podeu continuar, però esteu sols pel que fa als problemes que pugueu trobar!",
"Checking for an update...": "Comprovant si hi ha actualitzacions...",
"There are advanced notifications which are not shown here": "Hi ha notificacions avançades que no es mostren aquí",
@ -1024,7 +825,6 @@
"Your device resolution": "La resolució del vostre dispositiu",
"Show Stickers": "Mostra els adhesius",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quan aquesta pàgina contingui informació identificable, com per exemple una sala, usuari o ID de grup, aquestes dades se suprimeixen abans d'enviar-se al servidor.",
"A conference call could not be started because the intgrations server is not available": "No s'ha pogut iniciar la trucada de conferència perquè el servidor d'integració no és disponible",
"Call in Progress": "Trucada en curs",
"A call is currently being placed!": "S'està fent una trucada en aquest moment!",
"A call is already in progress!": "Ja hi ha una trucada en curs!",
@ -1034,18 +834,14 @@
"Registration Required": "Es requereix registre",
"You need to register to do this. Would you like to register now?": "Us heu de registrar per fer això. Us voleu registrar ara?",
"Failed to invite users to the room:": "No s'ha pogut convidar els usuaris a aquesta sala:",
"Waiting for %(userId)s to accept...": "S'està esperant a que %(userId)s accepti...",
"Waiting for %(userId)s to confirm...": "S'està esperant a que %(userId)s confirmi...",
"Missing roomId.": "Manca l'ID de la sala.",
"Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats",
"Changes your display nickname": "Canvia el vostre malnom",
"Changes colour scheme of current room": "Canvia l'esquema de colors de la sala actual",
"Sets the room topic": "Estableix el tema de la sala",
"Invites user with given id to current room": "Convida l'usuari amb l'id donat a la sala actual",
"Joins room with given alias": "Us introdueix a la sala amb l'àlies donat",
"Kicks user with given id": "Expulsa l'usuari amb l'id donat",
"Bans user with given id": "Bandeja l'usuari amb l'id donat",
"Unbans user with given id": "Permet l'accés a l'usuari amb l'id donat",
"Ignores a user, hiding their messages from you": "Ignora un usuari, amagant-te els seus missatges",
"Stops ignoring a user, showing their messages going forward": "Deixa d'ignorar un usuari, mostrant els seus missatges ara en avant",
"Define the power level of a user": "Defineix el nivell d'autoritat d'un usuari",
@ -1053,7 +849,6 @@
"Opens the Developer Tools dialog": "Obre el diàleg d'Eines del desenvolupador",
"Verifies a user, device, and pubkey tuple": "Verifica un usuari, dispositiu i tupla de clau pública",
"Displays action": "Mostra l'acció",
"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.": "També podeu establir un servidor d'identitat personalitzat, però no podreu convidar usuaris o ser convidats vosaltres mateixos per adreça de correu electrònic.",
"Whether or not you're logged in (we don't record your username)": "Si heu iniciat sessió o no (no desem el vostre usuari)",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fitxer %(fileName)s supera el límit de pujades del servidor",
"Upgrades a room to a new version": "Actualitza la sala a una versió nova",
@ -1109,10 +904,8 @@
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "No s'ha trobat el perfil pels IDs de Matrix següents, els voleu convidar igualment?",
"Invite anyway and never warn me again": "Convidar igualment i no avisar-me de nou",
"Invite anyway": "Convidar igualment",
"Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Feu servir una adreça de correu electrònic per recuperar el vostre compte. Altres usuaris us poden convidar a sales amb les vostres dades de contacte.",
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Sou administradors d'aquesta comunitat. No hi podreu tornar a entrar sense que un altre administrador us convidi.",
"Guest": "Visitant",
"Guest access is disabled on this homeserver.": "L'accés als visitants està desactivat en aquest servidor.",
"This homeserver has hit its Monthly Active User limit.": "Aquest homeserver ha assolit el seu límit d'usuaris actius mensuals.",
"This homeserver has exceeded one of its resource limits.": "Aquest homeserver ha sobrepassat on dels seus límits de recursos.",
"Please <a>contact your service administrator</a> to continue using the service.": "Si us plau <a>poseu-vos en contacte amb l'administrador</a> per poder continuar usant el servei.",

File diff suppressed because it is too large Load diff

View file

@ -42,45 +42,25 @@
"Sign in": "Log ind",
"Warning!": "Advarsel!",
"Account": "Konto",
"Add email address": "Tilføj e-mail-adresse",
"Add phone number": "Tilføj telefonnummer",
"Admin": "Administrator",
"Advanced": "Avanceret",
"Anyone who knows the room's link, apart from guests": "Alle der kender link til rummet, bortset fra gæster",
"Anyone who knows the room's link, including guests": "Alle der kender link til rummet, inklusiv gæster",
"Are you sure you want to reject the invitation?": "Er du sikker på du vil afvise invitationen?",
"Are you sure you want to upload the following files?": "Er du sikker på du vil sende de følgende filer?",
"Banned users": "Bortviste brugere",
"Bulk Options": "Masseindstillinger",
"Can't load user settings": "Kan ikke indlæse brugerindstillinger",
"Changes to who can read history will only apply to future messages in this room": "Ændringer til hvem der kan læse historie gælder kun for fremtidige meddelelser i dette rum",
"Clear Cache and Reload": "Ryd cache og genindlæs",
"Clear Cache": "Ryd cache",
"Click here to fix": "Klik her for at rette",
"Confirm your new password": "Bekræft din nye adgangskode",
"Continue": "Fortsæt",
"Could not connect to the integration server": "Kunne ikke oprette forbindelse til integrationsserveren",
"Create an account": "Opret en brugerkonto",
"Create Room": "Opret rum",
"Cryptography": "Kryptografi",
"Deactivate Account": "Deaktiver brugerkonto",
"Deactivate my account": "Deaktiver min brugerkonto",
"Default": "Standard",
"Devices will not yet be able to decrypt history from before they joined the room": "Enhederne vil ikke være i stand til at dekryptere historikken fra, før de kom til rummet",
"Display name": "Visningsnavn",
"Email, name or matrix ID": "E-mail, navn eller matrix-ID",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Krypterede meddelelser vil ikke være synlige på klienter, der endnu ikke implementerer kryptering",
"Encrypted room": "Krypteret rummet",
"Encryption is enabled in this room": "Kryptering er aktiveret i dette rum",
"Encryption is not enabled in this room": "Kryptering er ikke aktiveret i dette rum",
"End-to-end encryption is in beta and may not be reliable": "End-to-end kryptering er i beta og kan ikke være pålidelig",
"Error": "Fejl",
"Export E2E room keys": "Eksporter E2E rum nøgler",
"Failed to change password. Is your password correct?": "Kunne ikke ændre password. Er dit password korrekt?",
"Failed to leave room": "Kunne ikke forlade rum",
"Failed to reject invitation": "Kunne ikke afvise invitationen",
"Failed to send email": "Kunne ikke sende e-mail",
"Failed to set avatar.": "Kunne ikke angive avatar.",
"Failed to unban": "Var ikke i stand til at ophæve forbuddet",
"Favourite": "Favorit",
"Notifications": "Notifikationer",
@ -89,7 +69,6 @@
"unknown error code": "Ukendt fejlkode",
"%(targetName)s accepted an invitation.": "%(targetName)s accepterede en invitation.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepterede invitationen til %(displayName)s.",
"%(names)s and %(lastPerson)s are typing": "%(names)s og %(lastPerson)s er ved at skrive",
"%(senderName)s answered the call.": "%(senderName)s besvarede opkaldet.",
"Add a widget": "Tilføj en widget",
"OK": "OK",
@ -121,13 +100,6 @@
"VoIP is unsupported": "VoIP er ikke understøttet",
"You cannot place VoIP calls in this browser.": "Du kan ikke lave VoIP-opkald i denne browser.",
"You cannot place a call with yourself.": "Du kan ikke ringe til dig selv.",
"Conference calls are not supported in this client": "Konferenceopkald er ikke undersøttede i denne klient",
"Conference calls are not supported in encrypted rooms": "Konferenceopkald er ikke understøttede i krypterede rum",
"Conference calling is in development and may not be reliable.": "Konferenceopkald er under udvikling og kan være utilregnelige.",
"Failed to set up conference call": "Kunne ikke starte konferenceopkald",
"Conference call failed.": "Konferenceopkald fejlede.",
"The file '%(fileName)s' failed to upload": "Filen '%(fileName)s' kunne ikke uploades",
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "Filen '%(fileName)s' overskrider denne home servers størrelsesbegrænsning på uploads",
"Upload Failed": "Upload Fejlede",
"Sun": "Søn",
"Mon": "Man",
@ -156,7 +128,6 @@
"Who would you like to add to this community?": "Hvem vil du tilføje til dette fællesskab?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Advarsel: alle personer du tilføjer til et fællesskab vil være synlige for alle der kender Fællesskabs ID'et",
"Invite new community members": "Inviter nye fællesskabsmedlemmer",
"Name or matrix ID": "Navn eller matrix ID",
"Invite to Community": "Inviter til Fællesskab",
"Which rooms would you like to add to this community?": "Hvilke rum vil du tilføje til dette fællesskab?",
"Show these rooms to non-members on the community page and room list?": "Vis disse rum til ikke-medlemmer på fællesskabssiden og rumlisten?",
@ -178,9 +149,7 @@
"Who would you like to communicate with?": "Hvem vil du kommunikere med?",
"Start Chat": "Start Chat",
"Invite new room members": "Inviter nye rummedlemmer",
"Who would you like to add to this room?": "Hvem vil du tilføje til dette rum?",
"Send Invites": "Send invitationer",
"Failed to invite user": "Kunne ikke invitere bruger",
"Operation failed": "Operation mislykkedes",
"Failed to invite": "Kunne ikke invitere",
"Failed to invite the following users to the %(roomName)s room:": "Kunne ikke invitere de følgende brugere til %(roomName)s rummet:",
@ -237,13 +206,11 @@
"(no answer)": "(intet svar)",
"(unknown failure: %(reason)s)": "(ukendt fejl: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s afsluttede opkaldet.",
"%(senderName)s placed a %(callType)s call.": "%(senderName)s startede et %(callType)s opkald.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s inviterede %(targetDisplayName)s til rummet.",
"Submit debug logs": "Indsend debug-logfiler",
"Online": "Online",
"Fetching third party location failed": "Hentning af tredjeparts placering mislykkedes",
"A new version of Riot is available.": "En ny version a Riot er tilgængelig.",
"Couldn't load home page": "Kunne ikke indlæse startsiden",
"Send Account Data": "Send Konto Data",
"All notifications are currently disabled for all targets.": "Alle meddelelser er for øjeblikket deaktiveret for alle mål.",
"Uploading report": "Uploader rapport",
@ -257,10 +224,7 @@
"Friday": "Fredag",
"Update": "Opdater",
"What's New": "Hvad er nyt",
"Add an email address above to configure email notifications": "Tilføj en emailadresse ovenfor for at konfigurere e-mail-underretninger",
"Expand panel": "Udvid panel",
"On": "Tændt",
"Filter room names": "Filtrér rumnavne",
"Changelog": "Ændringslog",
"Waiting for response from server": "Venter på svar fra server",
"Uploaded on %(date)s by %(user)s": "Uploadet den %(date)s af %(user)s",
@ -270,13 +234,10 @@
"delete the alias.": "Slet aliaset.",
"To return to your account in future you need to <u>set a password</u>": "For at komme ind på din konto i fremtiden skal du <u>indstille et password</u>",
"Forget": "Glem",
"#example": "#eksempel",
"Hide panel": "Gem panel",
"You cannot delete this image. (%(code)s)": "Du kan ikke slette dette billede. (%(code)s)",
"Cancel Sending": "Stop Forsendelse",
"Warning": "Advarsel",
"This Room": "Dette rum",
"The Home Server may be too old to support third party networks": "Hjemmeserveren kan være for gammel til at understøtte tredjepartsnetværk",
"Room not found": "Rummet ikke fundet",
"Messages containing my display name": "Beskeder der indeholder mit viste navn",
"Messages in one-to-one chats": "Beskeder i en-til-en chats",
@ -286,7 +247,6 @@
"Failed to update keywords": "Kunne ikke opdatere søgeord",
"remove %(name)s from the directory.": "fjern %(name)s fra kataloget.",
"Notifications on the following keywords follow rules which cant be displayed here:": "Meddelelser om følgende søgeord følger regler, der ikke kan vises her:",
"<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.": "<safariLink>Safari</safariLink> og <operaLink>Opera</operaLink> virker også.",
"Please set a password!": "Indstil venligst et password!",
"You have successfully set a password!": "Du har succesfuldt indstillet et password!",
"An error occurred whilst saving your email notification preferences.": "Der opstod en fejl under opbevaring af dine e-mail-underretningsindstillinger.",
@ -297,15 +257,12 @@
"Members": "Medlemmer",
"No update available.": "Ingen opdatering tilgængelig.",
"Noisy": "Støjende",
"Failed to get protocol list from Home Server": "Kunne ikke få protokolliste fra Home Server",
"Collecting app version information": "Indsamler app versionsoplysninger",
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Slet rumaliaset %(alias)s og fjern %(name)s fra kataloget?",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Dette vil tillade dig at vende tilbage til din konto efter at have logget ud og at logge ind på andre enheder.",
"Keywords": "Søgeord",
"Enable notifications for this account": "Aktivér underretninger for dette brugernavn",
"Directory": "Rum katalog",
"Invite to this community": "Inviter til dette fællesskab",
"Search for a room": "Søg efter et rum",
"Search…": "Søg…",
"Messages containing <span>keywords</span>": "Beskeder der indeholder <span>keywords</span>",
"When I'm invited to a room": "Når jeg bliver inviteret til et rum",
@ -325,16 +282,12 @@
"Monday": "Mandag",
"Remove from Directory": "Fjern fra Katalog",
"Enable them now": "Aktivér dem nu",
"Messages containing my user name": "Beskeder der indeholder mit brugernavn",
"Toolbox": "Værktøjer",
"Collecting logs": "Indsamler logfiler",
"more": "mere",
"You must specify an event type!": "Du skal angive en begivenhedstype!",
"(HTTP status %(httpStatus)s)": "(HTTP tilstand %(httpStatus)s)",
"Invite to this room": "Inviter til dette rum",
"State Key": "Tilstandsnøgle",
"Please install <chromeLink>Chrome</chromeLink> or <firefoxLink>Firefox</firefoxLink> for the best experience.": "Installer venligst <chromeLink>Chrome</chromeLink> eller <firefoxLink>Firefox</firefoxLink> for den bedste oplevelse.",
"Failed to get public room list": "Kunne ikke få offentlig rumliste",
"Send": "Send",
"Send logs": "Send logs",
"All messages": "Alle beskeder",
@ -367,36 +320,28 @@
"Low Priority": "Lav prioritet",
"Unable to fetch notification target list": "Kan ikke hente meddelelsesmålliste",
"Set Password": "Indstil Password",
"Enable audible notifications in web client": "Aktivér hørbare underretninger i webklienten",
"Resend": "Send igen",
"Riot does not know how to join a room on this network": "Riot ved ikke, hvordan man kan deltage i et rum på dette netværk",
"Mentions only": "Kun nævninger",
"Failed to remove tag %(tagName)s from room": "Kunne ikke fjerne tag(s): %(tagName)s fra rummet",
"Wednesday": "Onsdag",
"You can now return to your account after signing out, and sign in on other devices.": "Du kan nu vende tilbage til din konto efter at have logget ud og logge ind på andre enheder.",
"Enable desktop notifications": "Aktivér desktop meddelelser",
"Enable email notifications": "Aktivér e-mail-underretninger",
"Login": "Log ind",
"Download this file": "Download denne fil",
"Pin Message": "Fasthold Besked",
"Failed to change settings": "Kunne ikke ændre indstillinger",
"Developer Tools": "Udviklingsværktøjer",
"Event Content": "Begivenhedsindhold",
"Thank you!": "Tak!",
"Collapse panel": "Slå panel sammen",
"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!": "Med din nuværnde broser kan udseendet og fornemmelsen af programmet være helt forkert og nogle funktioner virker måske ikke. Hvis du alligevel vil prøve så kan du fortsætte, men det er på egen risiko!",
"Checking for an update...": "Checker om der er en opdatering...",
"There are advanced notifications which are not shown here": "Der er avancerede meddelelser, som ikke vises her",
"%(count)s Members|other": "%(count)s medlemmer",
"Logs sent": "Logfiler sendt",
"Reply": "Besvar",
"All messages (noisy)": "Alle meddelelser (højlydt)",
"GitHub issue link:": "Link til GitHub issue:",
"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-logfiler indeholder brugerdata såsom brugernavn, ID'er eller aliaser for de rum eller grupper, du har besøgt, og andres brugernavne. De indeholder ikke meddelelser.",
"Failed to send logs: ": "Kunne ikke sende logfiler: ",
"View Community": "Vis community",
"%(count)s Members|one": "%(count)s medlem",
"Notes:": "Noter:",
"Preparing to send logs": "Forbereder afsendelse af logfiler",
"The platform you're on": "Den platform du bruger",
"The version of Riot.im": "Versionen af Riot.im",
@ -463,7 +408,6 @@
"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 />.": "Rumopgraderinger påvirker normalt kun <i>servernes</i> behandling af rum. Hvis du har problemer med din Riot-klient bedes du oprette en sag på <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>Advarsel:</b> Opgradering af et rum <i>flytter ikke automatisk rummets medlemmer til den nye version af rummet.</i> Vi sender et link til den nye version i den gamle version af rummet - rummets medlemmer må klikke på dette link for at tilgå det nye rum.",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Bekræft venligst at du vil fortsætte med at opgradere rummet fra <oldVersion /> til <newVersion />.",
"Upgrade": "Opgradering",
"Changes your display nickname in the current room only": "Ændrer dit viste navn kun for det nuværende rum",
"Changes the avatar of the current room": "Ændrer avataren af det nuværende rum",
"Changes your avatar in this current room only": "Ændrer din avatar kun for det nuværende rum",

File diff suppressed because it is too large Load diff

View file

@ -12,10 +12,7 @@
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s δέχτηκες την πρόσκληση για %(displayName)s.",
"Account": "Λογαριασμός",
"Add a topic": "Προσθήκη θέματος",
"Add email address": "Προσθήκη διεύθυνσης ηλ. αλληλογραφίας",
"Add phone number": "Προσθήκη αριθμού",
"Admin": "Διαχειριστής",
"VoIP": "VoIP",
"No Microphones detected": "Δεν εντοπίστηκε μικρόφωνο",
"No Webcams detected": "Δεν εντοπίστηκε κάμερα",
"Default Device": "Προεπιλεγμένη συσκευή",
@ -23,7 +20,6 @@
"Camera": "Κάμερα",
"Advanced": "Προχωρημένα",
"Algorithm": "Αλγόριθμος",
"Hide removed messages": "Απόκρυψη διαγραμμένων μηνυμάτων",
"Authentication": "Πιστοποίηση",
"A new password must be entered.": "Ο νέος κωδικός πρόσβασης πρέπει να εισαχθεί.",
"%(senderName)s answered the call.": "Ο χρήστης %(senderName)s απάντησε την κλήση.",
@ -32,36 +28,27 @@
"Are you sure?": "Είστε σίγουροι;",
"Are you sure you want to leave the room '%(roomName)s'?": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από το δωμάτιο '%(roomName)s';",
"Are you sure you want to reject the invitation?": "Είστε σίγουροι ότι θέλετε να απορρίψετε την πρόσκληση;",
"Are you sure you want to upload the following files?": "Είστε σίγουροι ότι θέλετε να αποστείλετε τα ακόλουθα αρχεία;",
"Attachment": "Επισύναψη",
"%(senderName)s banned %(targetName)s.": "Ο χρήστης %(senderName)s έδιωξε τον χρήστη %(targetName)s.",
"Autoplay GIFs and videos": "Αυτόματη αναπαραγωγή GIFs και βίντεο",
"Anyone who knows the room's link, apart from guests": "Oποιοσδήποτε",
"%(items)s and %(lastItem)s": "%(items)s %(lastItem)s",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Ένα μήνυμα στάλθηκε στο +%(msisdn)s. Παρακαλώ γράψε τον κωδικό επαλήθευσης που περιέχει",
"Access Token:": "Κωδικός πρόσβασης:",
"Always show message timestamps": "Εμφάνιση πάντα της ένδειξης ώρας στα μηνύματα",
"and %(count)s others...|one": "και ένας ακόμα...",
"and %(count)s others...|other": "και %(count)s άλλοι...",
"%(names)s and %(lastPerson)s are typing": "%(names)s και %(lastPerson)s γράφουν",
"Anyone who knows the room's link, including guests": "Οποιοσδήποτε γνωρίζει τον σύνδεσμο του δωματίου, συμπεριλαμβανομένων των επισκεπτών",
"Blacklisted": "Στη μαύρη λίστα",
"Can't load user settings": "Δεν είναι δυνατή η φόρτωση των ρυθμίσεων χρήστη",
"Change Password": "Αλλαγή κωδικού πρόσβασης",
"%(senderName)s changed their profile picture.": "Ο %(senderName)s άλλαξε τη φωτογραφία του προφίλ του.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "Ο %(senderDisplayName)s άλλαξε το όνομα του δωματίου σε %(roomName)s.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "Ο %(senderDisplayName)s άλλαξε το θέμα σε \"%(topic)s\".",
"Clear Cache and Reload": "Εκκαθάριση μνήμης και ανανέωση",
"Clear Cache": "Εκκαθάριση μνήμης",
"Bans user with given id": "Αποκλεισμός χρήστη με το συγκεκριμένο αναγνωριστικό",
"%(senderDisplayName)s removed the room name.": "Ο %(senderDisplayName)s διέγραψε το όνομα του δωματίου.",
"Changes your display nickname": "Αλλάζει το ψευδώνυμο χρήστη",
"Conference call failed.": "Η κλήση συνδιάσκεψης απέτυχε.",
"powered by Matrix": "λειτουργεί με το Matrix",
"Confirm password": "Επιβεβαίωση κωδικού πρόσβασης",
"Confirm your new password": "Επιβεβαίωση του νέου κωδικού πρόσβασης",
"Continue": "Συνέχεια",
"Create an account": "Δημιουργία λογαριασμού",
"Create Room": "Δημιουργία δωματίου",
"Cryptography": "Κρυπτογραφία",
"Current password": "Τωρινός κωδικός πρόσβασης",
@ -69,10 +56,8 @@
"Custom level": "Προσαρμοσμένο επίπεδο",
"/ddg is not a command": "/ddg δεν αναγνωρίζεται ως εντολή",
"Deactivate Account": "Απενεργοποίηση λογαριασμού",
"Deactivate my account": "Απενεργοποίηση του λογαριασμού μου",
"Decrypt %(text)s": "Αποκρυπτογράφηση %(text)s",
"Decryption error": "Σφάλμα αποκρυπτογράφησης",
"Delete": "Διαγραφή",
"Default": "Προεπιλογή",
"Device already verified!": "Η συσκευή έχει ήδη επαληθευτεί!",
"Device ID": "Αναγνωριστικό συσκευής",
@ -82,16 +67,11 @@
"Devices": "Συσκευές",
"Direct chats": "Απευθείας συνομιλίες",
"Disinvite": "Ανάκληση πρόσκλησης",
"Display name": "Όνομα χρήστη",
"Download %(text)s": "Λήψη %(text)s",
"Ed25519 fingerprint": "Αποτύπωμα Ed25519",
"Email": "Ηλεκτρονική διεύθυνση",
"Email address": "Ηλεκτρονική διεύθυνση",
"Email address (optional)": "Ηλεκτρονική διεύθυνση (προαιρετικό)",
"Email, name or matrix ID": "Ηλεκτρονική διεύθυνση, όνομα ή matrix ID",
"Emoji": "Εικονίδια",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Τα κρυπτογραφημένα μηνύματα δεν θα είναι ορατά σε εφαρμογές που δεν παρέχουν τη δυνατότητα κρυπτογράφησης",
"Encrypted room": "Κρυπτογραφημένο δωμάτιο",
"%(senderName)s ended the call.": "%(senderName)s τερμάτισε την κλήση.",
"End-to-end encryption information": "Πληροφορίες σχετικά με τη κρυπτογράφηση από άκρο σε άκρο (End-to-end encryption)",
"Error decrypting attachment": "Σφάλμα κατά την αποκρυπτογράφηση της επισύναψης",
@ -105,7 +85,6 @@
"Failed to mute user": "Δεν ήταν δυνατή η σίγαση του χρήστη",
"Failed to reject invite": "Δεν ήταν δυνατή η απόρριψη της πρόσκλησης",
"Failed to reject invitation": "Δεν ήταν δυνατή η απόρριψη της πρόσκλησης",
"Failed to save settings": "Δεν ήταν δυνατή η αποθήκευση των ρυθμίσεων",
"Failed to send email": "Δεν ήταν δυνατή η αποστολή ηλ. αλληλογραφίας",
"Failed to verify email address: make sure you clicked the link in the email": "Δεν ήταν δυνατή η επιβεβαίωση της διεύθυνσης ηλεκτρονικής αλληλογραφίας: βεβαιωθείτε οτι κάνατε κλικ στον σύνδεσμο που σας στάλθηκε",
"Favourite": "Αγαπημένο",
@ -113,9 +92,7 @@
"Fill screen": "Γέμισε την οθόνη",
"Filter room members": "Φιλτράρισμα μελών",
"Forget room": "Αγνόηση δωματίου",
"Forgot your password?": "Ξεχάσατε τoν κωδικό πρόσβασης σας;",
"For security, this session has been signed out. Please sign in again.": "Για λόγους ασφαλείας, αυτή η συνεδρία έχει τερματιστεί. Παρακαλούμε συνδεθείτε ξανά.",
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Για λόγους ασφαλείας, τα κλειδιά κρυπτογράφησης θα διαγράφονται από τον περιηγητή κατά την αποσύνδεση σας. Εάν επιθυμείτε να αποκρυπτογραφήσετε τις συνομιλίες σας στο μέλλον, εξάγετε τα κλειδιά σας και κρατήστε τα ασφαλή.",
"Hangup": "Κλείσιμο",
"Historical": "Ιστορικό",
"Homeserver is": "Ο διακομιστής είναι",
@ -125,12 +102,10 @@
"Import E2E room keys": "Εισαγωγή κλειδιών E2E",
"Incorrect username and/or password.": "Λανθασμένο όνομα χρήστη και/ή κωδικός.",
"Incorrect verification code": "Λανθασμένος κωδικός επαλήθευσης",
"Interface Language": "Γλώσσα διεπαφής",
"Invalid Email Address": "Μη έγκυρη διεύθυνση ηλεκτρονικής αλληλογραφίας",
"Invite new room members": "Προσκαλέστε νέα μέλη",
"Invited": "Προσκλήθηκε",
"Invites": "Προσκλήσεις",
"%(displayName)s is typing": "Ο χρήστης %(displayName)s γράφει",
"Sign in with": "Συνδεθείτε με",
"%(targetName)s joined the room.": "ο %(targetName)s συνδέθηκε στο δωμάτιο.",
"Jump to first unread message.": "Πηγαίνετε στο πρώτο μη αναγνωσμένο μήνυμα.",
@ -141,27 +116,19 @@
"Leave room": "Αποχώρηση από το δωμάτιο",
"%(targetName)s left the room.": "Ο χρήστης %(targetName)s έφυγε από το δωμάτιο.",
"Local addresses for this room:": "Τοπική διεύθυνση για το δωμάτιο:",
"Logged in as:": "Συνδεθήκατε ως:",
"Logout": "Αποσύνδεση",
"Low priority": "Χαμηλής προτεραιότητας",
"matrix-react-sdk version:": "Έκδοση matrix-react-sdk:",
"Message not sent due to unknown devices being present": "Το μήνυμα δεν στάλθηκε γιατί υπάρχουν άγνωστες συσκευές",
"Mobile phone number": "Αριθμός κινητού τηλεφώνου",
"Click here to fix": "Κάνε κλικ εδώ για διόρθωση",
"Command error": "Σφάλμα εντολής",
"Commands": "Εντολές",
"Conference calls are not supported in encrypted rooms": "Οι κλήσεις συνδιάσκεψης δεν είναι υποστηρίζονται σε κρυπτογραφημένα δωμάτια",
"Conference calls are not supported in this client": "Οι κλήσεις συνδιάσκεψης δεν είναι υποστηρίζονται από την εφαρμογή",
"Enable encryption": "Ενεργοποίηση κρυπτογραφίας",
"Enter Code": "Εισαγωγή κωδικού πρόσβασης",
"Failed to send request.": "Δεν ήταν δυνατή η αποστολή αιτήματος.",
"Failed to upload file": "Δεν ήταν δυνατή η αποστολή του αρχείου",
"Failure to create room": "Δεν ήταν δυνατή η δημιουργία δωματίου",
"Join Room": "Είσοδος σε δωμάτιο",
"Moderator": "Συντονιστής",
"Name": "Όνομα",
"New address (e.g. #foo:%(localDomain)s)": "Νέα διεύθυνση (e.g. #όνομα:%(localDomain)s)",
"New password": "Νέος κωδικός πρόσβασης",
"New passwords don't match": "Οι νέοι κωδικοί πρόσβασης είναι διαφορετικοί",
"New passwords must match each other.": "Οι νέοι κωδικοί πρόσβασης πρέπει να ταιριάζουν.",
"none": "κανένα",
@ -172,14 +139,12 @@
"OK": "Εντάξει",
"olm version:": "Έκδοση olm:",
"Password": "Κωδικός πρόσβασης",
"Password:": "Κωδικός πρόσβασης:",
"Passwords can't be empty": "Οι κωδικοί πρόσβασης δεν γίνετε να είναι κενοί",
"People": "Άτομα",
"Phone": "Τηλέφωνο",
"Register": "Εγγραφή",
"riot-web version:": "Έκδοση riot-web:",
"Room Colour": "Χρώμα δωματίου",
"Room name (optional)": "Όνομα δωματίου (προαιρετικό)",
"Rooms": "Δωμάτια",
"Save": "Αποθήκευση",
"Search failed": "Η αναζήτηση απέτυχε",
@ -198,9 +163,6 @@
"Dismiss": "Απόρριψη",
"bold": "έντονα",
"italic": "πλάγια",
"underline": "υπογράμμιση",
"code": "κώδικας",
"quote": "παράθεση",
"Close": "Κλείσιμο",
"Create new room": "Δημιουργία νέου δωματίου",
"Room directory": "Ευρετήριο",
@ -213,52 +175,38 @@
"Alias (optional)": "Ψευδώνυμο (προαιρετικό)",
"Ban": "Αποκλεισμός",
"Banned users": "Αποκλεισμένοι χρήστες",
"Bulk Options": "Μαζικές επιλογές",
"Call Timeout": "Λήξη χρόνου κλήσης",
"<a>Click here</a> to join the discussion!": "<a>Κλικ εδώ</a> για να συμμετάσχετε στην συζήτηση!",
"Click to mute audio": "Κάντε κλικ για σίγαση του ήχου",
"Click to mute video": "Κάντε κλικ για σίγαση του βίντεο",
"click to reveal": "κάντε κλικ για εμφάνιση",
"Click to unmute video": "Κάντε κλικ για άρση σίγασης του βίντεο",
"Click to unmute audio": "Κάντε κλικ για άρση σίγασης του ήχου",
"%(count)s new messages|one": "%(count)s νέο μήνυμα",
"%(count)s new messages|other": "%(count)s νέα μηνύματα",
"Custom": "Προσαρμοσμένο",
"Decline": "Απόρριψη",
"Disable Notifications": "Απενεργοποίηση ειδοποιήσεων",
"Drop File Here": "Αποθέστε εδώ το αρχείο",
"Enable Notifications": "Ενεργοποίηση ειδοποιήσεων",
"Encryption is enabled in this room": "Η κρυπτογράφηση είναι ενεργοποιημένη σε αυτό το δωμάτιο",
"Encryption is not enabled in this room": "Η κρυπτογράφηση είναι απενεργοποιημένη σε αυτό το δωμάτιο",
"Enter passphrase": "Εισαγωγή συνθηματικού",
"Failed to set avatar.": "Δεν ήταν δυνατό ο ορισμός της προσωπικής εικόνας.",
"Failed to set display name": "Δεν ήταν δυνατό ο ορισμός του ονόματος εμφάνισης",
"Failed to set up conference call": "Δεν ήταν δυνατή η ρύθμιση κλήσης συνδιάσκεψης",
"Failed to toggle moderator status": "Δεν ήταν δυνατή η εναλλαγή κατάστασης του συντονιστή",
"Failed to upload profile picture!": "Δεν ήταν δυνατή η αποστολή της εικόνας προφίλ!",
"Hide read receipts": "Απόκρυψη αποδείξεων ανάγνωσης",
"Home": "Αρχική",
"Last seen": "Τελευταία εμφάνιση",
"Level:": "Επίπεδο:",
"Manage Integrations": "Διαχείριση ενσωματώσεων",
"Markdown is disabled": "Το Markdown είναι απενεργοποιημένο",
"Markdown is enabled": "Το Markdown είναι ενεργοποιημένο",
"Missing room_id in request": "Λείπει το room_id στο αίτημα",
"Permissions": "Δικαιώματα",
"Power level must be positive integer.": "Το επίπεδο δύναμης πρέπει να είναι ένας θετικός ακέραιος.",
"Privacy warning": "Προειδοποίηση ιδιωτικότητας",
"Private Chat": "Προσωπική συνομιλία",
"Privileged Users": "Προνομιούχοι χρήστες",
"Profile": "Προφίλ",
"Public Chat": "Δημόσια συνομιλία",
"Reason": "Αιτία",
"Reason: %(reasonText)s": "Αιτία: %(reasonText)s",
"Revoke Moderator": "Ανάκληση συντονιστή",
"%(targetName)s rejected the invitation.": "Ο %(targetName)s απέρριψε την πρόσκληση.",
"Reject invitation": "Απόρριψη πρόσκλησης",
"Remote addresses for this room:": "Απομακρυσμένες διευθύνσεις για το δωμάτιο:",
"Remove Contact Information?": "Αφαίρεση πληροφοριών επαφής;",
"Remove %(threePid)s?": "Αφαίρεση %(threePid)s;",
"Results from DuckDuckGo": "Αποτελέσματα από DuckDuckGo",
"Return to login screen": "Επιστροφή στην οθόνη σύνδεσης",
"Room %(roomId)s not visible": "Το δωμάτιο %(roomId)s δεν είναι ορατό",
@ -269,22 +217,14 @@
"Send Invites": "Αποστολή προσκλήσεων",
"Send Reset Email": "Αποστολή μηνύματος επαναφοράς",
"%(senderDisplayName)s sent an image.": "Ο %(senderDisplayName)s έστειλε μια φωτογραφία.",
"Server may be unavailable or overloaded": "Ο διακομιστής μπορεί να είναι μη διαθέσιμος ή υπερφορτωμένος",
"Session ID": "Αναγνωριστικό συνεδρίας",
"%(senderName)s set a profile picture.": "Ο %(senderName)s όρισε τη φωτογραφία του προφίλ του.",
"Start authentication": "Έναρξη πιστοποίησης",
"Submit": "Υποβολή",
"Tagged as: ": "Με ετικέτα: ",
"The default role for new room members is": "Ο προεπιλεγμένος ρόλος για νέα μέλη είναι",
"The main address for this room is": "Η κύρια διεύθυνση για το δωμάτιο είναι",
"The file '%(fileName)s' failed to upload": "Απέτυχε η αποστολή του αρχείου '%(fileName)s'",
"This room has no local addresses": "Αυτό το δωμάτιο δεν έχει τοπικές διευθύνσεις",
"This doesn't appear to be a valid email address": "Δεν μοιάζει με μια έγκυρη διεύθυνση ηλεκτρονικής αλληλογραφίας",
"This phone number is already in use": "Αυτός ο αριθμός τηλεφώνου είναι ήδη σε χρήση",
"This room": "Αυτό το δωμάτιο",
"This room's internal ID is": "Το εσωτερικό αναγνωριστικό του δωματίου είναι",
"Turn Markdown off": "Απενεργοποίηση Markdown",
"Turn Markdown on": "Ενεργοποίηση Markdown",
"Unable to add email address": "Αδυναμία προσθήκης διεύθυνσης ηλ. αλληλογραφίας",
"Unable to remove contact information": "Αδυναμία αφαίρεσης πληροφοριών επαφής",
"Unable to verify email address.": "Αδυναμία επιβεβαίωσης διεύθυνσης ηλεκτρονικής αλληλογραφίας.",
@ -292,7 +232,6 @@
"%(senderName)s unbanned %(targetName)s.": "Ο χρήστης %(senderName)s έδιωξε τον χρήστη %(targetName)s.",
"Unable to enable Notifications": "Αδυναμία ενεργοποίησης των ειδοποιήσεων",
"Unable to load device list": "Αδυναμία φόρτωσης της λίστας συσκευών",
"Unencrypted room": "Μη κρυπτογραφημένο δωμάτιο",
"unencrypted": "μη κρυπτογραφημένο",
"Unencrypted message": "Μη κρυπτογραφημένο μήνυμα",
"unknown caller": "άγνωστος καλών",
@ -302,17 +241,12 @@
"Unnamed Room": "Ανώνυμο δωμάτιο",
"Unrecognised command:": "Μη αναγνωρίσιμη εντολή:",
"Unrecognised room alias:": "Μη αναγνωρίσιμο ψευδώνυμο:",
"Unverified": "Ανεπιβεβαίωτο",
"Upload avatar": "Αποστολή προσωπικής εικόνας",
"Upload Failed": "Απέτυχε η αποστολή",
"Upload Files": "Αποστολή αρχείων",
"Upload file": "Αποστολή αρχείου",
"Upload new:": "Αποστολή νέου:",
"Usage": "Χρήση",
"Use with caution": "Χρησιμοποιήστε τα με προσοχή",
"User ID": "Αναγνωριστικό χρήστη",
"User Interface": "Διεπαφή χρήστη",
"User name": "Όνομα χρήστη",
"Username invalid: %(errMessage)s": "Μη έγκυρο όνομα χρήστη: %(errMessage)s",
"Users": "Χρήστες",
"Video call": "Βιντεοκλήση",
@ -323,8 +257,6 @@
"You have no visible notifications": "Δεν έχετε ορατές ειδοποιήσεις",
"You must <a>register</a> to use this functionality": "Πρέπει να <a>εγγραφείτε</a> για να χρησιμοποιήσετε αυτή την λειτουργία",
"You need to be logged in.": "Πρέπει να είστε συνδεδεμένος.",
"You need to enter a user name.": "Πρέπει να εισάγετε ένα όνομα χρήστη.",
"Your password has been reset": "Ο κωδικός πρόσβασης σας έχει επαναφερθεί",
"Sun": "Κυρ",
"Mon": "Δευ",
"Tue": "Τρί",
@ -349,21 +281,12 @@
"Set a display name:": "Ορισμός ονόματος εμφάνισης:",
"Upload an avatar:": "Αποστολή προσωπικής εικόνας:",
"This server does not support authentication with a phone number.": "Αυτός ο διακομιστής δεν υποστηρίζει πιστοποίηση με αριθμό τηλεφώνου.",
"Missing password.": "Λείπει ο κωδικός πρόσβασης.",
"Passwords don't match.": "Δεν ταιριάζουν οι κωδικοί πρόσβασης.",
"This doesn't look like a valid email address.": "Δεν μοιάζει με μια έγκυρη διεύθυνση ηλ. αλληλογραφίας.",
"An unknown error occurred.": "Προέκυψε ένα άγνωστο σφάλμα.",
"I already have an account": "Έχω ήδη λογαριασμό",
"An error occurred: %(error_string)s": "Προέκυψε ένα σφάλμα: %(error_string)s",
"Topic": "Θέμα",
"Make Moderator": "Ορισμός συντονιστή",
"Encrypt room": "Κρυπτογράφηση δωματίου",
"Room": "Δωμάτιο",
"(~%(count)s results)|one": "(~%(count)s αποτέλεσμα)",
"(~%(count)s results)|other": "(~%(count)s αποτελέσματα)",
"Active call": "Ενεργή κλήση",
"strike": "επιγράμμιση",
"bullet": "κουκκίδα",
"New Password": "Νέος κωδικός πρόσβασης",
"Start automatically after system login": "Αυτόματη έναρξη μετά τη σύνδεση",
"Options": "Επιλογές",
@ -373,13 +296,11 @@
"Confirm passphrase": "Επιβεβαίωση συνθηματικού",
"Import room keys": "Εισαγωγή κλειδιών δωματίου",
"File to import": "Αρχείο για εισαγωγή",
"Start new chat": "Έναρξη νέας συνομιλίας",
"Confirm Removal": "Επιβεβαίωση αφαίρεσης",
"Unknown error": "Άγνωστο σφάλμα",
"Incorrect password": "Λανθασμένος κωδικός πρόσβασης",
"To continue, please enter your password.": "Για να συνεχίσετε, παρακαλούμε πληκτρολογήστε τον κωδικό πρόσβασής σας.",
"Device name": "Όνομα συσκευής",
"Device Name": "Όνομα συσκευής",
"Device key": "Κλειδί συσκευής",
"Verify device": "Επιβεβαίωση συσκευής",
"Unable to restore session": "Αδυναμία επαναφοράς συνεδρίας",
@ -389,15 +310,8 @@
"Verify...": "Επιβεβαίωση...",
"ex. @bob:example.com": "π.χ @bob:example.com",
"Add User": "Προσθήκη χρήστη",
"Sign in with CAS": "Σύνδεση με CAS",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Μπορείτε να χρησιμοποιήσετε τις προσαρμοσμένες ρυθμίσεις για να εισέλθετε σε άλλους διακομιστές Matrix επιλέγοντας μια διαφορετική διεύθυνση για το διακομιστή.",
"Token incorrect": "Εσφαλμένο διακριτικό",
"Please enter the code it contains:": "Παρακαλούμε εισάγετε τον κωδικό που περιέχει:",
"Default server": "Προεπιλεγμένος διακομιστής",
"Custom server": "Προσαρμοσμένος διακομιστής",
"Home server URL": "Διεύθυνση διακομιστή",
"Identity server URL": "Διεύθυνση διακομιστή ταυτοποίησης",
"What does this mean?": "Τι σημαίνει αυτό;",
"Error decrypting audio": "Σφάλμα κατά την αποκρυπτογράφηση του ήχου",
"Error decrypting image": "Σφάλμα κατά την αποκρυπτογράφηση της εικόνας",
"Error decrypting video": "Σφάλμα κατά την αποκρυπτογράφηση του βίντεο",
@ -407,18 +321,12 @@
"Online": "Σε σύνδεση",
"Idle": "Αδρανής",
"Offline": "Εκτός σύνδεσης",
"Start chatting": "Έναρξη συνομιλίας",
"Start Chatting": "Έναρξη συνομιλίας",
"Click on the button below to start chatting!": "Κάντε κλικ στο κουμπί παρακάτω για να ξεκινήσετε μια συνομιλία!",
"%(senderDisplayName)s removed the room avatar.": "Ο %(senderDisplayName)s διέγραψε την προσωπική εικόνα του δωματίου.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "Ο %(senderDisplayName)s άλλαξε την προσωπική εικόνα του %(roomName)s",
"Username available": "Διαθέσιμο όνομα χρήστη",
"Username not available": "Μη διαθέσιμο όνομα χρήστη",
"Something went wrong!": "Κάτι πήγε στραβά!",
"Could not connect to the integration server": "Αδυναμία σύνδεσης στον διακομιστή ενσωμάτωσης",
"Create a new chat or reuse an existing one": "Δημιουργία νέας συνομιλίας ή επαναχρησιμοποίηση μιας υπάρχουσας",
"Don't send typing notifications": "Να μη γίνετε αποστολή ειδοποιήσεων πληκτρολόγησης",
"Encrypted by a verified device": "Κρυπτογραφημένο από μια επιβεβαιωμένη συσκευή",
"Encrypted by an unverified device": "Κρυπτογραφημένο από μια ανεπιβεβαίωτη συσκευή",
"Error: Problem communicating with the given homeserver.": "Σφάλμα: πρόβλημα κατά την επικοινωνία με τον ορισμένο διακομιστή.",
"Failed to ban user": "Δεν ήταν δυνατό ο αποκλεισμός του χρήστη",
@ -426,18 +334,15 @@
"Failed to fetch avatar URL": "Δεν ήταν δυνατή η ανάκτηση της διεύθυνσης εικόνας",
"Failed to unban": "Δεν ήταν δυνατή η άρση του αποκλεισμού",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s από %(fromPowerLevel)s σε %(toPowerLevel)s",
"Guest access is disabled on this Home Server.": "Έχει απενεργοποιηθεί η πρόσβαση στους επισκέπτες σε αυτόν τον διακομιστή.",
"Guests cannot join this room even if explicitly invited.": "Οι επισκέπτες δεν μπορούν να συνδεθούν στο δωμάτιο ακόμη και αν έχουν καλεστεί.",
"Hide Text Formatting Toolbar": "Απόκρυψη εργαλειοθήκης μορφοποίησης κειμένου",
"Incoming call from %(name)s": "Εισερχόμενη κλήση από %(name)s",
"Incoming video call from %(name)s": "Εισερχόμενη βιντεοκλήση από %(name)s",
"Incoming voice call from %(name)s": "Εισερχόμενη φωνητική κλήση από %(name)s",
"Invalid alias format": "Μη έγκυρη μορφή ψευδώνυμου",
"Invalid address format": "Μη έγκυρη μορφή διεύθυνσης",
"Invalid file%(extra)s": "Μη έγκυρο αρχείο %(extra)s",
"%(senderName)s invited %(targetName)s.": "Ο %(senderName)s προσκάλεσε τον %(targetName)s.",
"Invites user with given id to current room": "Προσκαλεί τον χρήστη με το δοσμένο αναγνωριστικό στο τρέχον δωμάτιο",
"'%(alias)s' is not a valid format for an address": "Το '%(alias)s' δεν είναι μια έγκυρη μορφή διεύθυνσης",
"'%(alias)s' is not a valid format for an alias": "Το '%(alias)s' δεν είναι μια έγκυρη μορφή ψευδώνυμου",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο όλα τα μέλη, από τη στιγμή που προσκλήθηκαν.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο όλα τα μέλη, από τη στιγμή που συνδέθηκαν.",
@ -445,21 +350,15 @@
"%(senderName)s made future room history visible to anyone.": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο οποιοσδήποτε.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο άγνωστο (%(visibility)s).",
"Missing user_id in request": "Λείπει το user_id στο αίτημα",
"Mobile phone number (optional)": "Αριθμός κινητού τηλεφώνου (προαιρετικό)",
"Never send encrypted messages to unverified devices from this device": "Να μη γίνει ποτέ αποστολή κρυπτογραφημένων μηνυμάτων σε ανεπιβεβαίωτες συσκευές από αυτή τη συσκευή",
"Never send encrypted messages to unverified devices in this room from this device": "Να μη γίνει ποτέ αποστολή κρυπτογραφημένων μηνυμάτων σε ανεπιβεβαίωτες συσκευές, σε αυτό το δωμάτιο, από αυτή τη συσκευή",
"not set": "δεν έχει οριστεί",
"not specified": "μη καθορισμένο",
"NOT verified": "ΧΩΡΙΣ επαλήθευση",
"No devices with registered encryption keys": "Καθόλου συσκευές με εγγεγραμένα κλειδιά κρυπτογράφησης",
"No display name": "Χωρίς όνομα",
"No users have specific privileges in this room": "Κανένας χρήστης δεν έχει συγκεκριμένα δικαιώματα σε αυτό το δωμάτιο",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Μόλις ενεργοποιηθεί η κρυπτογράφηση για ένα δωμάτιο, δεν μπορεί να απενεργοποιηθεί ξανά (για τώρα)",
"Only people who have been invited": "Μόνο άτομα που έχουν προσκληθεί",
"%(senderName)s placed a %(callType)s call.": "Ο %(senderName)s πραγματοποίησε μια %(callType)s κλήση.",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Παρακαλούμε ελέγξτε την ηλεκτρονική σας αλληλογραφία και κάντε κλικ στον σύνδεσμο που περιέχει. Μόλις γίνει αυτό, κάντε κλίκ στο κουμπί συνέχεια.",
"Refer a friend to Riot:": "Πείτε για το Riot σε έναν φίλο σας:",
"Rejoin": "Επανασύνδεση",
"%(senderName)s removed their profile picture.": "Ο %(senderName)s αφαίρεσε τη φωτογραφία του προφίλ του.",
"%(senderName)s requested a VoIP conference.": "Ο %(senderName)s αιτήθηκε μια συνδιάσκεψη VoIP.",
"Riot does not have permission to send you notifications - please check your browser settings": "Το Riot δεν έχει δικαιώματα για αποστολή ειδοποιήσεων - παρακαλούμε ελέγξτε τις ρυθμίσεις του περιηγητή σας",
@ -467,13 +366,10 @@
"Room contains unknown devices": "Το δωμάτιο περιέχει άγνωστες συσκευές",
"%(roomName)s is not accessible at this time.": "Το %(roomName)s δεν είναι προσβάσιμο αυτή τη στιγμή.",
"Scroll to bottom of page": "Μετάβαση στο τέλος της σελίδας",
"Scroll to unread messages": "Μεταβείτε στα μη αναγνωσμένα μηνύματα",
"Sender device information": "Πληροφορίες συσκευής αποστολέα",
"Server may be unavailable, overloaded, or search timed out :(": "Ο διακομιστής μπορεί να είναι μη διαθέσιμος, υπερφορτωμένος, ή να έχει λήξει η αναζήτηση :(",
"Server may be unavailable, overloaded, or the file too big": "Ο διακομιστής μπορεί να είναι μη διαθέσιμος, υπερφορτωμένος, ή το αρχείο να είναι πολύ μεγάλο",
"Server may be unavailable, overloaded, or you hit a bug.": "Ο διακομιστής μπορεί να είναι μη διαθέσιμος, υπερφορτωμένος, ή να πέσατε σε ένα σφάλμα.",
"Server unavailable, overloaded, or something else went wrong.": "Ο διακομιστής μπορεί να είναι μη διαθέσιμος, υπερφορτωμένος, ή κάτι άλλο να πήγε στραβά.",
"Show panel": "Εμφάνιση καρτέλας",
"Show Text Formatting Toolbar": "Εμφάνιση της εργαλειοθήκης μορφοποίησης κειμένου",
"%(count)s of your messages have not been sent.|other": "Μερικά από τα μηνύματα σας δεν έχουν αποσταλεί.",
"This room is not recognised.": "Αυτό το δωμάτιο δεν αναγνωρίζεται.",
@ -485,103 +381,63 @@
"Verification Pending": "Εκκρεμεί επιβεβαίωση",
"Verification": "Επιβεβαίωση",
"verified": "επαληθεύτηκε",
"Verified": "Επαληθεύτηκε",
"Verified key": "Επιβεβαιωμένο κλειδί",
"VoIP conference finished.": "Ολοκληρώθηκε η συνδιάσκεψη VoIP.",
"VoIP conference started.": "Ξεκίνησησε η συνδιάσκεψη VoIP.",
"VoIP is unsupported": "Δεν υποστηρίζεται το VoIP",
"(warning: cannot be disabled again!)": "(προειδοποίηση: δεν μπορεί να απενεργοποιηθεί ξανά)",
"WARNING: Device already verified, but keys do NOT MATCH!": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η συσκευή έχει επαληθευτεί, αλλά τα κλειδιά ΔΕΝ ΤΑΙΡΙΑΖΟΥΝ!",
"Who can access this room?": "Ποιος μπορεί να προσπελάσει αυτό το δωμάτιο;",
"Who can read history?": "Ποιος μπορεί να διαβάσει το ιστορικό;",
"Who would you like to add to this room?": "Ποιον θέλετε να προσθέσετε σε αυτό το δωμάτιο;",
"%(senderName)s withdrew %(targetName)s's invitation.": "Ο %(senderName)s ανακάλεσε την πρόσκληση του %(targetName)s.",
"You cannot place a call with yourself.": "Δεν μπορείτε να καλέσετε τον ευατό σας.",
"You cannot place VoIP calls in this browser.": "Δεν μπορείτε να πραγματοποιήσετε κλήσεις VoIP με αυτόν τον περιηγητή.",
"You do not have permission to post to this room": "Δεν έχετε δικαιώματα για να δημοσιεύσετε σε αυτό το δωμάτιο",
"You have been banned from %(roomName)s by %(userName)s.": "Έχετε αποκλειστεί από το δωμάτιο %(roomName)s από τον %(userName)s.",
"You have been invited to join this room by %(inviterName)s": "Έχετε προσκληθεί να συνδεθείτε στο δωμάτιο από τον %(inviterName)s",
"You seem to be in a call, are you sure you want to quit?": "Φαίνεται ότι είστε σε μια κλήση, είστε βέβαιοι ότι θέλετε να αποχωρήσετε;",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s",
"This doesn't look like a valid phone number.": "Δεν μοιάζει με έναν έγκυρο αριθμό τηλεφώνου.",
"Make this room private": "Κάντε το δωμάτιο ιδιωτικό",
"There are no visible files in this room": "Δεν υπάρχουν ορατά αρχεία σε αυτό το δωμάτιο",
"Connectivity to the server has been lost.": "Χάθηκε η συνδεσιμότητα στον διακομιστή.",
"Please select the destination room for this message": "Παρακαλούμε επιλέξτε ένα δωμάτιο προορισμού για αυτό το μήνυμα",
"Desktop specific": "Μόνο για επιφάνεια εργασίας",
"Analytics": "Αναλυτικά δεδομένα",
"Riot collects anonymous analytics to allow us to improve the application.": "Το Riot συλλέγει ανώνυμα δεδομένα επιτρέποντας μας να βελτιώσουμε την εφαρμογή.",
"Failed to invite": "Δεν ήταν δυνατή η πρόσκληση",
"Failed to invite user": "Δεν ήταν δυνατή η πρόσκληση του χρήστη",
"In future this verification process will be more sophisticated.": "Στο μέλλον η διαδικασία επαλήθευσης θα είναι πιο εξελιγμένη.",
"I verify that the keys match": "Επιβεβαιώνω πως ταιριάζουν τα κλειδιά",
"\"%(RoomName)s\" contains devices that you haven't seen before.": "Το \"%(RoomName)s\" περιέχει συσκευές που δεν έχετε ξαναδεί.",
"This Home Server would like to make sure you are not a robot": "Ο διακομιστής θέλει να σιγουρευτεί ότι δεν είσαστε ρομπότ",
"Please check your email to continue registration.": "Παρακαλούμε ελέγξτε την ηλεκτρονική σας αλληλογραφία για να συνεχίσετε με την εγγραφή.",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Αν δεν ορίσετε μια διεύθυνση ηλεκτρονικής αλληλογραφίας, δεν θα θα μπορείτε να επαναφέρετε τον κωδικό πρόσβασης σας. Είστε σίγουροι;",
"You are registering with %(SelectedTeamName)s": "Εγγραφείτε με %(SelectedTeamName)s",
"Removed or unknown message type": "Αφαιρέθηκε ή άγνωστος τύπος μηνύματος",
" (unsupported)": " (μη υποστηριζόμενο)",
"%(senderDisplayName)s changed the room avatar to <img/>": "Ο %(senderDisplayName)s άλλαξε την εικόνα του δωματίου σε <img/>",
"Missing Media Permissions, click here to request.": "Λείπουν τα δικαιώματα πολύμεσων, κάντε κλικ για να ζητήσετε.",
"You may need to manually permit Riot to access your microphone/webcam": "Μπορεί να χρειαστεί να ορίσετε χειροκίνητα την πρόσβαση του Riot στο μικρόφωνο/κάμερα",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Δεν είναι δυνατή η σύνδεση στον διακομιστή - παρακαλούμε ελέγξτε την συνδεσιμότητα, βεβαιωθείτε ότι το <a>πιστοποιητικό SSL</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>.": "Δεν είναι δυνατή η σύνδεση στον διακομιστή μέσω HTTP όταν μια διεύθυνση HTTPS βρίσκεται στην μπάρα του περιηγητή. Είτε χρησιμοποιήστε HTTPS ή <a>ενεργοποιήστε τα μη ασφαλή σενάρια εντολών</a>.",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "Ο %(senderName)s άλλαξε το επίπεδο δύναμης του %(powerLevelDiffText)s.",
"Changes to who can read history will only apply to future messages in this room": "Οι αλλαγές που αφορούν την ορατότητα του ιστορικού θα εφαρμοστούν μόνο στα μελλοντικά μηνύματα του δωματίου",
"Conference calling is in development and may not be reliable.": "Η κλήση συνδιάσκεψης είναι υπό ανάπτυξη και μπορεί να μην είναι αξιόπιστη.",
"Devices will not yet be able to decrypt history from before they joined the room": "Οι συσκευές δεν θα είναι σε θέση να αποκρυπτογραφήσουν το ιστορικό πριν από την είσοδο τους στο δωμάτιο",
"End-to-end encryption is in beta and may not be reliable": "Η κρυπτογράφηση από άκρο σε άκρο είναι σε δοκιμαστικό στάδιο και μπορεί να μην είναι αξιόπιστη",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "Ο %(senderName)s αφαίρεσε το όνομα εμφάνισης του (%(oldDisplayName)s).",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "Ο %(senderName)s έστειλε μια πρόσκληση στον %(targetDisplayName)s για να συνδεθεί στο δωμάτιο.",
"%(senderName)s set their display name to %(displayName)s.": "Ο %(senderName)s όρισε το όνομα του σε %(displayName)s.",
"The phone number entered looks invalid": "Ο αριθμός που καταχωρίσατε δεν είναι έγκυρος",
"The email address linked to your account must be entered.": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.",
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "Το αρχείο '%(fileName)s' υπερβαίνει το όριο μεγέθους του διακομιστή για αποστολές",
"The remote side failed to pick up": "Η απομακρυσμένη πλευρά απέτυχε να συλλέξει",
"This Home Server does not support login using email address.": "Ο διακομιστής δεν υποστηρίζει σύνδεση με διευθύνσεις ηλ. αλληλογραφίας.",
"This invitation was sent to an email address which is not associated with this account:": "Η πρόσκληση στάλθηκε σε μια διεύθυνση που δεν έχει συσχετιστεί με αυτόν τον λογαριασμό:",
"These are experimental features that may break in unexpected ways": "Αυτά είναι πειραματικά χαρακτηριστικά και μπορεί να καταρρεύσουν με απροσδόκητους τρόπους",
"The visibility of existing history will be unchanged": "Η ορατότητα του υπάρχοντος ιστορικού θα παραμείνει αμετάβλητη",
"This is a preview of this room. Room interactions have been disabled": "Αυτή είναι μια προεπισκόπηση του δωματίου. Οι αλληλεπιδράσεις δωματίου έχουν απενεργοποιηθεί",
"This room is not accessible by remote Matrix servers": "Αυτό το δωμάτιο δεν είναι προσβάσιμο από απομακρυσμένους διακομιστές Matrix",
"To reset your password, enter the email address linked to your account": "Για να επαναφέρετε τον κωδικό πρόσβασης σας, πληκτρολογήστε τη διεύθυνση ηλ. αλληλογραφίας όπου είναι συνδεδεμένη με τον λογαριασμό σας",
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "Ο %(senderName)s ενεργοποίησε την από άκρο σε άκρο κρυπτογράφηση (algorithm %(algorithm)s).",
"Undecryptable": "Μη αποκρυπτογραφημένο",
"Uploading %(filename)s and %(count)s others|one": "Γίνεται αποστολή του %(filename)s και %(count)s υπολοίπα",
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Θα θέλατε να <acceptText>δεχθείτε</acceptText> ή να <declineText>απορρίψετε</declineText> την πρόσκληση;",
"You already have existing direct chats with this user:": "Έχετε ήδη απευθείας συνομιλίες με τον χρήστη:",
"You are trying to access %(roomName)s.": "Προσπαθείτε να έχετε πρόσβαση στο %(roomName)s.",
"You have been kicked from %(roomName)s by %(userName)s.": "Έχετε διωχθεί από το δωμάτιο %(roomName)s από τον %(userName)s.",
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Έχετε αποσυνδεθεί από όλες τις συσκευές και δεν θα λαμβάνετε πλέον ειδοποιήσεις push. Για να ενεργοποιήσετε τις ειδοποιήσεις, συνδεθείτε ξανά σε κάθε συσκευή",
"You have <a>disabled</a> URL previews by default.": "Έχετε <a>απενεργοποιημένη</a> από προεπιλογή την προεπισκόπηση συνδέσμων.",
"You have <a>enabled</a> URL previews by default.": "Έχετε <a>ενεργοποιημένη</a> από προεπιλογή την προεπισκόπηση συνδέσμων.",
"You may wish to login with a different account, or add this email to this account.": "Μπορεί να θέλετε να συνδεθείτε με διαφορετικό λογαριασμό, ή να προσθέσετε αυτή τη διεύθυνση ηλεκτρονικής αλληλογραφίας σε αυτόν τον λογαριασμό.",
"You need to be able to invite users to do that.": "Για να το κάνετε αυτό πρέπει να έχετε τη δυνατότητα να προσκαλέσετε χρήστες.",
"You seem to be uploading files, are you sure you want to quit?": "Φαίνεται ότι αποστέλετε αρχεία, είστε βέβαιοι ότι θέλετε να αποχωρήσετε;",
"You should not yet trust it to secure data": "Δεν πρέπει να το εμπιστεύεστε για να ασφαλίσετε δεδομένα",
"Your home server does not support device management.": "Ο διακομιστής δεν υποστηρίζει διαχείριση συσκευών.",
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Ο κωδικός πρόσβασης είναι πολύ μικρός (ελ. %(MIN_PASSWORD_LENGTH)s).",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "Τα ονόματα μπορεί να περιέχουν μόνο γράμματα, αριθμούς, τελείες, πάνω και κάτω παύλα.",
"Share message history with new users": "Διαμοιρασμός ιστορικού μηνυμάτων με τους νέους χρήστες",
"numbullet": "απαρίθμηση",
"You must join the room to see its files": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του",
"Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s",
"Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των παρακάτω χρηστών στο δωμάτιο %(roomName)s:",
"Deops user with given id": "Deop χρήστη με το συγκεκριμένο αναγνωριστικό",
"Drop here to tag %(section)s": "Απόθεση εδώ για ορισμό ετικέτας στο %(section)s",
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Συμμετάσχετε με <voiceText>φωνή</voiceText> ή <videoText>βίντεο</videoText>.",
"Joins room with given alias": "Συνδέεστε στο δωμάτιο με δοσμένο ψευδώνυμο",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Εμφάνιση χρονικών σημάνσεων σε 12ωρη μορφή ώρας (π.χ. 2:30 μ.μ.)",
"The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Το κλειδί υπογραφής που δώσατε αντιστοιχεί στο κλειδί υπογραφής που λάβατε από τη συσκευή %(userId)s %(deviceId)s. Η συσκευή έχει επισημανθεί ως επιβεβαιωμένη.",
"To link to a room it must have <a>an address</a>.": "Για να συνδεθείτε σε ένα δωμάτιο πρέπει να έχετε <a>μια διεύθυνση</a>.",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Η διεύθυνση της ηλ. αλληλογραφίας σας δεν φαίνεται να συσχετίζεται με μια ταυτότητα Matrix σε αυτόν τον Διακομιστή Φιλοξενίας.",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Ο κωδικός πρόσβασής σας άλλαξε επιτυχώς. Δεν θα λάβετε ειδοποιήσεις push σε άλλες συσκευές μέχρι να συνδεθείτε ξανά σε αυτές",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Δεν θα μπορέσετε να αναιρέσετε αυτήν την αλλαγή καθώς προωθείτε τον χρήστη να έχει το ίδιο επίπεδο δύναμης με τον εαυτό σας.",
"Sent messages will be stored until your connection has returned.": "Τα απεσταλμένα μηνύματα θα αποθηκευτούν μέχρι να αακτηθεί η σύνδεσή σας.",
"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.": "Είστε βέβαιοι ότι θέλετε να καταργήσετε (διαγράψετε) αυτό το συμβάν; Σημειώστε ότι αν διαγράψετε το όνομα δωματίου ή αλλάξετε το θέμα, θα μπορούσε να αναιρέσει την αλλαγή.",
"This allows you to use this app with an existing Matrix account on a different home server.": "Αυτό σας επιτρέπει να χρησιμοποιήσετε την εφαρμογή με έναν υπάρχον λογαριασμό Matrix σε έναν διαφορετικό διακομιστή.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Μπορείτε επίσης να ορίσετε έναν προσαρμοσμένο διακομιστή ταυτοποίησης, αλλά αυτό συνήθως θα αποτρέψει την αλληλεπίδραση με τους χρήστες που βασίζονται στην ηλεκτρονική διεύθυνση αλληλογραφίας.",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Αυτό θα είναι το όνομα του λογαριασμού σας στον διακομιστή <span></span>, ή μπορείτε να επιλέξετε <a>διαφορετικό διακομιστή</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Αν έχετε ήδη λογαριασμό Matrix μπορείτε να <a>συνδεθείτε</a>.",
"Failed to load timeline position": "Δεν ήταν δυνατή η φόρτωση της θέσης του χρονολόγιου",
@ -599,9 +455,7 @@
"Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Η αλλαγή του κωδικού πρόσβασης θα επαναφέρει τα κλειδιά κρυπτογράφησης από άκρο σε άκρο σε όλες τις συσκευές, καθιστώντας το κρυπτογραφημένο ιστορικό συζητήσεων μη αναγνώσιμο, εκτός και αν εξάγετε πρώτα τα κλειδιά και τα εισαγάγετε ξανά στο δωμάτιο. Στο μέλλον αυτή η διαδικασία θα βελτιωθεί.",
"Claimed Ed25519 fingerprint key": "Απαιτήθηκε κλειδί αποτυπώματος Ed25519",
"Displays action": "Εμφανίζει την ενέργεια",
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Η επαναφορά του κωδικού πρόσβασης θα επαναφέρει τα κλειδιά κρυπτογράφησης από άκρο σε άκρο σε όλες τις συσκευές, καθιστώντας το κρυπτογραφημένο ιστορικό συζητήσεων μη αναγνώσιμο, εκτός και αν εξάγετε πρώτα τα κλειδιά και τα εισαγάγετε ξανά στο δωμάτιο. Στο μέλλον αυτή η διαδικασία θα βελτιωθεί.",
"To use it, just wait for autocomplete results to load and tab through them.": "Για να το χρησιμοποιήσετε, απλά περιμένετε μέχρι να φορτωθούν τα αποτέλεσμα αυτόματης συμπλήρωσης. Έπειτα επιλέξτε ένα από αυτά χρησιμοποιώντας τον στηλοθέτη.",
"Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Δεν είναι δυνατό να εξακριβωθεί ότι η διεύθυνση αυτής της πρόσκλησης στάλθηκε σε αντιστοιχία με εκείνη που σχετίζεται με το λογαριασμό σας.",
"Use compact timeline layout": "Χρήση συμπαγούς διάταξης χρονολογίου",
"(could not connect media)": "(αδυναμία σύνδεσης με το πολυμέσο)",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: ΑΠΕΤΥΧΕ Η ΕΠΙΒΕΒΑΙΩΣΗ ΤΟΥ ΚΛΕΙΔΙΟΥ! Το κλειδί υπογραφής για τον χρήστη %(userId)s και συσκευή %(deviceId)s είναι \"%(fprint)s\" και δεν ταιριάζει με το δοσμένο κλειδί \"%(fingerprint)s\". Αυτό σημαίνει ότι η επικοινωνία σας μπορεί να έχει υποκλαπεί!",
@ -615,12 +469,8 @@
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Αυτήν τη στιγμή βάζετε σε μαύρη λίστα μη επιβαιωμένες συσκευές. Για να στείλετε μηνύματα σε αυτές τις συσκευές, πρέπει να τις επιβεβαιώσετε.",
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Σας συνιστούμε να ολοκληρώσετε τη διαδικασία επαλήθευσης για κάθε συσκευή και να επιβεβαιώσετε ότι ανήκουν στον νόμιμο κάτοχό της, αλλά εάν προτιμάτε μπορείτε να στείλετε ξανά το μήνυμα χωρίς επαλήθευση.",
"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?": "Θα μεταφερθείτε σε έναν ιστότοπου τρίτου για να πραγματοποιηθεί η πιστοποίηση του λογαριασμού σας με το %(integrationsUrl)s. Θα θέλατε να συνεχίσετε;",
"Disable Peer-to-Peer for 1:1 calls": "Απενεργοποίηση του ομότιμου (Peer-to-Peer) για κλήσεις έναν προς έναν",
"Do you want to set an email address?": "Θέλετε να ορίσετε μια διεύθυνση ηλεκτρονικής αλληλογραφίας;",
"This will allow you to reset your password and receive notifications.": "Αυτό θα σας επιτρέψει να επαναφέρετε τον κωδικό πρόσβαση σας και θα μπορείτε να λαμβάνετε ειδοποιήσεις.",
"Press <StartChatButton> to start a chat with someone": "Πατήστε <StartChatButton> για να ξεκινήσετε μια συνομιλία",
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "Δεν είστε σε κανένα δωμάτιο! Πατήστε <CreateRoomButton> για να δημιουργήσετε ένα δωμάτιο ή <RoomDirectoryButton> για να δείτε το ευρετήριο",
"To return to your account in future you need to set a password": "Για να επιστρέψετε στον λογαριασμό σας μελλοντικα πρέπει να ορίσετε έναν κωδικό πρόσβασης",
"Skip": "Παράβλεψη",
"Start verification": "Έναρξη επιβεβαίωσης",
"Share without verifying": "Κοινή χρήση χωρίς επιβεβαίωση",
@ -628,7 +478,6 @@
"You added a new device '%(displayName)s', which is requesting encryption keys.": "Έχετε προσθέσει μια νέα συσκευή '%(displayName)s', η οποία ζητά κλειδιά κρυπτογράφησης.",
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Η ανεπιβεβαίωτη συσκευή σας '%(displayName)s' ζητά κλειδιά κρυπτογράφησης.",
"Encryption key request": "Αίτημα κλειδιού κρυπτογράφησης",
"Updates": "Ενημερώσεις",
"Check for update": "Έλεγχος για ενημέρωση",
"Fetching third party location failed": "Η λήψη τοποθεσίας απέτυχε",
"A new version of Riot is available.": "Μία νέα έκδοση του Riot είναι διαθέσιμη.",
@ -645,10 +494,7 @@
"Friday": "Παρασκευή",
"Update": "Ενημέρωση",
"Riot does not know how to join a room on this network": "To Riot δεν γνωρίζει πως να συνδεθεί σε δωμάτια που ανήκουν σ' αυτό το δίκτυο",
"Add an email address above to configure email notifications": "Προσθέστε μια διεύθυνση ηλεκτρονικής αλληλογραφίας στο παραπάνω πεδίο, για να έχετε τη δυνατότητα να λαμβάνετε ειδοποιήσεις",
"Expand panel": "Μεγιστοποίηση καρτέλας",
"On": "Ενεργό",
"Filter room names": "Φιλτράρισμα δωματίων",
"Changelog": "Αλλαγές",
"Waiting for response from server": "Αναμονή απάντησης από τον διακομιστή",
"Leave": "Αποχώρηση",
@ -658,12 +504,10 @@
"To return to your account in future you need to <u>set a password</u>": "Για να επιστρέψετε στον λογαριασμό σας μελλοντικα πρέπει να ορίσετε έναν <u>κωδικό πρόσβασης</u>",
"Forget": "Παράλειψη",
"World readable": "Εμφανές σε όλους",
"Hide panel": "Απόκρυψη καρτέλας",
"You cannot delete this image. (%(code)s)": "Δεν μπορείτε να διαγράψετε αυτή την εικόνα. (%(code)s)",
"Cancel Sending": "Ακύρωση αποστολής",
"Warning": "Προειδοποίηση",
"This Room": "Στο δωμάτιο",
"The Home Server may be too old to support third party networks": "Ο διακομιστής μπορεί να είναι αρκετά παλιός για να υποστηρίζει δίκτυα τρίτων",
"Resend": "Αποστολή ξανά",
"Room not found": "Το δωμάτιο δεν βρέθηκε",
"Messages containing my display name": "Μηνύματα που περιέχουν το όνομα μου",
@ -681,14 +525,11 @@
"Members": "Μέλη",
"No update available.": "Δεν υπάρχει διαθέσιμη ενημέρωση.",
"Noisy": "Δυνατά",
"Failed to get protocol list from Home Server": "Δεν ήταν δυνατή η εύρεση των διαθέσιμων πρωτοκόλλων από το διακομιστή",
"Collecting app version information": "Συγκέντρωση πληροφοριών σχετικά με την έκδοση της εφαρμογής",
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Διαγραφή του ψευδώνυμου %(alias)s και αφαίρεση του %(name)s από το ευρετήριο;",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Αυτό θα σας επιτρέψει να επιστρέψετε στον λογαριασμό σας αφού αποσυνδεθείτε και συνδεθείτε από άλλες συσκευές.",
"Keywords": "Λέξεις κλειδιά",
"Enable notifications for this account": "Ενεργοποίηση ειδοποιήσεων για τον λογαριασμό",
"Directory": "Ευρετήριο",
"Search for a room": "Αναζήτηση δωματίου",
"Messages containing <span>keywords</span>": "Μηνύματα που περιέχουν <span>λέξεις κλειδιά</span>",
"Error saving email notification preferences": "Σφάλμα κατά την αποθήκευση των προτιμήσεων",
"Tuesday": "Τρίτη",
@ -708,10 +549,7 @@
"All messages (noisy)": "Όλα τα μηνύματα (δυνατά)",
"Enable them now": "Ενεργοποίηση",
"Forward Message": "Προώθηση",
"Messages containing my user name": "Μηνύματα που περιέχουν το ψευδώνυμο μου",
"Collecting logs": "Συγκέντρωση πληροφοριών",
"more": "περισσότερα",
"Failed to get public room list": "Δεν ήταν δυνατή η λήψη της λίστας με τα δημόσια δωμάτια",
"(HTTP status %(httpStatus)s)": "(Κατάσταση HTTP %(httpStatus)s)",
"All Rooms": "Όλα τα δωμάτια",
"Wednesday": "Τετάρτη",
@ -742,22 +580,17 @@
"Low Priority": "Χαμηλή προτεραιότητα",
"What's New": "Τι νέο υπάρχει",
"Set Password": "Ορισμός κωδικού πρόσβασης",
"Enable audible notifications in web client": "Ενεργοποίηση ηχητικών ειδοποιήσεων",
"Off": "Ανενεργό",
"#example": "#παράδειγμα",
"Mentions only": "Μόνο αναφορές",
"Failed to remove tag %(tagName)s from room": "Δεν ήταν δυνατή η διαγραφή της ετικέτας %(tagName)s από το δωμάτιο",
"You can now return to your account after signing out, and sign in on other devices.": "Μπορείτε να επιστρέψετε στον λογαριασμό σας αφού αποσυνδεθείτε και συνδεθείτε από άλλες συσκευές.",
"Enable desktop notifications": "Ενεργοποίηση ειδοποιήσεων στην επιφάνεια εργασίας",
"Enable email notifications": "Ενεργοποίηση ειδοποιήσεων μέσω μηνυμάτων ηλ. αλληλογραφίας",
"Login": "Σύνδεση",
"No rooms to show": "Δεν υπάρχουν δωμάτια για εμφάνιση",
"Download this file": "Λήψη αρχείου",
"Failed to change settings": "Δεν ήταν δυνατή η αλλαγή των ρυθμίσεων",
"View Source": "Προβολή κώδικα",
"Unable to fetch notification target list": "Δεν ήταν δυνατή η εύρεση στόχων για τις ειδοποιήσεις",
"Quote": "Παράθεση",
"Collapse panel": "Ελαχιστοποίηση καρτέλας",
"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!": "Με τον τρέχον περιηγητή, η εμφάνιση και η αίσθηση της εφαρμογής ενδέχεται να είναι εντελώς εσφαλμένη και ορισμένες ή όλες οι λειτουργίες ενδέχεται να μην λειτουργούν. Εάν θέλετε να το δοκιμάσετε ούτως ή άλλως μπορείτε να συνεχίσετε, αλλά είστε μόνοι σας σε ό, τι αφορά τα προβλήματα που μπορεί να αντιμετωπίσετε!",
"Checking for an update...": "Γίνεται έλεγχος για ενημέρωση...",
"There are advanced notifications which are not shown here": "Υπάρχουν προχωρημένες ειδοποιήσεις οι οποίες δεν εμφανίζονται εδώ",
@ -771,7 +604,6 @@
"Your device resolution": "Η ανάλυση της συσκευής σας",
"The information being sent to us to help make Riot.im better includes:": "Οι πληροφορίες που στέλνονται σε εμάς με σκοπό την βελτίωση του Riot.im περιλαμβάνουν:",
"Call Failed": "Η κλήση απέτυχε",
"Whether or not you're logged in (we don't record your user name)": "Εάν είστε συνδεδεμένος/η ή όχι (δεν καταγράφουμε το όνομα χρήστη σας)",
"e.g. %(exampleValue)s": "π.χ. %(exampleValue)s",
"Review Devices": "Ανασκόπηση συσκευών",
"Call Anyway": "Πραγματοποίηση Κλήσης όπως και να 'χει",
@ -784,7 +616,6 @@
"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": "Προσοχή: κάθε άτομο που προσθέτετε στην κοινότητα θε είναι δημοσίως ορατό σε οποιονδήποτε γνωρίζει το αναγνωριστικό της κοινότητας",
"Invite new community members": "Προσκαλέστε νέα μέλη στην κοινότητα",
"Name or matrix ID": "Όνομα ή αναγνωριστικό του matrix",
"Invite to Community": "Προσκαλέστε στην κοινότητα",
"Which rooms would you like to add to this community?": "Ποια δωμάτια θα θέλατε να προσθέσετε σε αυτή την κοινότητα;",
"Add rooms to the community": "Προσθήκη δωματίων στην κοινότητα",
@ -798,7 +629,6 @@
"Room name or alias": "Όνομα η ψευδώνυμο δωματίου",
"Restricted": "Περιορισμένο/η",
"Unable to create widget.": "Αδυναμία δημιουργίας γραφικού στοιχείου.",
"Reload widget": "Επαναφόρτωση γραφικού στοιχείου",
"You are not in this room.": "Δεν είστε μέλος αυτού του δωματίου.",
"You do not have permission to do that in this room.": "Δεν έχετε την άδεια να το κάνετε αυτό σε αυτό το δωμάτιο.",
"You are now ignoring %(userId)s": "Τώρα αγνοείτε τον/την %(userId)s",
@ -808,16 +638,10 @@
"%(widgetName)s widget modified by %(senderName)s": "Έγινε αλλαγή στο widget %(widgetName)s από τον/την %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "Προστέθηκε το widget %(widgetName)s από τον/την %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "Το widget %(widgetName)s αφαιρέθηκε από τον/την %(senderName)s",
"%(names)s and %(count)s others are typing|other": "Ο/Η %(names)s και άλλοι/ες %(count)s πληκτρολογούν",
"%(names)s and %(count)s others are typing|one": "Ο/Η %(names)s και άλλος ένας πληκτρολογούν",
"Message Pinning": "Καρφίτσωμα Μηνυμάτων",
"Hide avatar changes": "Απόκρυψη αλλαγών εικονιδίων χρηστών",
"Hide display name changes": "Απόκρυψη αλλαγών εμφανιζόμενων ονομάτων",
"Hide avatars in user and room mentions": "Απόκρυψη εικονιδίων στις αναφορές χρηστών και δωματίων",
"Enable URL previews for this room (only affects you)": "Ενεργοποίηση προεπισκόπισης URL για αυτό το δωμάτιο (επηρεάζει μόνο εσάς)",
"Delete %(count)s devices|other": "Διαγραφή %(count)s συσκευών",
"Delete %(count)s devices|one": "Διαγραφή συσκευής",
"Select devices": "Επιλογή συσκευών",
"Cannot add any more widgets": "Δεν είναι δυνατή η προσθήκη άλλων γραφικών στοιχείων",
"The maximum permitted number of widgets have already been added to this room.": "Ο μέγιστος επιτρεπτός αριθμός γραφικών στοιχείων έχει ήδη προστεθεί σε αυτό το δωμάτιο.",
"Add a widget": "Προσθέστε ένα γραφικό στοιχείο",
@ -849,7 +673,6 @@
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Διαβάστηκε από τον/την %(displayName)s (%(userName)s) στις %(dateTime)s",
"Room Notification": "Ειδοποίηση Δωματίου",
"Notify the whole room": "Ειδοποιήστε όλο το δωμάτιο",
"Sets the room topic": "Ορίζει το θέμα του δωματίου",
"Add Email Address": "Προσθήκη Διεύθυνσης Ηλ. Ταχυδρομείου",
"Add Phone Number": "Προσθήκη Τηλεφωνικού Αριθμού",
"Whether or not you're logged in (we don't record your username)": "Χωρίς να έχει σημασία εάν είστε συνδεδεμένοι (δεν καταγράφουμε το όνομα χρήστη σας)",
@ -898,7 +721,6 @@
"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> Θα αναρτήσουμε ένα σύνδεσμο προς το νέο δωμάτιο στη παλιά έκδοση του δωματίου - τα μέλη του δωματίου θα πρέπει να πατήσουν στον σύνδεσμο για να μπουν στο νέο δωμάτιο.",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Παρακαλώ επιβεβαιώστε ότι θα θέλατε να προχωρήσετε με την αναβάθμιση του δωματίου από <oldVersion /> σε <newVersion />.",
"Upgrade": "Αναβάθμιση",
"Changes your display nickname in the current room only": "Αλλάζει το εμφανιζόμενο ψευδώνυμο μόνο στο παρόν δωμάτιο",
"Changes the avatar of the current room": "Αλλάζει το άβαταρ αυτού του δωματίου",
"Changes your avatar in this current room only": "Αλλάζει το άβαταρ σας μόνο στο παρόν δωμάτιο",

View file

@ -154,13 +154,8 @@
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
"Upgrades a room to a new version": "Upgrades a room to a new version",
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
"Room upgrade confirmation": "Room upgrade confirmation",
"Upgrading a room can be destructive and isn't always necessary.": "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.": "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.",
"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 />.": "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 />.",
"<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>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.",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.",
"Upgrade": "Upgrade",
"Error upgrading room": "Error upgrading room",
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Changes your display nickname": "Changes your display nickname",
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
"Changes the avatar of the current room": "Changes the avatar of the current room",
@ -730,6 +725,7 @@
"Camera": "Camera",
"Voice & Video": "Voice & Video",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"<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>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.",
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
"this room": "this room",
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
@ -848,9 +844,9 @@
"If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.",
"Key request sent.": "Key request sent.",
"<requestLink>Re-request encryption keys</requestLink> from your other devices.": "<requestLink>Re-request encryption keys</requestLink> from your other devices.",
"Undecryptable": "Undecryptable",
"This message cannot be decrypted": "This message cannot be decrypted",
"Encrypted by an unverified device": "Encrypted by an unverified device",
"Unencrypted message": "Unencrypted message",
"Unencrypted": "Unencrypted",
"Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to bottom of page": "Scroll to bottom of page",
"device id: ": "device id: ",
@ -994,6 +990,8 @@
"Use an identity server in Settings to receive invites directly in Riot.": "Use an identity server in Settings to receive invites directly in Riot.",
"Share this email in Settings to receive invites directly in Riot.": "Share this email in Settings to receive invites directly in Riot.",
"Do you want to chat with %(user)s?": "Do you want to chat with %(user)s?",
"<userName/> wants to chat": "<userName/> wants to chat",
"Start chatting": "Start chatting",
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
"<userName/> invited you": "<userName/> invited you",
"Reject": "Reject",
@ -1009,6 +1007,7 @@
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
"Not now": "Not now",
"Don't ask me again": "Don't ask me again",
"Options": "Options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
"%(count)s unread messages.|other": "%(count)s unread messages.",
@ -1085,7 +1084,6 @@
"%(count)s verified Sign-In's|one": "1 verified Sign-In",
"Direct message": "Direct message",
"Unverify user": "Unverify user",
"Options": "Options",
"Remove from community": "Remove from community",
"Disinvite this user from community?": "Disinvite this user from community?",
"Remove this user from community?": "Remove this user from community?",
@ -1131,6 +1129,8 @@
"You sent a verification request": "You sent a verification request",
"Error decrypting video": "Error decrypting video",
"Show all": "Show all",
"Reactions": "Reactions",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
"<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.",
@ -1445,6 +1445,13 @@
"Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room",
"Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages",
"Automatically invite users": "Automatically invite users",
"Upgrade private room": "Upgrade private room",
"Upgrade public room": "Upgrade public room",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
"This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.",
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
"Upgrade": "Upgrade",
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
"Send Logs": "Send Logs",
@ -1570,6 +1577,7 @@
"Report Content": "Report Content",
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
"Notification settings": "Notification settings",
"All messages (noisy)": "All messages (noisy)",
"All messages": "All messages",
"Mentions only": "Mentions only",
@ -1589,6 +1597,7 @@
"Take picture": "Take picture",
"Remove for everyone": "Remove for everyone",
"Remove for me": "Remove for me",
"User Status": "User Status",
"powered by Matrix": "powered by Matrix",
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
"Custom Server Options": "Custom Server Options",

View file

@ -2,17 +2,12 @@
"Add a widget": "Add a widget",
"AM": "AM",
"PM": "PM",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
"Account": "Account",
"Access Token:": "Access Token:",
"Add a topic": "Add a topic",
"Add email address": "Add email address",
"Add phone number": "Add phone number",
"Admin": "Admin",
"VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
"No Microphones detected": "No Microphones detected",
"No Webcams detected": "No Webcams detected",
"No media permissions": "No media permissions",
@ -22,13 +17,11 @@
"Camera": "Camera",
"Advanced": "Advanced",
"Algorithm": "Algorithm",
"Hide removed messages": "Hide removed messages",
"Always show message timestamps": "Always show message timestamps",
"Authentication": "Authentication",
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
"and %(count)s others...|other": "and %(count)s others...",
"and %(count)s others...|one": "and one other...",
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
"A new password must be entered.": "A new password must be entered.",
"%(senderName)s answered the call.": "%(senderName)s answered the call.",
"An error has occurred.": "An error has occurred.",
@ -38,7 +31,6 @@
"Are you sure?": "Are you sure?",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
"Attachment": "Attachment",
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
"%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
@ -46,22 +38,17 @@
"Banned users": "Banned users",
"Bans user with given id": "Bans user with given id",
"Blacklisted": "Blacklisted",
"Bulk Options": "Bulk Options",
"Call Timeout": "Call Timeout",
"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>.",
"Can't load user settings": "Can't load user settings",
"Change Password": "Change Password",
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
"Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room",
"Changes your display nickname": "Changes your display nickname",
"Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key",
"Clear Cache and Reload": "Clear Cache and Reload",
"Clear Cache": "Clear Cache",
"Click here to fix": "Click here to fix",
"Click to mute audio": "Click to mute audio",
"Click to mute video": "Click to mute video",
@ -70,15 +57,9 @@
"Click to unmute audio": "Click to unmute audio",
"Command error": "Command error",
"Commands": "Commands",
"Conference call failed.": "Conference call failed.",
"Conference calling is in development and may not be reliable.": "Conference calling is in development and may not be reliable.",
"Conference calls are not supported in encrypted rooms": "Conference calls are not supported in encrypted rooms",
"Conference calls are not supported in this client": "Conference calls are not supported in this client",
"Confirm password": "Confirm password",
"Confirm your new password": "Confirm your new password",
"Continue": "Continue",
"Could not connect to the integration server": "Could not connect to the integration server",
"Create an account": "Create an account",
"Create Room": "Create Room",
"Cryptography": "Cryptography",
"Current password": "Current password",
@ -86,10 +67,8 @@
"Custom level": "Custom level",
"/ddg is not a command": "/ddg is not a command",
"Deactivate Account": "Deactivate Account",
"Deactivate my account": "Deactivate my account",
"Decrypt %(text)s": "Decrypt %(text)s",
"Decryption error": "Decryption error",
"Delete": "Delete",
"Deops user with given id": "Deops user with given id",
"Default": "Default",
"Delete widget": "Delete widget",
@ -99,28 +78,17 @@
"device id: ": "device id: ",
"Device key:": "Device key:",
"Devices": "Devices",
"Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room",
"Direct chats": "Direct chats",
"Disinvite": "Disinvite",
"Display name": "Display name",
"Displays action": "Displays action",
"Don't send typing notifications": "Don't send typing notifications",
"Download %(text)s": "Download %(text)s",
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
"Ed25519 fingerprint": "Ed25519 fingerprint",
"Edit": "Edit",
"Email": "Email",
"Email address": "Email address",
"Email address (optional)": "Email address (optional)",
"Email, name or matrix ID": "Email, name or matrix ID",
"Emoji": "Emoji",
"Enable encryption": "Enable encryption",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption",
"Encrypted room": "Encrypted room",
"%(senderName)s ended the call.": "%(senderName)s ended the call.",
"End-to-end encryption information": "End-to-end encryption information",
"End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable",
"Enter Code": "Enter Code",
"Error": "Error",
"Error decrypting attachment": "Error decrypting attachment",
"Event information": "Event information",
@ -140,15 +108,11 @@
"Failed to mute user": "Failed to mute user",
"Failed to reject invite": "Failed to reject invite",
"Failed to reject invitation": "Failed to reject invitation",
"Failed to save settings": "Failed to save settings",
"Failed to send email": "Failed to send email",
"Failed to send request.": "Failed to send request.",
"Failed to set avatar.": "Failed to set avatar.",
"Failed to set display name": "Failed to set display name",
"Failed to set up conference call": "Failed to set up conference call",
"Failed to toggle moderator status": "Failed to toggle moderator status",
"Failed to unban": "Failed to unban",
"Failed to upload file": "Failed to upload file",
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
"Failure to create room": "Failure to create room",
"Favourite": "Favorite",
@ -156,14 +120,10 @@
"Fill screen": "Fill screen",
"Filter room members": "Filter room members",
"Forget room": "Forget room",
"Forgot your password?": "Forgot your password?",
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
"Hangup": "Hangup",
"Hide read receipts": "Hide read receipts",
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
"Historical": "Historical",
"Homeserver is": "Homeserver is",
@ -173,9 +133,7 @@
"Import E2E room keys": "Import E2E room keys",
"Incorrect username and/or password.": "Incorrect username and/or password.",
"Incorrect verification code": "Incorrect verification code",
"Interface Language": "Interface Language",
"Invalid alias format": "Invalid alias format",
"Invalid address format": "Invalid address format",
"Invalid Email Address": "Invalid Email Address",
"Invalid file%(extra)s": "Invalid file%(extra)s",
"%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
@ -183,9 +141,7 @@
"Invited": "Invited",
"Invites": "Invites",
"Invites user with given id to current room": "Invites user with given id to current room",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address",
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias",
"%(displayName)s is typing": "%(displayName)s is typing",
"Sign in with": "Sign in with",
"Join Room": "Join Room",
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
@ -195,7 +151,6 @@
"Kick": "Kick",
"Kicks user with given id": "Kicks user with given id",
"Labs": "Labs",
"Ignored Users": "Ignored Users",
"Ignore": "Ignore",
"Unignore": "Unignore",
"User Options": "User Options",
@ -209,7 +164,6 @@
"%(targetName)s left the room.": "%(targetName)s left the room.",
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
"Local addresses for this room:": "Local addresses for this room:",
"Logged in as:": "Logged in as:",
"Logout": "Logout",
"Low priority": "Low priority",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
@ -221,23 +175,18 @@
"Markdown is disabled": "Markdown is disabled",
"Markdown is enabled": "Markdown is enabled",
"matrix-react-sdk version:": "matrix-react-sdk version:",
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
"Missing room_id in request": "Missing room_id in request",
"Missing user_id in request": "Missing user_id in request",
"Mobile phone number": "Mobile phone number",
"Mobile phone number (optional)": "Mobile phone number (optional)",
"Moderator": "Moderator",
"Mute": "Mute",
"Name": "Name",
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
"Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device",
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
"New password": "New password",
"New passwords don't match": "New passwords don't match",
"New passwords must match each other.": "New passwords must match each other.",
"none": "none",
"not set": "not set",
"not specified": "not specified",
"Notifications": "Notifications",
"(not supported by this browser)": "(not supported by this browser)",
@ -249,37 +198,28 @@
"No users have specific privileges in this room": "No users have specific privileges in this room",
"OK": "OK",
"olm version:": "olm version:",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)",
"Only people who have been invited": "Only people who have been invited",
"Operation failed": "Operation failed",
"Password": "Password",
"Password:": "Password:",
"Passwords can't be empty": "Passwords can't be empty",
"People": "People",
"Permissions": "Permissions",
"Phone": "Phone",
"%(senderName)s placed a %(callType)s call.": "%(senderName)s placed a %(callType)s call.",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
"Power level must be positive integer.": "Power level must be positive integer.",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
"Privacy warning": "Privacy warning",
"Privileged Users": "Privileged Users",
"Profile": "Profile",
"Reason": "Reason",
"Revoke Moderator": "Revoke Moderator",
"Revoke widget access": "Revoke widget access",
"Refer a friend to Riot:": "Refer a friend to Riot:",
"Register": "Register",
"%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
"Reject invitation": "Reject invitation",
"Remote addresses for this room:": "Remote addresses for this room:",
"Remove Contact Information?": "Remove Contact Information?",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.",
"Remove": "Remove",
"Remove %(threePid)s?": "Remove %(threePid)s?",
"%(senderName)s requested a VoIP conference.": "%(senderName)s requested a VoIP conference.",
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Results from DuckDuckGo": "Results from DuckDuckGo",
"Return to login screen": "Return to login screen",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
@ -287,11 +227,9 @@
"riot-web version:": "riot-web version:",
"Room %(roomId)s not visible": "Room %(roomId)s not visible",
"Room Colour": "Room Color",
"Room name (optional)": "Room name (optional)",
"Rooms": "Rooms",
"Save": "Save",
"Scroll to bottom of page": "Scroll to bottom of page",
"Scroll to unread messages": "Scroll to unread messages",
"Search": "Search",
"Search failed": "Search failed",
"Searches DuckDuckGo for results": "Searches DuckDuckGo for results",
@ -301,16 +239,13 @@
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
"Server error": "Server error",
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
"Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(",
"Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big",
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
"Session ID": "Session ID",
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
"Settings": "Settings",
"Show panel": "Show panel",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
"Signed Out": "Signed Out",
"Sign in": "Sign in",
@ -321,32 +256,19 @@
"Start Chat": "Start Chat",
"Submit": "Submit",
"Success": "Success",
"Tagged as: ": "Tagged as: ",
"The default role for new room members is": "The default role for new room members is",
"The main address for this room is": "The main address for this room is",
"The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.",
"This email address is already in use": "This email address is already in use",
"This email address was not found": "This email address was not found",
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads",
"The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload",
"The remote side failed to pick up": "The remote side failed to pick up",
"This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
"This room has no local addresses": "This room has no local addresses",
"This room is not recognised.": "This room is not recognized.",
"These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways",
"The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
"This phone number is already in use": "This phone number is already in use",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"This room's internal ID is": "This room's internal ID is",
"To reset your password, enter the email address linked to your account": "To reset your password, enter the email address linked to your account",
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"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.",
"Turn Markdown off": "Turn Markdown off",
"Turn Markdown on": "Turn Markdown on",
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).",
"Unable to add email address": "Unable to add email address",
"Unable to remove contact information": "Unable to remove contact information",
@ -356,7 +278,6 @@
"Unable to capture screen": "Unable to capture screen",
"Unable to enable Notifications": "Unable to enable Notifications",
"Unable to load device list": "Unable to load device list",
"Unencrypted room": "Unencrypted room",
"unencrypted": "unencrypted",
"unknown device": "unknown device",
"unknown error code": "unknown error code",
@ -367,14 +288,10 @@
"Unrecognised room alias:": "Unrecognized room alias:",
"Upload avatar": "Upload avatar",
"Upload Failed": "Upload Failed",
"Upload Files": "Upload Files",
"Upload file": "Upload file",
"Usage": "Usage",
"Use compact timeline layout": "Use compact timeline layout",
"Use with caution": "Use with caution",
"User ID": "User ID",
"User Interface": "User Interface",
"User name": "User name",
"Users": "Users",
"Verification Pending": "Verification Pending",
"Verification": "Verification",
@ -388,34 +305,26 @@
"(could not connect media)": "(could not connect media)",
"(no answer)": "(no answer)",
"(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
"Warning!": "Warning!",
"WARNING: Device already verified, but keys do NOT MATCH!": "WARNING: Device already verified, but keys do NOT MATCH!",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!",
"Who can access this room?": "Who can access this room?",
"Who can read history?": "Who can read history?",
"Who would you like to add to this room?": "Who would you like to add to this room?",
"Who would you like to communicate with?": "Who would you like to communicate with?",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.",
"You are already in a call.": "You are already in a call.",
"You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.",
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
"You do not have permission to post to this room": "You do not have permission to post to this room",
"You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s",
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have no visible notifications": "You have no visible notifications",
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
"You need to be logged in.": "You need to be logged in.",
"You need to enter a user name.": "You need to enter a user name.",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
"Your password has been reset": "Your password has been reset",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
"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?",
"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 should not yet trust it to secure data": "You should not yet trust it to secure data",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"Sun": "Sun",
"Mon": "Mon",
@ -442,20 +351,8 @@
"Set a display name:": "Set a display name:",
"Upload an avatar:": "Upload an avatar:",
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
"Missing password.": "Missing password.",
"Passwords don't match.": "Passwords don't match.",
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Password too short (min %(MIN_PASSWORD_LENGTH)s).",
"This doesn't look like a valid email address.": "This doesn't look like a valid email address.",
"This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.",
"An unknown error occurred.": "An unknown error occurred.",
"I already have an account": "I already have an account",
"An error occurred: %(error_string)s": "An error occurred: %(error_string)s",
"Topic": "Topic",
"Make Moderator": "Make Moderator",
"Make this room private": "Make this room private",
"Share message history with new users": "Share message history with new users",
"Encrypt room": "Encrypt room",
"There are no visible files in this room": "There are no visible files in this room",
"Room": "Room",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
@ -464,15 +361,8 @@
"Active call": "Active call",
"bold": "bold",
"italic": "italic",
"strike": "strike",
"underline": "underline",
"code": "code",
"quote": "quote",
"bullet": "bullet",
"numbullet": "numbullet",
"Please select the destination room for this message": "Please select the destination room for this message",
"Start automatically after system login": "Start automatically after system login",
"Desktop specific": "Desktop specific",
"Analytics": "Analytics",
"Banned by %(displayName)s": "Banned by %(displayName)s",
"Options": "Options",
@ -490,9 +380,7 @@
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
"You must join the room to see its files": "You must join the room to see its files",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Start new chat": "Start new chat",
"Failed to invite": "Failed to invite",
"Failed to invite user": "Failed to invite user",
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
"Confirm Removal": "Confirm Removal",
"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.": "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.",
@ -503,7 +391,6 @@
"Device name": "Device name",
"Device key": "Device key",
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.",
"In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.",
"Verify device": "Verify device",
"I verify that the keys match": "I verify that the keys match",
"Unable to restore session": "Unable to restore session",
@ -519,38 +406,13 @@
"Verify...": "Verify...",
"ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User",
"To send messages, you must be a": "To send messages, you must be a",
"To invite users into the room, you must be a": "To invite users into the room, you must be a",
"To configure the room, you must be a": "To configure the room, you must be a",
"To kick users, you must be a": "To kick users, you must be a",
"To ban users, you must be a": "To ban users, you must be a",
"To remove other users' messages, you must be a": "To remove other users' messages, you must be a",
"To send events of type <eventType/>, you must be a": "To send events of type <eventType/>, you must be a",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
"To change the room's name, you must be a": "To change the room's name, you must be a",
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
"To change the room's history visibility, you must be a": "To change the room's history visibility, you must be a",
"To change the permissions in the room, you must be a": "To change the permissions in the room, you must be a",
"To change the topic, you must be a": "To change the topic, you must be a",
"To modify widgets in the room, you must be a": "To modify widgets in the room, you must be a",
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
"Sign in with CAS": "Sign in with CAS",
"Custom Server Options": "Custom Server Options",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.",
"This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.",
"Dismiss": "Dismiss",
"Please check your email to continue registration.": "Please check your email to continue registration.",
"Token incorrect": "Token incorrect",
"Please enter the code it contains:": "Please enter the code it contains:",
"powered by Matrix": "powered by Matrix",
"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?",
"You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s",
"Default server": "Default server",
"Custom server": "Custom server",
"Home server URL": "Home server URL",
"Identity server URL": "Identity server URL",
"What does this mean?": "What does this mean?",
"Error decrypting audio": "Error decrypting audio",
"Error decrypting image": "Error decrypting image",
"Error decrypting video": "Error decrypting video",
@ -573,10 +435,7 @@
"Admin Tools": "Admin Tools",
"Alias (optional)": "Alias (optional)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
"Close": "Close",
"%(count)s new messages|one": "%(count)s new message",
"%(count)s new messages|other": "%(count)s new messages",
"Custom": "Custom",
"Decline": "Decline",
"Disable Notifications": "Disable Notifications",
@ -584,12 +443,8 @@
"Create new room": "Create new room",
"Room directory": "Room directory",
"Start chat": "Start chat",
"Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one",
"Drop File Here": "Drop File Here",
"Encrypted by a verified device": "Encrypted by a verified device",
"Encrypted by an unverified device": "Encrypted by an unverified device",
"Encryption is enabled in this room": "Encryption is enabled in this room",
"Encryption is not enabled in this room": "Encryption is not enabled in this room",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
"Failed to upload profile picture!": "Failed to upload profile picture!",
@ -599,12 +454,9 @@
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
"Last seen": "Last seen",
"Level:": "Level:",
"No display name": "No display name",
"Private Chat": "Private Chat",
"Public Chat": "Public Chat",
"Reason: %(reasonText)s": "Reason: %(reasonText)s",
"Rejoin": "Rejoin",
"Room contains unknown devices": "Room contains unknown devices",
"%(roomName)s does not exist.": "%(roomName)s does not exist.",
"%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.",
@ -613,52 +465,31 @@
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
"Start authentication": "Start authentication",
"The phone number entered looks invalid": "The phone number entered looks invalid",
"This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:",
"This room": "This room",
"To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
"Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.",
"Undecryptable": "Undecryptable",
"Unencrypted message": "Unencrypted message",
"unknown caller": "unknown caller",
"Unnamed Room": "Unnamed Room",
"Unverified": "Unverified",
"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",
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Upload new:": "Upload new:",
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
"Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
"Verified": "Verified",
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?",
"You already have existing direct chats with this user:": "You already have existing direct chats with this user:",
"You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.",
"You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.",
"You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.",
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
"Your home server does not support device management.": "Your home server does not support device management.",
"(~%(count)s results)|one": "(~%(count)s result)",
"(~%(count)s results)|other": "(~%(count)s results)",
"New Password": "New Password",
"Device Name": "Device Name",
"Start chatting": "Start chatting",
"Start Chatting": "Start Chatting",
"Click on the button below to start chatting!": "Click on the button below to start chatting!",
"Username available": "Username available",
"Username not available": "Username not available",
"Something went wrong!": "Something went wrong!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.",
"If you would like to create a Matrix account you can <a>register</a> now.": "If you would like to create a Matrix account you can <a>register</a> now.",
"If you already have a Matrix account you can <a>log in</a> instead.": "If you already have a Matrix account you can <a>log in</a> instead.",
"You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.",
"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?",
"Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
"Do you want to set an email address?": "Do you want to set an email address?",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Press <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone",
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory",
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
"Skip": "Skip",
"Start verification": "Start verification",
"Share without verifying": "Share without verifying",
@ -666,26 +497,17 @@
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
"Encryption key request": "Encryption key request",
"Updates": "Updates",
"Check for update": "Check for update",
"Allow": "Allow",
"Cannot add any more widgets": "Cannot add any more widgets",
"Changes colour scheme of current room": "Changes color scheme of current room",
"Define the power level of a user": "Define the power level of a user",
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
"Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
"Integrations Error": "Integrations Error",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Sets the room topic": "Sets the room topic",
"Sets the room name": "Sets the room name",
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
"To get started, please pick a username!": "To get started, please pick a username!",
"Unable to create widget.": "Unable to create widget.",
"Unbans user with given id": "Unbans user with given id",
"You are not in this room.": "You are not in this room.",
"You do not have permission to do that in this room.": "You do not have permission to do that in this room.",
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
"Loading device info...": "Loading device info...",
"Message removed by %(userId)s": "Message removed by %(userId)s",
"Example": "Example",
@ -695,15 +517,12 @@
"Featured Users:": "Featured Users:",
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Failed to upload image": "Failed to upload image",
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"Fetching third party location failed": "Fetching third party location failed",
"A new version of Riot is available.": "A new version of Riot is available.",
"Couldn't load home page": "Couldn't load home page",
"All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.",
"Uploading report": "Uploading report",
"Sunday": "Sunday",
@ -712,15 +531,11 @@
"Notification targets": "Notification targets",
"Failed to set direct chat tag": "Failed to set direct chat tag",
"Today": "Today",
"Failed to get protocol list from Home Server": "Failed to get protocol list from Home Server",
"You are not receiving desktop notifications": "You are not receiving desktop notifications",
"Friday": "Friday",
"Update": "Update",
"What's New": "What's New",
"Add an email address above to configure email notifications": "Add an email address above to configure email notifications",
"Expand panel": "Expand panel",
"On": "On",
"Filter room names": "Filter room names",
"Changelog": "Changelog",
"Waiting for response from server": "Waiting for response from server",
"Leave": "Leave",
@ -729,13 +544,10 @@
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Forget": "Forget",
"World readable": "World readable",
"#example": "#example",
"Hide panel": "Hide panel",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Cancel Sending": "Cancel Sending",
"Warning": "Warning",
"This Room": "This Room",
"The Home Server may be too old to support third party networks": "The Home Server may be too old to support third party networks",
"Noisy": "Noisy",
"Room not found": "Room not found",
"Messages containing my display name": "Messages containing my display name",
@ -760,8 +572,6 @@
"Keywords": "Keywords",
"Unpin Message": "Unpin Message",
"Enable notifications for this account": "Enable notifications for this account",
"Directory": "Directory",
"Failed to get public room list": "Failed to get public room list",
"Messages containing <span>keywords</span>": "Messages containing <span>keywords</span>",
"Error saving email notification preferences": "Error saving email notification preferences",
"Tuesday": "Tuesday",
@ -769,7 +579,6 @@
"Search…": "Search…",
"Remove %(name)s from the directory?": "Remove %(name)s from the directory?",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
"Enable desktop notifications": "Enable desktop notifications",
"Unnamed room": "Unnamed room",
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
"All messages (noisy)": "All messages (noisy)",
@ -782,10 +591,7 @@
"Monday": "Monday",
"Remove from Directory": "Remove from Directory",
"Enable them now": "Enable them now",
"Messages containing my user name": "Messages containing my user name",
"Collecting logs": "Collecting logs",
"more": "more",
"Search for a room": "Search for a room",
"(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)",
"All Rooms": "All Rooms",
"Wednesday": "Wednesday",
@ -818,26 +624,22 @@
"Low Priority": "Low Priority",
"Unable to fetch notification target list": "Unable to fetch notification target list",
"Set Password": "Set Password",
"Enable audible notifications in web client": "Enable audible notifications in web client",
"Off": "Off",
"Riot does not know how to join a room on this network": "Riot does not know how to join a room on this network",
"Mentions only": "Mentions only",
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
"You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.",
"Enable email notifications": "Enable email notifications",
"Login": "Login",
"No rooms to show": "No rooms to show",
"Download this file": "Download this file",
"Pin Message": "Pin Message",
"Failed to change settings": "Failed to change settings",
"View Source": "View Source",
"Collapse panel": "Collapse panel",
"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!": "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!",
"Checking for an update...": "Checking for an update...",
"There are advanced notifications which are not shown here": "There are advanced notifications which are not shown here",
"The platform you're on": "The platform you're on",
"The version of Riot.im": "The version of Riot.im",
"Whether or not you're logged in (we don't record your user name)": "Whether or not you're logged in (we don't record your user name)",
"Your language of choice": "Your language of choice",
"Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
@ -857,7 +659,6 @@
"Answer Anyway": "Answer Anyway",
"Call": "Call",
"Answer": "Answer",
"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",
"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!",
@ -867,7 +668,6 @@
"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",
"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?",
@ -910,7 +710,6 @@
"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 />.": "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 />.",
"<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>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.",
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.",
"Upgrade": "Upgrade",
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
"Changes your avatar in this current room only": "Changes your avatar in this current room only",
"Gets or sets the room topic": "Gets or sets the room topic",

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