Merge branches 'develop' and 't3chguy/m.relates_to' of github.com:matrix-org/matrix-react-sdk into t3chguy/m.relates_to

This commit is contained in:
Michael Telatynski 2018-05-02 13:08:38 +01:00
commit f2102e283c
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
71 changed files with 1699 additions and 1077 deletions

View file

@ -9,16 +9,9 @@ set -ev
RIOT_WEB_DIR=riot-web
REACT_SDK_DIR=`pwd`
curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
echo "Determined branch to be $curbranch"
git clone https://github.com/vector-im/riot-web.git \
"$RIOT_WEB_DIR"
scripts/fetchdep.sh vector-im riot-web
cd "$RIOT_WEB_DIR"
git checkout "$curbranch" || git checkout develop
mkdir node_modules
npm install

View file

@ -15,6 +15,5 @@ addons:
chrome: stable
install:
- npm install
- (cd node_modules/matrix-js-sdk && npm install)
script:
./scripts/travis.sh

6
package-lock.json generated
View file

@ -3719,9 +3719,9 @@
"integrity": "sha1-uKnFSTISqTkvAiK2SclhFJfr+4g="
},
"hoek": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"home-or-tmp": {
"version": "2.0.0",

View file

@ -125,8 +125,9 @@
"karma-spec-reporter": "^0.0.31",
"karma-summary-reporter": "^1.3.3",
"karma-webpack": "^1.7.0",
"matrix-mock-request": "^1.2.1",
"matrix-react-test-utils": "^0.1.1",
"mocha": "^2.4.5",
"mocha": "^5.0.5",
"parallelshell": "^3.0.2",
"react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1",

View file

@ -250,6 +250,7 @@ textarea {
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
background-color: $warning-color;
border: solid 1px $warning-color;
color: $accent-fg-color;
}
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {

View file

@ -50,13 +50,13 @@ limitations under the License.
margin-right: 0px;
}
.mx_FilePanel .mx_EventTile .mx_MImageBody_download {
.mx_FilePanel .mx_EventTile .mx_MFileBody_download {
display: flex;
font-size: 14px;
color: $event-timestamp-color;
}
.mx_FilePanel .mx_EventTile .mx_MImageBody_downloadLink {
.mx_FilePanel .mx_EventTile .mx_MFileBody_downloadLink {
flex: 1 1 auto;
color: $light-fg-color;
}

View file

@ -251,3 +251,7 @@ input.mx_UserSettings_phoneNumberField {
color: $accent-color;
word-break: break-all;
}
.mx_UserSettings_analyticsModal table {
margin: 10px 0px;
}

View file

@ -15,6 +15,10 @@ limitations under the License.
*/
.mx_MImageBody {
display: block;
margin-right: 34px;
display: block;
margin-right: 34px;
}
.mx_MImageBody_thumbnail {
max-width: 100%;
}

View file

@ -294,6 +294,16 @@ limitations under the License.
cursor: pointer;
}
.mx_EventTile_e2eIcon_hidden {
display: none;
}
/* always override hidden attribute for blocked and warning */
.mx_EventTile_e2eIcon_hidden[src="img/e2e-blocked.svg"],
.mx_EventTile_e2eIcon_hidden[src="img/e2e-warning.svg"] {
display: block;
}
.mx_EventTile_keyRequestInfo {
font-size: 12px;
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="612px" height="612px" viewBox="0 90 612 612" enable-background="new 0 90 612 612" xml:space="preserve">
<path fill="#76CFA6" d="M612,149.5v135.983c0,22.802-27.582,33.979-43.531,18.032l-37.939-37.941L271.787,524.317
c-9.959,9.958-26.104,9.958-36.062,0l-24.041-24.042c-9.959-9.958-9.959-26.104,0-36.062l258.746-258.746l-37.935-37.937
C416.48,151.519,427.822,124,450.525,124H586.5C600.583,124,612,135.417,612,149.5z M432.469,411.719l-17,17
c-4.782,4.782-7.469,11.269-7.469,18.031V600H68V260h280.5c6.763,0,13.248-2.687,18.03-7.468l17-17
C399.595,219.467,388.218,192,365.5,192H51c-28.167,0-51,22.833-51,51v374c0,28.167,22.833,51,51,51h374c28.167,0,51-22.833,51-51
V429.749C476,407.031,448.532,395.654,432.469,411.719z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

12
scripts/fetchdep.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
set -e
org="$1"
repo="$2"
curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
echo "Determined branch to be $curbranch"
git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" || git clone https://github.com/$org/$repo.git $repo --branch develop

View file

@ -2,6 +2,14 @@
set -ex
scripts/fetchdep.sh matrix-org matrix-js-sdk
rm -r node_modules/matrix-js-sdk || true
ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
cd matrix-js-sdk
npm install
cd ..
npm run test
./.travis-test-riot.sh

View file

@ -16,17 +16,33 @@
import { getCurrentLanguage, _t, _td } from './languageHandler';
import PlatformPeg from './PlatformPeg';
import SdkConfig, { DEFAULTS } from './SdkConfig';
import SdkConfig from './SdkConfig';
import Modal from './Modal';
import sdk from './index';
function getRedactedHash() {
return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/;
const hashVarRegex = /#\/(group|room|user)\/.*$/;
// Remove all but the first item in the hash path. Redact unexpected hashes.
function getRedactedHash(hash) {
// Don't leak URLs we aren't expecting - they could contain tokens/PII
const match = hashRegex.exec(hash);
if (!match) {
console.warn(`Unexpected hash location "${hash}"`);
return '#/<unexpected hash location>';
}
if (hashVarRegex.test(hash)) {
return hash.replace(hashVarRegex, "#/$1/<redacted>");
}
return hash.replace(hashRegex, "#/$1");
}
// Return the current origin and hash separated with a `/`. This does not include query parameters.
function getRedactedUrl() {
// hardcoded url to make piwik happy
return 'https://riot.im/app/' + getRedactedHash();
const { origin, pathname, hash } = window.location;
return origin + pathname + getRedactedHash(hash);
}
const customVariables = {
@ -174,6 +190,7 @@ class Analytics {
}
_setVisitVariable(key, value) {
if (this.disabled) return;
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
}
@ -181,8 +198,10 @@ class Analytics {
if (this.disabled) return;
const config = SdkConfig.get();
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls;
const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls;
if (!config.piwik) return;
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
const whitelistedISUrls = config.piwik.whitelistedISUrls || [];
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
@ -199,11 +218,25 @@ class Analytics {
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
const resolution = `${window.screen.width}x${window.screen.height}`;
const otherVariables = [
{
expl: _td('Every page you use in the app'),
value: _t(
'e.g. <CurrentPageURL>',
{},
{
CurrentPageURL: getRedactedUrl(),
},
),
},
{ expl: _td('Your User Agent'), value: navigator.userAgent },
{ expl: _td('Your device resolution'), value: resolution },
];
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'),
description: <div>
description: <div className="mx_UserSettings_analyticsModal">
<div>
{ _t('The information being sent to us to help make Riot.im better includes:') }
</div>
@ -212,19 +245,14 @@ class Analytics {
<td>{ _t(customVariables[row[0]].expl) }</td>
<td><code>{ row[1] }</code></td>
</tr>) }
</table>
<br />
<div>
{ _t('We also record each page you use in the app (currently <CurrentPageHash>), your User Agent'
+ ' (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).',
{},
{
CurrentPageHash: <code>{ getRedactedHash() }</code>,
CurrentUserAgent: <code>{ navigator.userAgent }</code>,
CurrentDeviceResolution: <code>{ resolution }</code>,
},
{ otherVariables.map((item, index) =>
<tr key={index}>
<td>{ _t(item.expl) }</td>
<td><code>{ item.value }</code></td>
</tr>,
) }
</table>
<div>
{ _t('Where this page includes identifiable information, such as a room, '
+ 'user or group ID, that data is removed before being sent to the server.') }
</div>

View file

@ -19,7 +19,7 @@ import sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import GroupStoreCache from './stores/GroupStoreCache';
import GroupStore from './stores/GroupStore';
export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => {
@ -116,11 +116,10 @@ function _onGroupInviteFinished(groupId, addrs) {
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get();
const groupStore = GroupStoreCache.getGroupStore(groupId);
const errorList = [];
return Promise.all(addrs.map((addr) => {
return groupStore
.addRoomToGroup(addr.address, addRoomsPublicly)
return GroupStore
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); })
.then(() => {
const roomId = addr.address;

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -64,33 +65,33 @@ import sdk from './index';
* Resolves to `true` if we ended up starting a session, or `false` if we
* failed.
*/
export function loadSession(opts) {
const fragmentQueryParams = opts.fragmentQueryParams || {};
let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl;
const guestIsUrl = opts.guestIsUrl;
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
export async function loadSession(opts) {
try {
let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl;
const guestIsUrl = opts.guestIsUrl;
const fragmentQueryParams = opts.fragmentQueryParams || {};
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false;
}
if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false;
}
if (enableGuest &&
fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token
) {
console.log("Using guest access credentials");
return _doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id,
accessToken: fragmentQueryParams.guest_access_token,
homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl,
guest: true,
}, true).then(() => true);
}
return _restoreFromLocalStorage().then((success) => {
if (enableGuest &&
fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token
) {
console.log("Using guest access credentials");
return _doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id,
accessToken: fragmentQueryParams.guest_access_token,
homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl,
guest: true,
}, true).then(() => true);
}
const success = await _restoreFromLocalStorage();
if (success) {
return true;
}
@ -101,7 +102,9 @@ export function loadSession(opts) {
// fall back to login screen
return false;
});
} catch (e) {
return _handleLoadSessionFailure(e);
}
}
/**
@ -195,9 +198,9 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. teamToken, isGuest etc.)
function _restoreFromLocalStorage() {
async function _restoreFromLocalStorage() {
if (!localStorage) {
return Promise.resolve(false);
return false;
}
const hsUrl = localStorage.getItem("mx_hs_url");
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
@ -215,26 +218,23 @@ function _restoreFromLocalStorage() {
if (accessToken && userId && hsUrl) {
console.log(`Restoring session for ${userId}`);
try {
return _doSetLoggedIn({
userId: userId,
deviceId: deviceId,
accessToken: accessToken,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
}, false).then(() => true);
} catch (e) {
return _handleRestoreFailure(e);
}
await _doSetLoggedIn({
userId: userId,
deviceId: deviceId,
accessToken: accessToken,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
}, false);
return true;
} else {
console.log("No previous session found.");
return Promise.resolve(false);
return false;
}
}
function _handleRestoreFailure(e) {
console.log("Unable to restore session", e);
function _handleLoadSessionFailure(e) {
console.log("Unable to load session", e);
const def = Promise.defer();
const SessionRestoreErrorDialog =
@ -255,7 +255,7 @@ function _handleRestoreFailure(e) {
}
// try, try again
return _restoreFromLocalStorage();
return loadSession();
});
}

View file

@ -21,13 +21,6 @@ const DEFAULTS = {
integrations_rest_url: "https://scalar.vector.im/api",
// Where to send bug reports. If not specified, bugs cannot be sent.
bug_report_endpoint_url: null,
piwik: {
url: "https://piwik.riot.im/",
whitelistedHSUrls: ["https://matrix.org"],
whitelistedISUrls: ["https://vector.im", "https://matrix.org"],
siteId: 1,
},
};
class SdkConfig {
@ -52,4 +45,3 @@ class SdkConfig {
}
module.exports = SdkConfig;
module.exports.DEFAULTS = DEFAULTS;

View file

@ -58,7 +58,7 @@ function textForMemberEvent(ev) {
});
} else if (!prevContent.displayname && content.displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', {
senderName,
senderName: ev.getSender(),
displayName: content.displayname,
});
} else if (prevContent.displayname && !content.displayname) {

View file

@ -101,8 +101,13 @@ export default class UserProvider extends AutocompleteProvider {
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
completions = this.matcher.match(command[0]).map((user) => {
if (!command) return completions;
const fullMatch = command[0];
// Don't search if the query is a single "@"
if (fullMatch && fullMatch !== '@') {
completions = this.matcher.match(fullMatch).map((user) => {
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
return {
// Length of completion should equal length of text in decorator. draft-js

View file

@ -27,7 +27,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
@ -93,8 +92,8 @@ const CategoryRoomList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupStore
.addRoomToGroupSummary(addr.address)
return GroupStore
.addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
@ -174,7 +173,8 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupStore.removeRoomFromGroupSummary(
GroupStore.removeRoomFromGroupSummary(
this.props.groupId,
this.props.summaryInfo.room_id,
).catch((err) => {
console.error('Error whilst removing room from group summary', err);
@ -269,7 +269,7 @@ const RoleUserList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupStore
return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
@ -344,7 +344,8 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupStore.removeUserFromGroupSummary(
GroupStore.removeUserFromGroupSummary(
this.props.groupId,
this.props.summaryInfo.user_id,
).catch((err) => {
console.error('Error whilst removing user from group summary', err);
@ -390,15 +391,6 @@ const FeaturedUser = React.createClass({
},
});
const GroupContext = {
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
};
CategoryRoomList.contextTypes = GroupContext;
FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupContext;
const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite";
@ -415,12 +407,6 @@ export default React.createClass({
groupStore: PropTypes.instanceOf(GroupStore),
},
getChildContext: function() {
return {
groupStore: this._groupStore,
};
},
getInitialState: function() {
return {
summary: null,
@ -440,6 +426,7 @@ export default React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
@ -448,8 +435,8 @@ export default React.createClass({
},
componentWillUnmount: function() {
this._unmounted = true;
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupStore.removeAllListeners();
},
componentWillReceiveProps: function(newProps) {
@ -464,8 +451,7 @@ export default React.createClass({
},
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
if (this._unmounted || group.groupId !== this.props.groupId) return;
if (group.myMembership === 'leave') {
// Leave settings - the user might have clicked the "Leave" button
this._closeSettings();
@ -478,34 +464,11 @@ export default React.createClass({
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
}
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary();
if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which
// requires that the fields are strings, not null)
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
summary.profile[k] = summary.profile[k] || "";
});
}
this.setState({
summary,
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: this._groupStore.getGroupPublicity(),
isUserPrivileged: this._groupStore.isUserPrivileged(),
groupRooms: this._groupStore.getGroupRooms(),
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
isUserMember: this._groupStore.getGroupMembers().some(
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
});
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
let willDoOnboarding = false;
this._groupStore.on('error', (err) => {
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
GroupStore.on('error', (err, errorGroupId) => {
if (this._unmounted || groupId !== errorGroupId) return;
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
action: 'do_after_sync_prepared',
@ -520,15 +483,45 @@ export default React.createClass({
this.setState({
summary: null,
error: err,
editing: false,
});
});
},
onGroupStoreUpdated(firstInit) {
if (this._unmounted) return;
const summary = GroupStore.getSummary(this.props.groupId);
if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which
// requires that the fields are strings, not null)
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
summary.profile[k] = summary.profile[k] || "";
});
}
this.setState({
summary,
summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
groupRooms: GroupStore.getGroupRooms(this.props.groupId),
groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
// XXX: This might not work but this.props.groupIsNew unused anyway
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
},
_fetchInviterProfile(userId) {
this.setState({
inviterProfileBusy: true,
});
this._matrixClient.getProfileInfo(userId).then((resp) => {
if (this._unmounted) return;
this.setState({
inviterProfile: {
avatarUrl: resp.avatar_url,
@ -538,6 +531,7 @@ export default React.createClass({
}).catch((e) => {
console.error('Error getting group inviter profile', e);
}).finally(() => {
if (this._unmounted) return;
this.setState({
inviterProfileBusy: false,
});
@ -677,7 +671,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.acceptGroupInvite().then(() => {
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -696,7 +690,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -715,7 +709,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.joinGroup().then(() => {
GroupStore.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -743,7 +737,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -351,16 +351,16 @@ export default React.createClass({
guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).catch((e) => {
console.error(`Error attempting to load session: ${e}`);
return false;
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
});
}).done();
// Note we don't catch errors from this: we catch everything within
// loadSession as there's logic there to ask the user if they want
// to try logging out.
});
},
componentWillUnmount: function() {

View file

@ -27,7 +27,7 @@ import Analytics from '../../Analytics';
import RateLimitedFunc from '../../ratelimitedfunc';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import { formatCount } from '../../utils/FormattingUtils';
@ -120,7 +120,7 @@ module.exports = React.createClass({
if (this.context.matrixClient) {
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
},
getInitialState: function() {
@ -132,26 +132,23 @@ module.exports = React.createClass({
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
_initGroupStore(groupId) {
if (!groupId) return;
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
onGroupStoreUpdated: function() {
this.setState({
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
},

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -63,6 +63,7 @@ const gHVersionLabel = function(repo, token='') {
const SIMPLE_SETTINGS = [
{ id: "urlPreviewsEnabled" },
{ id: "autoplayGifsAndVideos" },
{ id: "alwaysShowEncryptionIcons" },
{ id: "hideReadReceipts" },
{ id: "dontSendTypingNotifications" },
{ id: "alwaysShowTimestamps" },
@ -801,10 +802,10 @@ module.exports = React.createClass({
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contian messages.",
"other users. They do not contain messages.",
)
}</p>
<button className="mx_UserSettings_button danger"
<button className="mx_UserSettings_button"
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
</button>
</div>

View file

@ -22,7 +22,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -243,9 +243,8 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
const results = [];
groupStore.getGroupRooms().forEach((r) => {
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -36,8 +37,18 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
// Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
// cancelled (BaseDialog itself only calls this with false).
onFinished: PropTypes.func.isRequired,
// Whether the dialog should have a 'close' button that will
// cause the dialog to be cancelled. This should only be set
// to false if there is nothing the app can sensibly do if the
// dialog is cancelled, eg. "We can't restore your session and
// the app cannot work". Default: true.
hasCancel: PropTypes.bool,
// called when a key is pressed
onKeyDown: PropTypes.func,
@ -56,6 +67,12 @@ export default React.createClass({
contentId: React.PropTypes.string,
},
getDefaultProps: function() {
return {
hasCancel: true,
};
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
@ -74,15 +91,15 @@ export default React.createClass({
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (e.keyCode === KeyCode.ESCAPE) {
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
this.props.onFinished(false);
}
},
_onCancelClick: function(e) {
this.props.onFinished();
this.props.onFinished(false);
},
render: function() {
@ -101,11 +118,11 @@ export default React.createClass({
// AT users can skip its presentation.
aria-describedby={this.props.contentId}
>
<AccessibleButton onClick={this._onCancelClick}
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
</AccessibleButton> : null }
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title }
</div>

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -105,6 +106,8 @@ export default class BugReportDialog extends React.Component {
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let error = null;
if (this.state.err) {
@ -113,13 +116,6 @@ export default class BugReportDialog extends React.Component {
</div>;
}
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel}>
{ _t("Cancel") }
</button>;
}
let progress = null;
if (this.state.busy) {
progress = (
@ -131,11 +127,11 @@ export default class BugReportDialog extends React.Component {
}
return (
<div className="mx_BugReportDialog">
<div className="mx_Dialog_title">
{ _t("Submit debug logs") }
</div>
<div className="mx_Dialog_content">
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{ _t(
"Debug logs contain application usage data including your " +
@ -146,7 +142,7 @@ export default class BugReportDialog extends React.Component {
</p>
<p>
{ _t(
"<a>Click here</a> to create a GitHub issue.",
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
{},
{
a: (sub) => <a
@ -191,19 +187,13 @@ export default class BugReportDialog extends React.Component {
{progress}
{error}
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onSubmit}
autoFocus={true}
disabled={this.state.busy}
>
{ _t("Send logs") }
</button>
{cancelButton}
</div>
</div>
<DialogButtons primaryButton={_t("Send logs")}
onPrimaryButtonClick={this._onSubmit}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -30,59 +31,79 @@ export default React.createClass({
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
if (this.refs.bugreportLink) {
this.refs.bugreportLink.focus();
}
},
_sendBugReport: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
},
_continueClicked: function() {
this.props.onFinished(true);
_onClearStorageClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"),
description:
<div>{ _t("Log out and remove encryption keys?") }</div>,
button: _t("Sign out"),
danger: true,
onFinished: this.props.onFinished,
});
},
_onRefreshClick: function() {
// Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful.
window.location.reload(true);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bugreport;
const clearStorageButton = (
<button onClick={this._onClearStorageClick} className="danger">
{ _t("Clear Storage and Sign Out") }
</button>
);
let dialogButtons;
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>
{ _t(
"Otherwise, <a>click here</a> to send a bug report.",
{},
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
key="bugreport" href='#'>{ sub }</a> },
) }
</p>
);
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
onPrimaryButtonClick={this._sendBugReport}
focus={true}
hasCancel={false}
>
{ clearStorageButton }
</DialogButtons>;
} else {
dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
onPrimaryButtonClick={this._onRefreshClick}
focus={true}
hasCancel={false}
>
{ clearStorageButton }
</DialogButtons>;
}
const shouldFocusContinueButton =!(bugreport==true);
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}
title={_t('Unable to restore session')}
contentId='mx_Dialog_content'
hasCancel={false}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{ _t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.") }</p>
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
<p>{ _t("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.") }</p>
<p>{ _t(
"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.",
) }</p>
{ bugreport }
<p>{ _t(
"Clearing your browser's storage may fix the problem, but will sign you " +
"out and cause any encrypted chat history to become unreadable.",
) }</p>
</div>
<DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
onCancel={this.props.onFinished} />
{ dialogButtons }
</BaseDialog>
);
},

View file

@ -54,6 +54,7 @@ export default class AppTile extends React.Component {
this._onInitialLoad = this._onInitialLoad.bind(this);
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
}
/**
@ -499,6 +500,13 @@ export default class AppTile extends React.Component {
}
}
_onPopoutWidgetClick(e) {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes,noreferrer=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
}
render() {
let appTileBody;
@ -581,6 +589,7 @@ export default class AppTile extends React.Component {
// Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
const showPictureSnapshotIcon = 'img/camera_green.svg';
const popoutWidgetIcon = 'img/button-new-window.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
return (
@ -599,15 +608,25 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <TintableSvgButton
src={showPictureSnapshotIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Picture')}
onClick={this._onSnapshotClick}
width="10"
height="10"
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <TintableSvgButton
src={popoutWidgetIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
width="10"
height="10"
/> }
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <TintableSvgButton
src={showPictureSnapshotIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Picture')}
onClick={this._onSnapshotClick}
width="10"
height="10"
/> }
{ /* Edit widget */ }
{ showEditButton && <TintableSvgButton
@ -670,6 +689,8 @@ AppTile.propTypes = {
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the delete icon
showDelete: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Widget apabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
@ -686,6 +707,7 @@ AppTile.defaultProps = {
showTitle: true,
showMinimise: true,
showDelete: true,
showPopout: true,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
};

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Aidan Gauland
Copyright 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
@ -33,10 +32,26 @@ module.exports = React.createClass({
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// onClick handler for the cancel button.
onCancel: PropTypes.func.isRequired,
onCancel: PropTypes.func,
focus: PropTypes.bool,
disabled: PropTypes.bool,
},
getDefaultProps: function() {
return {
hasCancel: true,
disabled: false,
};
},
_onCancelClick: function() {
this.props.onCancel();
},
render: function() {
@ -44,18 +59,23 @@ module.exports = React.createClass({
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
let cancelButton;
if (this.props.hasCancel) {
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
{ _t("Cancel") }
</button>;
}
return (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
{ cancelButton }
</div>
);
},

View file

@ -24,6 +24,7 @@ import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
// 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:
@ -57,6 +58,8 @@ export default React.createClass({
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
this._onFlairStoreUpdated();
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
@ -80,6 +83,11 @@ export default React.createClass({
});
},
_refreshGroup(groupId) {
GroupStore.refreshGroupRooms(groupId);
GroupStore.refreshGroupMembers(groupId);
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
@ -89,6 +97,10 @@ export default React.createClass({
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
if (this.props.tag[0] === '+') {
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
onContextButtonClick: function(e) {

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import TintableSvg from './TintableSvg';
import AccessibleButton from './AccessibleButton';
export default class TintableSvgButton extends React.Component {
@ -39,9 +40,11 @@ export default class TintableSvgButton extends React.Component {
width={this.props.width}
height={this.props.height}
></TintableSvg>
<span
<AccessibleButton
onClick={this.props.onClick}
element='span'
title={this.props.title}
onClick={this.props.onClick} />
/>
</span>
);
}

View file

@ -23,7 +23,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
@ -47,33 +47,37 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore(this.props.groupId);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
onGroupStoreUpdated: function() {
if (this._unmounted) return;
this.setState({
isUserInvited: this._groupStore.getGroupInvitedMembers().some(
isUserInvited: GroupStore.getGroupInvitedMembers(this.props.groupId).some(
(m) => m.userId === this.props.groupMember.userId,
),
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
},

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -42,9 +42,12 @@ export default React.createClass({
this._initGroupStore(this.props.groupId);
},
componentWillUnmount: function() {
this._unmounted = true;
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
GroupStore.registerListener(groupId, () => {
this._fetchMembers();
});
},
@ -52,8 +55,8 @@ export default React.createClass({
_fetchMembers: function() {
if (this._unmounted) return;
this.setState({
members: this._groupStore.getGroupMembers(),
invitedMembers: this._groupStore.getGroupInvitedMembers(),
members: GroupStore.getGroupMembers(this.props.groupId),
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
});
},

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import { _t } from '../../../languageHandler.js';
@ -41,15 +40,18 @@ export default React.createClass({
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
this.setState({
isGroupPublicised: this._groupStore.getGroupPublicity(),
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: GroupStore.getGroupPublicity(groupId),
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
});
});
},
componentWillUnmount() {
if (this._groupStoreToken) this._groupStoreToken.unregister();
},
_onPublicityToggle: function(e) {
e.stopPropagation();
this.setState({
@ -57,7 +59,7 @@ export default React.createClass({
// Optimistic early update
isGroupPublicised: !this.state.isGroupPublicised,
});
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
this.setState({
busy: false,
});

View file

@ -21,7 +21,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
module.exports = React.createClass({
displayName: 'GroupRoomInfo',
@ -50,29 +50,26 @@ module.exports = React.createClass({
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
componentWillUnmount() {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
_updateGroupRoom() {
this.setState({
groupRoom: this._groupStore.getGroupRooms().find(
groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
(r) => r.roomId === this.props.groupRoomId,
),
});
@ -80,7 +77,7 @@ module.exports = React.createClass({
onGroupStoreUpdated: function() {
this.setState({
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
this._updateGroupRoom();
},
@ -100,7 +97,7 @@ module.exports = React.createClass({
this.setState({groupRoomRemoveLoading: true});
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
this._groupStore.removeRoomFromGroup(roomId).then(() => {
GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
dis.dispatch({
action: "view_group_room_list",
});
@ -134,7 +131,7 @@ module.exports = React.createClass({
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
const roomName = this.state.groupRoom.displayname;
this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_ROOMS = 30;
@ -39,22 +39,31 @@ export default React.createClass({
this._initGroupStore(this.props.groupId);
},
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore();
},
_unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._fetchRooms();
});
this._groupStore.on('error', (err) => {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
// XXX: This is also leaked - we should remove it when unmounting
GroupStore.on('error', (err, errorGroupId) => {
if (errorGroupId !== groupId) return;
this.setState({
rooms: null,
});
});
},
_fetchRooms: function() {
onGroupStoreUpdated: function() {
if (this._unmounted) return;
this.setState({
rooms: this._groupStore.getGroupRooms(),
rooms: GroupStore.getGroupRooms(this.props.groupId),
});
},

View file

@ -20,7 +20,7 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
export default class MAudioBody extends React.Component {
@ -54,7 +54,7 @@ export default class MAudioBody extends React.Component {
let decryptedBlob;
decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(decryptedBlob);
return URL.createObjectURL(decryptedBlob);
}).done((url) => {
this.setState({
decryptedUrl: url,
@ -69,6 +69,12 @@ export default class MAudioBody extends React.Component {
}
}
componentWillUnmount() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
}
render() {
const content = this.props.mxEvent.getContent();

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -99,16 +100,27 @@ Tinter.registerTintable(updateTintedDownloadImage);
// overridable so that people running their own version of the client can
// choose a different renderer.
//
// To that end the first version of the blob generation will be the following
// To that end the current version of the blob generation is the following
// html:
//
// <html><head><script>
// window.onmessage=function(e){eval("("+e.data.code+")")(e)}
// var params = window.location.search.substring(1).split('&');
// var lockOrigin;
// for (var i = 0; i < params.length; ++i) {
// var parts = params[i].split('=');
// if (parts[0] == 'origin') lockOrigin = decodeURIComponent(parts[1]);
// }
// window.onmessage=function(e){
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
// }
// </script></head><body></body></html>
//
// This waits to receive a message event sent using the window.postMessage API.
// When it receives the event it evals a javascript function in data.code and
// runs the function passing the event as an argument.
// runs the function passing the event as an argument. This version adds
// support for a query parameter controlling the origin from which messages
// will be processed as an extra layer of security (note that the default URL
// is still 'v1' since it is backwards compatible).
//
// In particular it means that the rendering function can be written as a
// ordinary javascript function which then is turned into a string using
@ -325,6 +337,7 @@ module.exports = React.createClass({
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
renderer_url = this.context.appConfig.cross_origin_renderer_url;
}
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
@ -348,7 +361,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
<a className="mx_MFileBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName }
</a>
<div className="mx_MImageBody_size">

View file

@ -25,7 +25,7 @@ import ImageUtils from '../../../ImageUtils';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -49,6 +49,8 @@ export default class extends React.Component {
super(props);
this.onAction = this.onAction.bind(this);
this.onImageError = this.onImageError.bind(this);
this.onImageLoad = this.onImageLoad.bind(this);
this.onImageEnter = this.onImageEnter.bind(this);
this.onImageLeave = this.onImageLeave.bind(this);
this.onClientSync = this.onClientSync.bind(this);
@ -70,6 +72,7 @@ export default class extends React.Component {
this.context.matrixClient.on('sync', this.onClientSync);
}
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
onClientSync(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
@ -136,6 +139,11 @@ export default class extends React.Component {
});
}
onImageLoad() {
this.fixupHeight();
this.props.onWidgetLoad();
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
@ -153,6 +161,13 @@ export default class extends React.Component {
return this.state.decryptedThumbnailUrl;
}
return this.state.decryptedUrl;
} else if (content.info.mimetype == "image/svg+xml" && content.info.thumbnail_url) {
// special case to return client-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent
// billion lol attacks and similar
return this.context.matrixClient.mxcUrlToHttp(
content.info.thumbnail_url, 800, 600,
);
} else {
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
}
@ -160,7 +175,6 @@ export default class extends React.Component {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.fixupHeight();
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
@ -168,21 +182,20 @@ export default class extends React.Component {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
});
}
let decryptedBlob;
thumbnailPromise.then((thumbnailUrl) => {
return decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
}).then((contentUrl) => {
this.setState({
decryptedUrl: contentUrl,
decryptedThumbnailUrl: thumbnailUrl,
decryptedBlob: decryptedBlob,
});
this.props.onWidgetLoad();
});
}).catch((err) => {
console.warn("Unable to decrypt attachment: ", err);
@ -205,6 +218,13 @@ export default class extends React.Component {
dis.unregister(this.dispatcherRef);
this.context.matrixClient.removeListener('sync', this.onClientSync);
this._afterComponentWillUnmount();
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
if (this.state.decryptedThumbnailUrl) {
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
}
}
// To be overridden by subclasses (e.g. MStickerBody) for further
@ -229,7 +249,16 @@ export default class extends React.Component {
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
// which may well be much smaller than the 800x600 bounding box.
// FIXME: It will also break really badly for images with broken or missing thumbnails
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
// us, we can't even really layout the page nicely for it. Instead we have to assume
// it'll target 800x600 and we'll downsize if needed to make things fit.
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
let thumbHeight = null;
if (content.info) {
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
@ -239,18 +268,22 @@ export default class extends React.Component {
}
_messageContent(contentUrl, thumbUrl, content) {
const thumbnail = (
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
);
return (
<span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
</span>
);
}
@ -285,14 +318,6 @@ export default class extends React.Component {
);
}
if (this.state.imgError) {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
const contentUrl = this._getContentUrl();
let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
@ -301,20 +326,6 @@ export default class extends React.Component {
thumbUrl = this._getThumbUrl();
}
if (thumbUrl) {
return this._messageContent(contentUrl, thumbUrl, content);
} else if (content.body) {
return (
<span className="mx_MImageBody">
{ _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) }
</span>
);
} else {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
return this._messageContent(contentUrl, thumbUrl, content);
}
}

View file

@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -94,14 +94,14 @@ module.exports = React.createClass({
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
});
}
let decryptedBlob;
thumbnailPromise.then((thumbnailUrl) => {
return decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
}).then((contentUrl) => {
this.setState({
decryptedUrl: contentUrl,
@ -120,6 +120,15 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
if (this.state.decryptedThumbnailUrl) {
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
}
},
render: function() {
const content = this.props.mxEvent.getContent();

View file

@ -33,6 +33,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
import {makeEventPermalink} from "../../../matrix-to";
import SettingsStore from "../../../settings/SettingsStore";
const ObjectUtils = require('../../../ObjectUtils');
@ -759,7 +760,11 @@ function E2ePadlockUnencrypted(props) {
}
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
} else {
return <img className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" {...props} />;
}
}
module.exports.getHandlerTile = getHandlerTile;

View file

@ -30,7 +30,7 @@ import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt');
import TagOrderStore from '../../../stores/TagOrderStore';
import RoomListStore from '../../../stores/RoomListStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -83,8 +83,6 @@ module.exports = React.createClass({
cli.on("Group.myMembership", this._onGroupMyMembership);
const dmRoomMap = DMRoomMap.shared();
this._groupStores = {};
this._groupStoreTokens = [];
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
@ -93,22 +91,22 @@ module.exports = React.createClass({
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getRooms();
// When the selected tags are changed, initialise a group store if necessary
this._tagStoreToken = TagOrderStore.addListener(() => {
// Listen to updates to group data. RoomList cares about members and rooms in order
// to filter the room list when group tags are selected.
this._groupStoreToken = GroupStore.registerListener(null, () => {
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
if (tag[0] !== '+') {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
this._groupStoreTokens.push(
this._groupStores[tag].registerListener(() => {
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
}),
);
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
});
// Filters themselves have changed, refresh the selected tags
});
this._tagStoreToken = TagOrderStore.addListener(() => {
// Filters themselves have changed
this.updateVisibleRooms();
});
@ -183,9 +181,9 @@ module.exports = React.createClass({
this._roomListStoreToken.remove();
}
if (this._groupStoreTokens.length > 0) {
// NB: GroupStore is not a Flux.Store
this._groupStoreTokens.forEach((token) => token.unregister());
// NB: GroupStore is not a Flux.Store
if (this._groupStoreToken) {
this._groupStoreToken.unregister();
}
// cancel any pending calls to the rate_limited_funcs
@ -259,12 +257,11 @@ module.exports = React.createClass({
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
const store = this._groupStores[tag];
if (!store) return;
if (tag[0] !== '+') return;
this._visibleRoomsForGroup[tag] = [];
store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
store.getGroupMembers().forEach((member) => {
GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
GroupStore.getGroupMembers(tag).forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),

View file

@ -174,6 +174,7 @@ export default class Stickerpicker extends React.Component {
showTitle={false}
showMinimise={true}
showDelete={false}
showPopout={false}
onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true}
whitelistCapabilities={['m.sticker']}

View file

@ -950,7 +950,6 @@
"Import room keys": "Импортиране на ключове за стая",
"File to import": "Файл за импортиране",
"Import": "Импортирай",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Също така записваме всяка страница, която използвате в приложението (в момента <CurrentPageHash>), браузъра, който използвате (<CurrentUserAgent>) и резолюцията на устройството (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Когато тази страница съдържа информация идентифицираща Вас (като например стая, потребител или идентификатор на група), тези данни биват премахнати преди да бъдат изпратени до сървъра.",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Има непознати устройства в тази стая. Ако продължите без да ги потвърдите, ще бъде възможно за някого да подслушва Вашия разговор.",
"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\". Това може да означава, че Вашата комуникация е прихваната!",
@ -987,7 +986,7 @@
"%(user)s is a %(userRole)s": "%(user)s е %(userRole)s",
"Code": "Код",
"Debug Logs Submission": "Изпращане на логове за дебъгване",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ако сте изпратили грешка чрез GitHub, логовете за дебъгване могат да ни помогнат да проследим проблема. Логовете за дебъгване съдържат данни за използване на приложението, включващи потребителското Ви име, идентификаторите или псевдонимите на стаите или групите, които сте посетили, и потребителските имена на други потребители. Те не съдържат съобщения.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ако сте изпратили грешка чрез GitHub, логовете за дебъгване могат да ни помогнат да проследим проблема. Логовете за дебъгване съдържат данни за използване на приложението, включващи потребителското Ви име, идентификаторите или псевдонимите на стаите или групите, които сте посетили, и потребителските имена на други потребители. Те не съдържат съобщения.",
"Submit debug logs": "Изпрати логове за дебъгване",
"Opens the Developer Tools dialog": "Отваря прозорец с инструменти на разработчика",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Видяно от %(displayName)s (%(userName)s) в %(dateTime)s",
@ -1157,5 +1156,7 @@
"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": "Съществуват разширени настройки за известия, които не са показани тук"
"There are advanced notifications which are not shown here": "Съществуват разширени настройки за известия, които не са показани тук",
"Missing roomId.": "Липсва идентификатор на стая.",
"Picture": "Изображение"
}

View file

@ -952,7 +952,6 @@
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Nachricht jetzt erneut senden</resendText> oder <cancelText>senden abbrechen</cancelText> now.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatsphäre ist uns wichtig, deshalb sammeln wir keine persönlichen oder identifizierbaren Daten für unsere Analysen.",
"The information being sent to us to help make Riot.im better includes:": "Die Informationen, die an uns gesendet werden um Riot.im zu verbessern enthalten:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Wir speichern auch jede Seite, die du in der App benutzt (currently <CurrentPageHash>), deinen User Agent (<CurrentUserAgent>) und die Bildschirmauflösung deines Gerätes (<CurrentDeviceResolution>).",
"The platform you're on": "Benutzte Plattform",
"The version of Riot.im": "Riot.im Version",
"Your language of choice": "Deine ausgewählte Sprache",
@ -986,7 +985,7 @@
"<requestLink>Re-request encryption keys</requestLink> from your other devices.": "Verschlüsselungs-Schlüssel von deinen anderen Geräten <requestLink>erneut anfragen</requestLink>.",
"%(user)s is a %(userRole)s": "%(user)s ist ein %(userRole)s",
"Debug Logs Submission": "Einsenden des Fehlerprotokolls",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
"Submit debug logs": "Fehlerberichte einreichen",
"Code": "Code",
"Opens the Developer Tools dialog": "Öffnet den Entwicklerwerkzeugkasten",
@ -1157,5 +1156,7 @@
"Collapse panel": "Panel einklappen",
"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!": "In deinem aktuell verwendeten Browser können Aussehen und Handhabung der Anwendung unter Umständen noch komplett fehlerhaft sein, so dass einige bzw. im Extremfall alle Funktionen nicht zur Verfügung stehen. Du kannst es trotzdem versuchen und fortfahren, bist dabei aber bezüglich aller auftretenden Probleme auf dich allein gestellt!",
"Checking for an update...": "Nach Updates suchen...",
"There are advanced notifications which are not shown here": "Es existieren erweiterte Benachrichtigungen, welche hier nicht angezeigt werden"
"There are advanced notifications which are not shown here": "Es existieren erweiterte Benachrichtigungen, welche hier nicht angezeigt werden",
"Missing roomId.": "Fehlende Raum-ID.",
"Picture": "Bild"
}

View file

@ -10,9 +10,12 @@
"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",
"Your homeserver's URL": "Your homeserver's URL",
"Your identity server's URL": "Your identity server's URL",
"Every page you use in the app": "Every page you use in the app",
"e.g. <CurrentPageURL>": "e.g. <CurrentPageURL>",
"Your User Agent": "Your User Agent",
"Your device resolution": "Your device resolution",
"Analytics": "Analytics",
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
"Call Failed": "Call Failed",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
@ -198,6 +201,7 @@
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
"Always show message timestamps": "Always show message timestamps",
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
"Always show encryption icons": "Always show encryption icons",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
"Disable big emoji in chat": "Disable big emoji in chat",
@ -654,6 +658,7 @@
"Delete widget": "Delete widget",
"Revoke widget access": "Revoke widget access",
"Minimize apps": "Minimize apps",
"Popout widget": "Popout widget",
"Picture": "Picture",
"Edit": "Edit",
"Create new room": "Create new room",
@ -745,7 +750,7 @@
"Failed to send logs: ": "Failed to send logs: ",
"Submit debug logs": "Submit debug logs",
"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 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.",
"<a>Click here</a> to create a GitHub issue.": "<a>Click here</a> to create a GitHub issue.",
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.": "Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
"GitHub issue link:": "GitHub issue link:",
"Notes:": "Notes:",
"Send logs": "Send logs",
@ -807,11 +812,15 @@
"Ignore request": "Ignore request",
"Loading device info...": "Loading device info...",
"Encryption key request": "Encryption key request",
"Otherwise, <a>click here</a> to send a bug report.": "Otherwise, <a>click here</a> to send a bug report.",
"Sign out": "Sign out",
"Log out and remove encryption keys?": "Log out and remove encryption keys?",
"Send Logs": "Send Logs",
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
"Refresh": "Refresh",
"Unable to restore session": "Unable to restore session",
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.",
"We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.",
"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.": "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.",
"Continue anyway": "Continue anyway",
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
"Invalid Email Address": "Invalid Email Address",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"Verification Pending": "Verification Pending",
@ -1016,7 +1025,6 @@
"Status.im theme": "Status.im theme",
"Can't load user settings": "Can't load user settings",
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
"Sign out": "Sign out",
"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.",
"Success": "Success",
"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",
@ -1034,7 +1042,7 @@
"Device key:": "Device key:",
"Ignored Users": "Ignored Users",
"Debug Logs Submission": "Debug Logs Submission",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
"Learn more about how we use analytics.": "Learn more about how we use analytics.",

View file

@ -952,7 +952,6 @@
"The platform you're on": "Via sistemtipo",
"Which officially provided instance you are using, if any": "Kiun oficiale disponeblan aperon vi uzas, se iun ajn",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Ĉu vi uzas la riĉtekstan reĝimon de la riĉteksta redaktilo aŭ ne",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Ni ankaŭ registras ĉiun paĝon, kiun vi uzas en la programo (nun <CurrentPageHash>), vian klientan aplikaĵon (<CurrentUserAgent>) kaj vian aparatan distingon (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kiam ĉi tiu paĝo enhavas identigeblajn informojn, ekzemple ĉambron, uzantan aŭ grupan identigilon, ĝi sendiĝas al la servilo sen tiuj.",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
"Disable Community Filter Panel": "Malŝalti komunuman filtran breton",

View file

@ -959,7 +959,6 @@
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Pribatutasuna garrantzitsua da guretzat, beraz ez dugu datu pertsonalik edo identifikagarririk jasotzen gure estatistiketan.",
"Learn more about how we use analytics.": "Ikasi gehiago estatistikei ematen diegun erabileraz.",
"The information being sent to us to help make Riot.im better includes:": "Riot.im hobetzeko bidaltzen zaigun informazioan hau dago:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Aplikazioan erabiltzen duzun orri bakoitza jasotzen dugu (orain <CurrentPageHash>), erabiltzaile-agentea (<CurrentUserAgent>) eta gailuaren bereizmena (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Orri honek informazio identifikagarria badu ere, esaterako gela, erabiltzailea edo talde ID-a, datu hauek ezabatu egiten dira zerbitzarira bidali aurretik.",
"Whether or not you're logged in (we don't record your user name)": "Saioa hasita dagoen ala ez (ez dugu erabiltzaile-izena gordetzen)",
"Which officially provided instance you are using, if any": "Erabiltzen ari zaren instantzia ofiziala, balego",
@ -987,7 +986,7 @@
"%(user)s is a %(userRole)s": "%(user)s %(userRole)s da",
"Code": "Kodea",
"Debug Logs Submission": "Arazte-egunkarien bidalketak",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Akats bat bidali baduzu GitHub bidez, arazte-egunkariek arazoa aurkitzen lagundu gaitzakete. Arazte-egunkariek aplikazioak darabilen datuak dauzkate, zure erabiltzaile izena barne, bisitatu dituzun gelen ID-ak edo ezizenak eta beste erabiltzaileen izenak. Ez dute mezurik.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Akats bat bidali baduzu GitHub bidez, arazte-egunkariek arazoa aurkitzen lagundu gaitzakete. Arazte-egunkariek aplikazioak darabilen datuak dauzkate, zure erabiltzaile izena barne, bisitatu dituzun gelen ID-ak edo ezizenak eta beste erabiltzaileen izenak. Ez dute mezurik.",
"Submit debug logs": "Bidali arazte-txostenak",
"Opens the Developer Tools dialog": "Garatzailearen tresnen elkarrizketa-koadroa irekitzen du",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s)(e)k ikusita %(dateTime)s(e)tan",
@ -1157,5 +1156,7 @@
"Collapse panel": "Tolestu panela",
"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!": "Zure oraingo nabigatzailearekin aplikazioaren itxura eta portaera guztiz okerra izan daiteke, eta funtzio batzuk ez dira ibiliko. Hala ere aurrera jarraitu dezakezu saiatu nahi baduzu, baina zure erantzukizunaren menpe geratzen dira aurkitu ditzakezun arazoak!",
"Checking for an update...": "Eguneraketarik dagoen egiaztatzen...",
"There are advanced notifications which are not shown here": "Hemen erakusten ez diren jakinarazpen aurreratuak daude"
"There are advanced notifications which are not shown here": "Hemen erakusten ez diren jakinarazpen aurreratuak daude",
"Missing roomId.": "Gelaren ID-a falta da.",
"Picture": "Irudia"
}

View file

@ -956,7 +956,6 @@
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Le respect de votre vie privée est important pour nous, donc nous ne collectons aucune donnée personnelle ou permettant de vous identifier pour nos statistiques.",
"Learn more about how we use analytics.": "En savoir plus sur notre utilisation des statistiques.",
"The information being sent to us to help make Riot.im better includes:": "Les informations qui nous sont envoyées pour nous aider à améliorer Riot.im comprennent :",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Nous enregistrons aussi chaque page que vous utilisez dans l'application (en ce moment <CurrentPageHash>), votre User Agent (<CurrentUserAgent>) et la résolution de votre appareil (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon, un identifiant d'utilisateur ou de groupe, ces données sont enlevées avant qu'elle ne soit envoyée au serveur.",
"The platform you're on": "La plateforme que vous utilisez",
"The version of Riot.im": "La version de Riot.im",
@ -988,7 +987,7 @@
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Vu par %(displayName)s (%(userName)s) à %(dateTime)s",
"Code": "Code",
"Debug Logs Submission": "Envoi des journaux de débogage",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
"Submit debug logs": "Envoyer les journaux de débogage",
"Opens the Developer Tools dialog": "Ouvre la fenêtre des Outils de développeur",
"Unable to join community": "Impossible de rejoindre la communauté",
@ -1156,5 +1155,8 @@
"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.": "Les rapports de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.",
"Failed to send logs: ": "Échec lors de l'envoi des rapports : ",
"Notes:": "Notes :",
"Preparing to send logs": "Préparation d'envoi des rapports"
"Preparing to send logs": "Préparation d'envoi des rapports",
"Missing roomId.": "Identifiant de salon manquant.",
"Picture": "Image",
"<a>Click here</a> to create a GitHub issue.": "<a>Cliquez ici</a> pour créer un signalement sur GitHub."
}

View file

@ -951,7 +951,6 @@
"File to import": "Ficheiro a importar",
"Import": "Importar",
"The information being sent to us to help make Riot.im better includes:": "A información enviada a Riot.im para axudarnos a mellorar inclúe:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Tamén rexistramos cada páxina que vostede utiliza no aplicativo (actualmente <CurrentPageHash>), o User Agent (<CurrentUserAgent>) e a resolución do dispositivo (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si esta páxina inclúe información identificable como ID de grupo, usuario ou sala, estes datos son eliminados antes de ser enviados ao servidor.",
"The platform you're on": "A plataforma na que está",
"The version of Riot.im": "A versión de Riot.im",
@ -993,7 +992,7 @@
"Join this community": "Únase a esta comunidade",
"Leave this community": "Deixar esta comunidade",
"Debug Logs Submission": "Envío de rexistro de depuración",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Si enviou un reporte de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso do aplicativo incluíndo o seu nome de usuaria, os IDs ou alcumes das salas e grupos que visitou e os nomes de usuaria de outras personas. Non conteñen mensaxes.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si enviou un reporte de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso do aplicativo incluíndo o seu nome de usuaria, os IDs ou alcumes das salas e grupos que visitou e os nomes de usuaria de outras personas. Non conteñen mensaxes.",
"Submit debug logs": "Enviar informes de depuración",
"Opens the Developer Tools dialog": "Abre o cadro de Ferramentas de Desenvolvedoras",
"Stickerpack": "Peganitas",

View file

@ -362,7 +362,7 @@
"This email address was not found": "Az e-mail cím nem található",
"The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.",
"Press <StartChatButton> to start a chat with someone": "Nyomd meg a <StartChatButton> gombot ha szeretnél csevegni valakivel",
"Privacy warning": "Magánéleti figyelmeztetés",
"Privacy warning": "Adatvédelmi figyelmeztetés",
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "'%(fileName)s' fájl túllépte a Saját szerverben beállított feltöltési méret határt",
"The file '%(fileName)s' failed to upload": "'%(fileName)s' fájl feltöltése sikertelen",
"The remote side failed to pick up": "A hívott fél nem vette fel",
@ -956,7 +956,6 @@
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "A személyes adatok védelme fontos számunkra, így mi nem gyűjtünk személyes és személyhez köthető adatokat az analitikánkhoz.",
"Learn more about how we use analytics.": "Tudj meg többet arról hogyan használjuk az analitikai adatokat.",
"The information being sent to us to help make Riot.im better includes:": "Az adatok amiket a Riot.im javításához felhasználunk az alábbiak:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Felvesszük az összes oldalt amit az alkalmazásban használsz (jelenleg <CurrentPageHash>), a \"User Agent\"-et (<CurrentUserAgent>) és az eszköz felbontását (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Minden azonosításra alkalmas adatot mint a szoba, felhasználó vagy csoport azonosítót mielőtt az adatokat elküldenénk eltávolításra kerülnek.",
"The platform you're on": "A platform amit használsz",
"The version of Riot.im": "Riot.im verziója",
@ -987,7 +986,7 @@
"%(user)s is a %(userRole)s": "%(user)s egy %(userRole)s",
"Code": "Kód",
"Debug Logs Submission": "Hibakeresési napló elküldése",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ha a GitHubon keresztül küldted be a hibát, a hibakeresési napló segíthet nekünk a javításban. A napló felhasználási adatokat tartalmaz mint a felhasználói neved, az általad meglátogatott szobák vagy csoportok azonosítóját vagy alternatív nevét és mások felhasználói nevét. De nem tartalmazzák az üzeneteket.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ha a GitHubon keresztül küldted be a hibát, a hibakeresési napló segíthet nekünk a javításban. A napló felhasználási adatokat tartalmaz mint a felhasználói neved, az általad meglátogatott szobák vagy csoportok azonosítóját vagy alternatív nevét és mások felhasználói nevét. De nem tartalmazzák az üzeneteket.",
"Submit debug logs": "Hibakeresési napló küldése",
"Opens the Developer Tools dialog": "Megnyitja a fejlesztői eszközök ablakát",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) az alábbi időpontban látta: %(dateTime)s",
@ -1157,5 +1156,7 @@
"Collapse panel": "Panel becsukása",
"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!": "Ebben a böngészőben az alkalmazás felülete tele lehet hibával, és az is lehet, hogy egyáltalán nem működik. Ha így is ki szeretnéd próbálni, megteheted, de ha valami gondod van, nem tudunk segíteni!",
"Checking for an update...": "Frissítés keresése...",
"There are advanced notifications which are not shown here": "Vannak itt nem látható, haladó értesítések"
"There are advanced notifications which are not shown here": "Vannak itt nem látható, haladó értesítések",
"Missing roomId.": "Hiányzó szoba azonosító.",
"Picture": "Kép"
}

View file

@ -101,7 +101,6 @@
"Your identity server's URL": "L'URL del tuo server di identità",
"Analytics": "Statistiche",
"The information being sent to us to help make Riot.im better includes:": "Le informazioni inviate per aiutarci a migliorare Riot.im includono:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Registriamo anche ogni pagina che usi nell'app (attualmente <CurrentPageHash>), il tuo User Agent (<CurrentUserAgent>) e la risoluzione del dispositivo (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Se questa pagina include informazioni identificabili, come una stanza, utente o ID di gruppo, questi dati sono rimossi prima che vengano inviati al server.",
"Call Failed": "Chiamata fallita",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Ci sono dispositivi sconosciuti in questa stanza: se procedi senza verificarli, qualcuno avrà la possibilità di intercettare la tua chiamata.",
@ -865,7 +864,7 @@
"Device key:": "Chiave dispositivo:",
"Ignored Users": "Utenti ignorati",
"Debug Logs Submission": "Invio dei log di debug",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Se hai segnalato un errore via Github, i log di debug possono aiutarci a identificare il problema. I log di debug contengono dati di utilizzo dell'applicazione inclusi il nome utente, gli ID o alias delle stanze o gruppi visitati e i nomi degli altri utenti. Non contengono messaggi.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se hai segnalato un errore via Github, i log di debug possono aiutarci a identificare il problema. I log di debug contengono dati di utilizzo dell'applicazione inclusi il nome utente, gli ID o alias delle stanze o gruppi visitati e i nomi degli altri utenti. Non contengono messaggi.",
"Submit debug logs": "Invia log di debug",
"Riot collects anonymous analytics to allow us to improve the application.": "Riot raccoglie statistiche anonime per permetterci di migliorare l'applicazione.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Diamo importanza alla privacy, perciò non raccogliamo dati personali o identificabili per le nostre statistiche.",

View file

@ -87,7 +87,6 @@
"Your identity server's URL": "あなたのアイデンティティサーバのURL",
"Analytics": "分析",
"The information being sent to us to help make Riot.im better includes:": "Riot.imをよりよくするために私達に送信される情報は以下を含みます:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "私達はこのアプリであなたが利用したページ(現在は<CurrentPageHash>)、あなたのユーザエージェント(現在は<CurrentUserAgent>)、並びにあなたの端末の解像度(現在の端末では<CurrentDeviceResolution>)も記録します。",
"Thursday": "木曜日",
"Messages in one-to-one chats": "一対一のチャットでのメッセージ",
"A new version of Riot is available.": "新しいバージョンのRiotが利用可能です。",
@ -242,5 +241,7 @@
"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": "ここに表示されない詳細な通知があります"
"There are advanced notifications which are not shown here": "ここに表示されない詳細な通知があります",
"Call": "通話",
"Answer": "応答"
}

View file

@ -12,7 +12,6 @@
"Your identity server's URL": "Jūsų identifikavimo serverio URL adresas",
"Analytics": "Statistika",
"The information being sent to us to help make Riot.im better includes:": "Informacijoje, kuri yra siunčiama Riot.im tobulinimui yra:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Mes taip pat saugome kiekvieną puslapį, kurį jūs naudojate programėlėje (dabartinis <CurrentPageHash>), jūsų paskyros agentas (<CurrentUserAgent>) ir jūsų įrenginio rezoliucija (<CurrentDeviceResolution>).",
"Fetching third party location failed": "Nepavyko gauti trečios šalies vietos",
"A new version of Riot is available.": "Yra nauja Riot versija.",
"I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti",

View file

@ -695,7 +695,6 @@
"Your homeserver's URL": "Bāzes servera URL adrese",
"Your identity server's URL": "Tava Identitātes servera URL adrese",
"The information being sent to us to help make Riot.im better includes:": "Informācija, kura mums tiek nosūtīta, lai ļautu padarīt Riot.im labāku, ietver:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Mēs arī fiksējam katru lapu, kuru tu izmanto programmā (currently <CurrentPageHash>), Tavu lietotāja aģentu (<CurrentUserAgent>) un Tavas ierīces ekrāna izšķirtspēju (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ja šī lapa ietver identificējamu informāciju, tādu kā istaba, lietotājs, grupas Id, šie dati tiek noņemti pirms nosūtīšanas uz serveri.",
"Call Failed": "Zvans neizdevās",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šajā istabā ir nepazīstamas ierīces: ja Tu turpināsi bez to pārbaudes, ir iespējams, ka kāda nepiederoša persona var noklausīties Tavas sarunas.",
@ -987,7 +986,7 @@
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "Tagad<resendText>visas atkārtoti sūtīt</resendText> vai <cancelText>visas atcelt</cancelText>. Tu vari atzīmēt arī individuālas ziņas, kuras atkārtoti sūtīt vai atcelt.",
"Clear filter": "Attīrīt filtru",
"Debug Logs Submission": "Iesniegt atutošanas logfailus",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ja esi paziņojis par kļūdu caur GitHub, atutošanas logfaili var mums palīdzēt identificēt problēmu. Atutošanas logfaili satur programmas lietošanas datus, tostarp Tavu lietotājvārdu, istabu/grupu Id vai aliases, kuras esi apmeklējis un citu lietotāju lietotājvārdus. Tie nesatur pašas ziņas.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ja esi paziņojis par kļūdu caur GitHub, atutošanas logfaili var mums palīdzēt identificēt problēmu. Atutošanas logfaili satur programmas lietošanas datus, tostarp Tavu lietotājvārdu, istabu/grupu Id vai aliases, kuras esi apmeklējis un citu lietotāju lietotājvārdus. Tie nesatur pašas ziņas.",
"Submit debug logs": "Iesniegt atutošanas logfailus",
"Opens the Developer Tools dialog": "Atver Izstrādātāja instrumentus",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Skatījis %(displayName)s (%(userName)s) %(dateTime)s",

View file

@ -959,7 +959,6 @@
"Notify the whole room": "Notificeer de gehele ruimte",
"Room Notification": "Ruimte Notificatie",
"The information being sent to us to help make Riot.im better includes:": "De informatie dat naar ons wordt verstuurd om Riot.im beter te maken betrekt:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "We nemen ook elke pagina die je in de applicatie gebruikt (momenteel <CurrentPageHash>), je User Agent (<CurrentUserAgent>) en de resolutie van je apparaat (<CurrentDeviceResolution>) op.",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een ruimte, gebruiker of groep ID, zal deze data verwijderd worden voordat het naar de server gestuurd wordt.",
"The platform you're on": "Het platform waar je je op bevindt",
"The version of Riot.im": "De versie van Riot.im",
@ -1002,7 +1001,7 @@
"Everyone": "Iedereen",
"Leave this community": "Deze gemeenschap verlaten",
"Debug Logs Submission": "Debug Logs Indienen",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Als je een bug via Github hebt ingediend kunnen debug logs ons helpen om het probleem te vinden. Debug logs bevatten applicatie-gebruik data inclusief je gebruikersnaam, de ID's of namen van de ruimtes en groepen die je hebt bezocht en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Als je een bug via Github hebt ingediend kunnen debug logs ons helpen om het probleem te vinden. Debug logs bevatten applicatie-gebruik data inclusief je gebruikersnaam, de ID's of namen van de ruimtes en groepen die je hebt bezocht en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.",
"Submit debug logs": "Debug logs indienen",
"Opens the Developer Tools dialog": "Opent het Ontwikkelaars Gereedschappen dialoog",
"Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt",

View file

@ -693,7 +693,6 @@
"Your homeserver's URL": "Adres URL twojego serwera domowego",
"Your identity server's URL": "Adres URL twojego serwera tożsamości",
"The information being sent to us to help make Riot.im better includes:": "Oto informacje przesyłane do nas, służące do poprawy Riot.im:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Zapisujemy również każdą stronę, z której korzystasz w aplikacji (obecnie <CurrentPageHash>), twój User Agent (<CurrentUserAgent>) oraz rozdzielczość ekranu twojego urządzenia (<CurrentDeviceResolution>).",
"The platform you're on": "Platforma na której jesteś",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "W tym pokoju są nieznane urządzenia: jeżeli będziesz kontynuować bez ich weryfikacji, możliwe będzie podsłuchiwanie Twojego połączenia.",
"Answer": "Odbierz",

View file

@ -664,7 +664,6 @@
"Your homeserver's URL": "A URL do seu Servidor de Base (homeserver)",
"Your identity server's URL": "A URL do seu servidor de identidade",
"The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo usadas para ajudar a melhorar o Riot.im incluem:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Nós também gravamos cada página que você usa no app (atualmente <CurrentPageHash>), o seu User Agent (<CurrentUserAgent>) e a resolução do seu dispositivo (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página tem informação de identificação, como uma sala, ID de usuária/o ou de grupo, estes dados são removidos antes de serem enviados ao servidor.",
"Call Failed": "A chamada falhou",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se você continuar sem verificá-los, será possível alguém espiar sua chamada.",

File diff suppressed because it is too large Load diff

View file

@ -957,7 +957,6 @@
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Vaše súkromie je pre nás dôležité, preto nezhromažďujeme žiadne osobné údaje alebo údaje, na základe ktorých je možné vás identifikovať.",
"Learn more about how we use analytics.": "Zistite viac o tom, ako spracúvame analytické údaje.",
"The information being sent to us to help make Riot.im better includes:": "S cieľom vylepšovať aplikáciu Riot.im zbierame nasledujúce údaje:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Zaznamenávame tiež každú stránku aplikácie Riot.im, ktorú otvoríte (momentálne <CurrentPageHash>), reťazec user agent (<CurrentUserAgent>) a rozlíšenie obrazovky vašeho zariadenia (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ak sa na stránke vyskytujú identifikujúce údaje, akými sú napríklad názov miestnosti, ID používateľa, miestnosti alebo skupiny, tieto sú pred odoslaním na server odstránené.",
"The platform you're on": "Vami používaná platforma",
"The version of Riot.im": "Verzia Riot.im",
@ -993,7 +992,7 @@
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Ak si chcete nastaviť filter, pretiahnite obrázok komunity na panel filtrovania úplne na ľavej strane obrazovky. Potom môžete kedykoľvek kliknúť na obrázok komunity na tomto panely a Riot.im vám bude zobrazovať len miestnosti a ľudí z komunity, na ktorej obrázok ste klikli.",
"Clear filter": "Zrušiť filter",
"Debug Logs Submission": "Odoslanie ladiacich záznamov",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ak ste nám poslali hlásenie o chybe cez Github, ladiace záznamy nám môžu pomôcť lepšie identifikovať chybu. Ladiace záznamy obsahujú údaje o používaní aplikácii, vrátane vašeho používateľského mena, názvy a aliasy miestností a komunít, ku ktorým ste sa pripojili a mená ostatných používateľov. Tieto záznamy neobsahujú samotný obsah vašich správ.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ak ste nám poslali hlásenie o chybe cez Github, ladiace záznamy nám môžu pomôcť lepšie identifikovať chybu. Ladiace záznamy obsahujú údaje o používaní aplikácii, vrátane vašeho používateľského mena, názvy a aliasy miestností a komunít, ku ktorým ste sa pripojili a mená ostatných používateľov. Tieto záznamy neobsahujú samotný obsah vašich správ.",
"Submit debug logs": "Odoslať ladiace záznamy",
"Opens the Developer Tools dialog": "Otvorí dialóg nástroje pre vývojárov",
"Stickerpack": "Balíček nálepiek",

View file

@ -12,7 +12,6 @@
"Your identity server's URL": "URL-ja e server-it identiteti tëndë",
"Analytics": "Analiza",
"The information being sent to us to help make Riot.im better includes:": "Informacionet që dërgohen për t'i ndihmuar Riot.im-it të përmirësohet përmbajnë:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Ne gjithashtu inçizojmë çdo faqe që përdorë në aplikacion (në këtë çast <CurrentPageHash>), agjentin e përdoruesit tëndë (<CurrentUserAgent>) dhe rezolucionin e pajisjes tënde (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se ti dërgohën një server-it.",
"Call Failed": "Thirrja nuk mundej të realizohet",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.",

View file

@ -773,7 +773,6 @@
"Your identity server's URL": "Адреса вашег идентитеског сервера",
"Analytics": "Аналитика",
"The information being sent to us to help make Riot.im better includes:": "У податке које нам шаљете зарад побољшавања Riot.im-а спадају:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Такође бележимо сваку страницу коју користите у апликацији (тренутно <CurrentPageHash>), ваш кориснички агент (<CurrentUserAgent>) и резолуцију вашег уређаја (<CurrentDeviceResolution>).",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ако страница садржи поверљиве податке (као што је назив собе, корисника или ИБ-ја групе), ти подаци се уклањају пре слања на сервер.",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "Корисник %(oldDisplayName)s је променио приказно име у %(displayName)s.",
"Failed to set direct chat tag": "Нисам успео да поставим ознаку директног ћаскања",

View file

@ -10,7 +10,7 @@
"Decryption error": "解密出错",
"Delete": "删除",
"Default": "默认",
"Device ID": "设备识别码",
"Device ID": "设备 ID",
"Devices": "设备列表",
"Devices will not yet be able to decrypt history from before they joined the room": "新加入聊天室的设备不能解密加入之前的聊天记录",
"Direct chats": "私聊",
@ -20,8 +20,8 @@
"Don't send typing notifications": "不要发送我的打字状态",
"Download %(text)s": "下载 %(text)s",
"Email": "电子邮箱",
"Email address": "电子邮箱地址",
"Email, name or matrix ID": "电子邮箱姓名或者matrix ID",
"Email address": "邮箱地址",
"Email, name or matrix ID": "邮箱地址名称或者Matrix ID",
"Emoji": "表情",
"Enable encryption": "启用加密",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "不支持加密的客户端将看不到加密的消息",
@ -29,7 +29,7 @@
"%(senderName)s ended the call.": "%(senderName)s 结束了通话。.",
"End-to-end encryption information": "端到端加密信息",
"End-to-end encryption is in beta and may not be reliable": "端到端加密现为 beta 版,不一定可靠",
"Enter Code": "输入码",
"Enter Code": "输入验证码",
"Error": "错误",
"Error decrypting attachment": "解密附件时出错",
"Event information": "事件信息",
@ -39,8 +39,8 @@
"Failed to change password. Is your password correct?": "修改密码失败。确认原密码输入正确吗?",
"Failed to forget room %(errCode)s": "忘记聊天室失败,错误代码: %(errCode)s",
"Failed to join room": "无法加入聊天室",
"Failed to kick": "踢人失败",
"Failed to leave room": "无法离开聊天室",
"Failed to kick": "移除失败",
"Failed to leave room": "无法退出聊天室",
"Failed to load timeline position": "无法加载时间轴位置",
"Failed to lookup current room": "找不到当前聊天室",
"Failed to mute user": "禁言用户失败",
@ -48,7 +48,7 @@
"Failed to reject invitation": "拒绝邀请失败",
"Failed to save settings": "保存设置失败",
"Failed to send email": "发送邮件失败",
"Failed to send request.": "发送请求失败。",
"Failed to send request.": "请求发送失败。",
"Failed to set avatar.": "设置头像失败。.",
"Failed to set display name": "设置昵称失败",
"Failed to set up conference call": "无法启动群组通话",
@ -104,7 +104,7 @@
"Server may be unavailable or overloaded": "服务器可能不可用或者超载",
"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 may be unavailable, overloaded, or you hit a bug.": "服务器可能不可用、超载,或者你遇到了一个 bug。",
"Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.",
"Session ID": "会话 ID",
"%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。.",
@ -129,11 +129,11 @@
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "文件 '%(fileName)s' 超过了此主服务器的上传大小限制",
"The file '%(fileName)s' failed to upload": "文件 '%(fileName)s' 上传失败",
"Add email address": "添加邮件地址",
"Add phone number": "添加电话号码",
"Add phone number": "添加手机号码",
"Advanced": "高级",
"Algorithm": "算法",
"Always show message timestamps": "总是显示消息时间戳",
"%(names)s and %(lastPerson)s are typing": "%(names)s 和 %(lastPerson)s 正在打字",
"%(names)s and %(lastPerson)s are typing": "%(names)s 和 %(lastPerson)s 正在输入",
"A new password must be entered.": "一个新的密码必须被输入。.",
"%(senderName)s answered the call.": "%(senderName)s 接了通话。.",
"An error has occurred.": "一个错误出现了。",
@ -152,7 +152,7 @@
"%(targetName)s joined the room.": "%(targetName)s 已加入聊天室。",
"Jump to first unread message.": "跳到第一条未读消息。",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s 把 %(targetName)s 踢出了聊天室。.",
"Leave room": "离开聊天室",
"Leave room": "退出聊天室",
"Login as guest": "以游客的身份登录",
"New password": "新密码",
"Add a topic": "添加一个主题",
@ -177,10 +177,10 @@
"Anyone who knows the room's link, apart from guests": "任何知道聊天室链接的人,游客除外",
"Anyone who knows the room's link, including guests": "任何知道聊天室链接的人,包括游客",
"Are you sure?": "你确定吗?",
"Are you sure you want to leave the room '%(roomName)s'?": "你确定要离开聊天室 “%(roomName)s” 吗?",
"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?": "你确定要上传这些文件吗?",
"Bans user with given id": "封禁指定 ID 的用户",
"Bans user with given id": "按照 ID 封禁指定的用户",
"Blacklisted": "已列入黑名单",
"Bulk Options": "批量操作",
"Call Timeout": "通话超时",
@ -210,8 +210,8 @@
"Conference calling is in development and may not be reliable.": "视频会议功能还在开发状态,可能不稳定。",
"Conference calls are not supported in encrypted rooms": "加密聊天室不支持视频会议",
"Conference calls are not supported in this client": "此客户端不支持视频会议",
"%(count)s new messages|one": "%(count)s 条消息",
"%(count)s new messages|other": "%(count)s 消息",
"%(count)s new messages|one": "%(count)s 条未读消息",
"%(count)s new messages|other": "%(count)s 未读消息",
"Create a new chat or reuse an existing one": "创建新聊天或使用已有的聊天",
"Custom": "自定义",
"Custom level": "自定义级别",
@ -222,7 +222,7 @@
"Device key:": "设备密钥 :",
"Disable Notifications": "关闭消息通知",
"Drop File Here": "把文件拖拽到这里",
"Email address (optional)": "电子邮件地址 (可选)",
"Email address (optional)": "邮箱地址 (可选)",
"Enable Notifications": "启用消息通知",
"Encrypted by a verified device": "由一个已验证的设备加密",
"Encrypted by an unverified device": "由一个未经验证的设备加密",
@ -232,31 +232,31 @@
"Error: Problem communicating with the given homeserver.": "错误: 与指定的主服务器通信时出错。",
"Export": "导出",
"Failed to fetch avatar URL": "获取 Avatar URL 失败",
"Failed to upload profile picture!": "无法上传头像!",
"Failed to upload profile picture!": "头像上传失败",
"Guest access is disabled on this Home Server.": "此服务器禁用了游客访问。",
"Home": "主页面",
"Import": "导入",
"Incoming call from %(name)s": "来自 %(name)s 的通话",
"Incoming video call from %(name)s": "来自 %(name)s 的视频通话",
"Incoming voice call from %(name)s": "来自 %(name)s 的视频通话",
"Incoming voice call from %(name)s": "来自 %(name)s 的语音通话",
"Incorrect username and/or password.": "用户名或密码错误。",
"%(senderName)s invited %(targetName)s.": "%(senderName)s 邀请了 %(targetName)s。",
"Invited": "已邀请",
"Invites": "邀请",
"Invites user with given id to current room": "邀请指定 ID 的用户加入当前聊天室",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' 不是一个合法的电子邮件地址格式",
"Invites user with given id to current room": "按照 ID 邀请指定用户加入当前聊天室",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' 不是一个合法的邮箱地址格式",
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' 不是一个合法的昵称格式",
"%(displayName)s is typing": "%(displayName)s 正在打字",
"%(displayName)s is typing": "%(displayName)s 正在输入",
"Sign in with": "第三方登录",
"Message not sent due to unknown devices being present": "消息未发送,因为有未知的设备存在",
"Missing room_id in request": "请求中没有 room_id",
"Missing room_id in request": "请求中没有 聊天室 ID",
"Missing user_id in request": "请求中没有 user_id",
"Mobile phone number": "手机号码",
"Mobile phone number (optional)": "手机号码 (可选)",
"Moderator": "协管员",
"Mute": "静音",
"Name": "姓名",
"Never send encrypted messages to unverified devices from this device": "不要从此设备向未验证的设备发送消息",
"Never send encrypted messages to unverified devices from this device": "在此设备上不向未经验证的设备发送消息",
"New passwords don't match": "两次输入的新密码不符",
"none": "无",
"not set": "未设置",
@ -274,7 +274,7 @@
"Password:": "密码:",
"Passwords can't be empty": "密码不能为空",
"Permissions": "权限",
"Phone": "电话",
"Phone": "手机号码",
"Cancel": "取消",
"Create new room": "创建新聊天室",
"Custom Server Options": "自定义服务器选项",
@ -293,7 +293,7 @@
"Edit": "编辑",
"Joins room with given alias": "以指定的别名加入聊天室",
"Labs": "实验室",
"%(targetName)s left the room.": "%(targetName)s 离开了聊天室。",
"%(targetName)s left the room.": "%(targetName)s 退出了聊天室。",
"Logged in as:": "登录为:",
"Logout": "登出",
"Low priority": "低优先级",
@ -365,11 +365,11 @@
"Unverify": "取消验证",
"ex. @bob:example.com": "例如 @bob:example.com",
"Add User": "添加用户",
"This Home Server would like to make sure you are not a robot": "这个Home Server想要确认你不是一个机器人",
"This Home Server would like to make sure you are not a robot": "此主服务器想确认你不是机器人",
"Token incorrect": "令牌错误",
"Default server": "默认服务器",
"Custom server": "自定义服务器",
"URL Previews": "URL 预览",
"URL Previews": "链接预览",
"Drop file here to upload": "把文件拖到这里以上传",
"Online": "在线",
"Idle": "空闲",
@ -395,8 +395,8 @@
"Drop here to tag %(section)s": "拖拽到这里标记 %(section)s",
"Enable automatic language detection for syntax highlighting": "启用自动语言检测用于语法高亮",
"Failed to change power level": "修改特权级别失败",
"Kick": "踢出",
"Kicks user with given id": "踢出指定 ID 的用户",
"Kick": "移除",
"Kicks user with given id": "按照 ID 移除特定的用户",
"Last seen": "上次看见",
"Level:": "级别:",
"Local addresses for this room:": "这个聊天室的本地地址:",
@ -416,9 +416,9 @@
"Sets the room topic": "设置聊天室主题",
"Show Text Formatting Toolbar": "显示文字格式工具栏",
"This room has no local addresses": "这个聊天室没有本地地址",
"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 phone number is already in use": "此电话号码已被使用",
"This phone number is already in use": "此手机号码已被使用",
"This room": "这个聊天室",
"This room is not accessible by remote Matrix servers": "这个聊天室无法被远程 Matrix 服务器访问",
"This room's internal ID is": "这个聊天室的内部 ID 是",
@ -433,7 +433,7 @@
"Unencrypted room": "未加密的聊天室",
"unencrypted": "未加密的",
"Unencrypted message": "未加密的消息",
"unknown caller": "未知呼叫者",
"unknown caller": "未知呼叫者",
"unknown device": "未知设备",
"Unnamed Room": "未命名的聊天室",
"Unverified": "未验证",
@ -449,10 +449,10 @@
"Passwords don't match.": "密码不匹配。",
"I already have an account": "我已经有一个帐号",
"Unblacklist": "移出黑名单",
"Not a valid Riot keyfile": "不是一个合法的 Riot 密钥文件",
"Not a valid Riot keyfile": "不是一个有效的 Riot 密钥文件",
"%(targetName)s accepted an invitation.": "%(targetName)s 接受了一个邀请。",
"Do you want to load widget from URL:": "你想从此 URL 加载小组件吗:",
"Hide join/leave messages (invites/kicks/bans unaffected)": "隐藏加入/离开消息(邀请/踢出/封禁不受影响)",
"Hide join/leave messages (invites/kicks/bans unaffected)": "隐藏加入/退出消息(邀请/踢出/封禁不受影响)",
"Integrations Error": "集成错误",
"Publish this room to the public in %(domain)s's room directory?": "把这个聊天室发布到 %(domain)s 的聊天室目录吗?",
"Manage Integrations": "管理集成",
@ -464,12 +464,12 @@
"%(senderName)s requested a VoIP conference.": "%(senderName)s 请求一个 VoIP 会议。",
"Seen by %(userName)s at %(dateTime)s": "在 %(dateTime)s 被 %(userName)s 看到",
"Tagged as: ": "标记为: ",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "验证码将发送到+%(msisdn)s请输入接收到的验证码",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "验证码将发送至 +%(msisdn)s请输入收到的验证码",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 接受了 %(displayName)s 的邀请。",
"Active call (%(roomName)s)": "%(roomName)s 的呼叫",
"Active call (%(roomName)s)": "当前通话 (来自聊天室 %(roomName)s)",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 将级别调整到%(powerLevelDiffText)s 。",
"Changes colour scheme of current room": "修改了样式",
"Deops user with given id": "Deops user",
"Deops user with given id": "按照 ID 取消特定用户的管理员权限",
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "通过 <voiceText>语言</voiceText> 或者 <videoText>视频</videoText>加入.",
"%(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 设定历史浏览功能为 所有聊天室成员,从他们加入开始.",
@ -486,16 +486,16 @@
"%(roomName)s is not accessible at this time.": "%(roomName)s 此时无法访问。",
"Start authentication": "开始认证",
"The maximum permitted number of widgets have already been added to this room.": "小部件的最大允许数量已经添加到这个聊天室了。",
"The phone number entered looks invalid": "输入的电话号码看起来无效",
"The phone number entered looks invalid": "输入的手机号码看起来无效",
"The remote side failed to pick up": "远端未能接收到",
"This Home Server does not support login using email address.": "HS不支持使用电子邮件地址登陆。",
"This invitation was sent to an email address which is not associated with this account:": "此邀请被发送到与此帐户不相关的电子邮件地址:",
"This Home Server does not support login using email address.": "HS不支持使用邮箱地址登陆。",
"This invitation was sent to an email address which is not associated with this account:": "此邀请被发送到与此帐户不相关的邮箱地址:",
"This room is not recognised.": "无法识别此聊天室。",
"To get started, please pick a username!": "请点击用户名!",
"Unable to add email address": "无法添加电子邮件地址",
"Unable to add email address": "无法添加邮箱地址",
"Automatically replace plain text Emoji": "文字、表情自动转换",
"To reset your password, enter the email address linked to your account": "要重置你的密码,请输入关联你的帐号的电子邮箱地址",
"Unable to verify email address.": "无法验证电子邮箱地址。",
"To reset your password, enter the email address linked to your account": "要重置你的密码,请输入关联你的帐号的邮箱地址",
"Unable to verify email address.": "无法验证邮箱地址。",
"Unknown room %(roomId)s": "未知聊天室 %(roomId)s",
"Unknown (user, device) pair:": "未知(用户,设备)对:",
"Unrecognised command:": "无法识别的命令:",
@ -515,18 +515,18 @@
"You cannot place VoIP calls in this browser.": "你不能在这个浏览器中发起 VoIP 通话。",
"You do not have permission to post to this room": "你没有发送到这个聊天室的权限",
"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?": "你好像在一个通话中,你确定要退出吗?",
"You seem to be uploading files, are you sure you want to quit?": "你好像正在上传文件,你确定要退出吗?",
"You seem to be in a call, are you sure you want to quit?": "您似乎正在进行通话,确定要退出吗?",
"You seem to be uploading files, are you sure you want to quit?": "您似乎正在上传文件,确定要退出吗?",
"You should not yet trust it to secure data": "你不应该相信它来保护你的数据",
"Upload an avatar:": "上传一个头像:",
"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 email address.": "这看起来不是一个合法的邮箱地址。",
"This doesn't look like a valid phone number.": "这看起来不是一个合法的手机号码。",
"User names may only contain letters, numbers, dots, hyphens and underscores.": "用户名只可以包含字母、数字、点、连字号和下划线。",
"An unknown error occurred.": "一个未知错误出现了。",
"An error occurred: %(error_string)s": "一个错误出现了: %(error_string)s",
"Encrypt room": "加密聊天室",
"There are no visible files in this room": "这个聊天室里面没有可见的文件",
"Active call": "活跃的通话",
"Active call": "当前通话",
"Verify...": "验证...",
"Error decrypting audio": "解密音频时出错",
"Error decrypting image": "解密图像时出错",
@ -537,7 +537,7 @@
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室头像。",
"Something went wrong!": "出了点问题!",
"If you already have a Matrix account you can <a>log in</a> instead.": "如果你已经有一个 Matrix 帐号,你可以<a>登录</a>。",
"Do you want to set an email address?": "你要设置一个电子邮箱地址吗?",
"Do you want to set an email address?": "你要设置一个邮箱地址吗?",
"New address (e.g. #foo:%(localDomain)s)": "新的地址(例如 #foo:%(localDomain)s",
"Upload new:": "上传新的:",
"User ID": "用户 ID",
@ -547,16 +547,16 @@
"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 和 device %(deviceId)s 的签名密钥是 \"%(fprint)s\",和提供的咪呀 \"%(fingerprint)s\" 不匹配。这可能意味着你的通信正在被窃听!",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s 收回了 %(targetName)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 already have existing direct chats with this user:": "你已经有和用户的直接聊天:",
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "你现在还不再任何聊天室!按下 <CreateRoomButton> 来创建一个聊天室或者 <RoomDirectoryButton> 来浏览目录",
"You cannot place a call with yourself.": "你不能和你自己发起一个通话。",
"You have been kicked from %(roomName)s by %(userName)s.": "你已经被 %(userName)s 踢出了 %(roomName)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 <a>disabled</a> URL previews by default.": "你已经默认 <a>禁用</a> URL 预览。",
"You have <a>enabled</a> URL previews by default.": "你已经默认 <a>启用</a> URL 预览。",
"You have <a>disabled</a> URL previews by default.": "你已经默认 <a>禁用</a> 链接预览。",
"You have <a>enabled</a> URL previews by default.": "你已经默认 <a>启用</a> 链接预览。",
"Your home server does not support device management.": "你的 home server 不支持设备管理。",
"Set a display name:": "设置一个昵称:",
"This server does not support authentication with a phone number.": "这个服务器不支持用电话号码认证。",
"This server does not support authentication with a phone number.": "这个服务器不支持用手机号码认证。",
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "密码过短(最短为 %(MIN_PASSWORD_LENGTH)s。",
"Make this room private": "使这个聊天室私密",
"Share message history with new users": "和新用户共享消息历史",
@ -593,7 +593,7 @@
"You must join the room to see its files": "你必须加入聊天室以看到它的文件",
"Failed to invite the following users to the %(roomName)s room:": "邀请以下用户到 %(roomName)s 聊天室失败:",
"Confirm Removal": "确认移除",
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "这会让你的账户永远不可用。你无法重新注册同一个用户 ID.",
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "这将会导致您的账户永远无法使用。你将无法重新注册同样的用户 ID。",
"Verifies a user, device, and pubkey tuple": "验证一个用户、设备和密钥元组",
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "我们在尝试恢复你之前的会话时遇到了一个错误。如果你继续,你将需要重新登录,加密的聊天历史将会不可读。",
"Unknown devices": "未知设备",
@ -609,9 +609,9 @@
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "你可以使用自定义的服务器选项来通过指定一个不同的主服务器 URL 来登录其他 Matrix 服务器。",
"This allows you to use this app with an existing Matrix account on a different home server.": "这允许你用一个已有在不同主服务器的 Matrix 账户使用这个应用。",
"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?": "如果你不指定一个电子邮箱地址,你将不能重置你的密码。你确定吗?",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "如果你不指定一个邮箱地址,你将不能重置你的密码。你确定吗?",
"Home server URL": "主服务器 URL",
"Identity server URL": "身份服务器 URL",
"Identity server URL": "身份认证服务器 URL",
"What does this mean?": "这是什么意思?",
"Image '%(Body)s' cannot be displayed.": "图像 '%(Body)s' 无法显示。",
"This image cannot be displayed.": "图像无法显示。",
@ -620,12 +620,12 @@
"Ongoing conference call%(supportedText)s.": "正在进行的会议通话 %(supportedText)s.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s 修改了 %(roomName)s 的头像",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "这将会成为你在 <span></span> 主服务器上的账户名,或者你可以选择一个 <a>不同的服务器</a>。",
"Your browser does not support the required cryptography extensions": "你的浏览器不支持所需的密码学扩展",
"Your browser does not support the required cryptography extensions": "你的浏览器不支持 Riot 所需的密码学特性",
"Authentication check failed: incorrect password?": "身份验证失败:密码错误?",
"This will allow you to reset your password and receive notifications.": "这将允许你重置你的密码和接收通知。",
"Share without verifying": "不验证就分享",
"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' 正在请求加密密钥。",
"Your unverified device '%(displayName)s' is requesting encryption keys.": "你的未验证的设备 '%(displayName)s' 正在请求加密密钥。",
"Encryption key request": "加密密钥请求",
"Autocomplete Delay (ms):": "自动补全延迟(毫秒):",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s 小组建被 %(senderName)s 添加",
@ -650,18 +650,18 @@
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s 解除了 %(targetName)s 的封禁。",
"(could not connect media)": "(无法连接媒体)",
"%(senderName)s changed the pinned messages for the room.": "%(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 和另一个人正在打字",
"%(names)s and %(count)s others are typing|other": "%(names)s 和另外 %(count)s 个人正在输入",
"%(names)s and %(count)s others are typing|one": "%(names)s 和另一个人正在输入",
"Send": "发送",
"Message Pinning": "消息置顶",
"Disable Emoji suggestions while typing": "禁用打字时Emoji建议",
"Disable Emoji suggestions while typing": "输入时禁用 Emoji 建议",
"Use compact timeline layout": "使用紧凑的时间线布局",
"Hide avatar changes": "隐藏头像修改",
"Hide display name changes": "隐藏昵称修改",
"Hide display name changes": "隐藏昵称修改",
"Disable big emoji in chat": "禁用聊天中的大Emoji",
"Never send encrypted messages to unverified devices in this room from this device": "在这个聊天室永不从这个设备发送加密消息到未验证的设备",
"Enable URL previews for this room (only affects you)": "在这个聊天室启用 URL 预览(只影响你)",
"Enable URL previews by default for participants in this room": "对这个聊天室的参与者默认启用 URL 预览",
"Never send encrypted messages to unverified devices in this room from this device": "在此设备上,在此聊天室中不向未经验证的设备发送加密的消息",
"Enable URL previews for this room (only affects you)": "在此聊天室启用链接预览(只影响你)",
"Enable URL previews by default for participants in this room": "对这个聊天室的参与者默认启用 链接预览",
"Delete %(count)s devices|other": "删除了 %(count)s 个设备",
"Delete %(count)s devices|one": "删除设备",
"Select devices": "选择设备",
@ -706,10 +706,10 @@
"were kicked %(count)s times|one": "被踢出",
"was kicked %(count)s times|other": "被踢出 %(count)s 次",
"was kicked %(count)s times|one": "被踢出",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s 改了他们的名 %(count)s 次",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s 改了他们的名",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s 改了他们的名 %(count)s 次",
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s 改了他们的名",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s 改了他们的名 %(count)s 次",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s 改了他们的名",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s 改了他们的名 %(count)s 次",
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s 改了他们的名",
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s 更换了他们的的头像 %(count)s 次",
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s 更换了他们的头像",
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s 更换了他们的头像 %(count)s 次",
@ -718,10 +718,10 @@
"%(items)s and %(count)s others|one": "%(items)s 和另一个人",
"collapse": "折叠",
"expand": "展开",
"email address": "电子邮箱地址",
"email address": "邮箱地址",
"You have entered an invalid address.": "你输入了一个无效地址。",
"Advanced options": "高级选项",
"Leave": "离开",
"Leave": "退出",
"Description": "描述",
"Warning": "警告",
"Light theme": "浅色主题",
@ -738,14 +738,13 @@
"Your homeserver's URL": "您的主服务器的链接",
"Your identity server's URL": "您的身份认证服务器的链接",
"The information being sent to us to help make Riot.im better includes:": "将要为帮助 Riot.im 发展而发送的信息包含:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "我们也记录了您在本应用中使用的页面(目前为 <CurrentPageHash> User Agent<CurrentUserAgent>)和设备的分辨率(<CurrentDeviceResolution>)。",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "这个页面中含有可能能用于识别您身份的信息,比如聊天室、用户或群组 ID在它们发送到服务器上之前这些数据会被移除。",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s%(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s%(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s%(monthName)s %(day)s %(fullYear)s %(time)s",
"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": "警告:您添加的用户对一切知道这个社区的 ID 的人公开",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:您添加的一切用户都将会对一切知道此社区的 ID 的人公开",
"Name or matrix ID": "名称或 Matrix ID",
"Which rooms would you like to add to this community?": "您想把哪个聊天室添加到这个社区中?",
"Add rooms to the community": "添加聊天室到社区",
@ -753,11 +752,11 @@
"Failed to invite users to community": "邀请用户到社区失败",
"Message Replies": "消息回复",
"Disable Peer-to-Peer for 1:1 calls": "在1:1通话中禁用点到点",
"Enable inline URL previews by default": "默认启用自动网址预览",
"Disinvite this user?": "取消邀请这个用户?",
"Kick this user?": "踢出这个用户?",
"Unban this user?": "解除这个用户的封禁?",
"Ban this user?": "封紧这个用户?",
"Enable inline URL previews by default": "默认启用网址预览",
"Disinvite this user?": "取消邀请用户?",
"Kick this user?": "移除此用户?",
"Unban this user?": "解除用户的封禁?",
"Ban this user?": "封紧用户?",
"Send an encrypted reply…": "发送加密的回复…",
"Send a reply (unencrypted)…": "发送回复(未加密)…",
"Send an encrypted message…": "发送加密消息…",
@ -790,7 +789,7 @@
"Add a User": "添加一个用户",
"Unable to accept invite": "无法接受邀请",
"Unable to reject invite": "无法拒绝邀请",
"Leave Community": "离开社区",
"Leave Community": "退出社区",
"Community Settings": "社区设置",
"Community %(groupId)s not found": "找不到社区 %(groupId)s",
"Your Communities": "你的社区",
@ -802,13 +801,13 @@
"Failed to invite users to %(groupId)s": "邀请用户到 %(groupId)s 失败",
"Failed to invite the following users to %(groupId)s:": "邀请下列用户到 %(groupId)s 失败:",
"Failed to add the following rooms to %(groupId)s:": "添加以下聊天室到 %(groupId)s 失败:",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "你似乎没有将此邮箱地址同在此主服务器上的任何一个 Matrix 账号相关联。",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "你似乎没有将此邮箱地址同在此主服务器上的任何一个 Matrix 账号绑定。",
"Restricted": "受限用户",
"To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。",
"Hide avatars in user and room mentions": "隐藏头像",
"Disable Community Filter Panel": "停用社区面板",
"Opt out of analytics": "禁用开发数据上传",
"Opt out of analytics": "退出统计分析服务",
"Stickerpack": "贴图集",
"Sticker Messages": "贴图消息",
"You don't currently have any stickerpacks enabled": "您目前没有启用任何贴纸包",
@ -849,13 +848,13 @@
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"You are registering with %(SelectedTeamName)s": "你将注册为 %(SelectedTeamName)s",
"Remove from community": "从社区中移除",
"Disinvite this user from community?": "是否要取消邀请此用户加入社区?",
"Disinvite this user from community?": "是否不再邀请此用户加入本社区?",
"Remove this user from community?": "是否要从社区中移除此用户?",
"Failed to withdraw invitation": "撤回邀请失败",
"Failed to remove user from community": "移除用户失败",
"Filter community members": "过滤社区成员",
"Flair will not appear": "将不会显示 Flair",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中删除1吗?",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中移除 %(roomName)s 吗?",
"Removing a room from the community will also remove it from the community page.": "从社区中移除房间时,同时也会将其从社区页面中移除。",
"Failed to remove room from community": "从社区中移除聊天室失败",
"Failed to remove '%(roomName)s' from %(groupId)s": "从 %(groupId)s 中移除 “%(roomName)s” 失败",
@ -868,18 +867,18 @@
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s 已加入 %(count)s 次",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s 已加入",
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已离开 %(count)s 次",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已离开",
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已离开 %(count)s 次",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已离开",
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s 已加入&已离开 %(count)s 次",
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s 已加入&已离开",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s 已加入&已离开 %(count)s 次",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s 已加入&已离开",
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s 离开并重新加入了 %(count)s 次",
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s 离开并重新加入了",
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s 离开并重新加入了 %(count)s 次",
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s 离开并重新加入了",
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已退出 %(count)s 次",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已退出",
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已退出 %(count)s 次",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已退出",
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s 已加入&已退出 %(count)s 次",
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s 已加入&已退出",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s 已加入&已退出 %(count)s 次",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s 已加入&已退出",
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s 退出并重新加入了 %(count)s 次",
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s 退出并重新加入了",
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s 退出并重新加入了 %(count)s 次",
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s 退出并重新加入了",
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s 拒绝了他们的邀请",
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s 拒绝了他们的邀请共 %(count)s 次",
"%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s 拒绝了他们的邀请共 %(count)s 次",
@ -893,7 +892,7 @@
"Community IDs cannot not be empty.": "社区 ID 不能为空。",
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社区 ID 只能包含 a-z、0-9 或 “=_-./” 等字符",
"Something went wrong whilst creating your community": "创建社区时出现问题",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "您目前默认将未验证的设备列入黑名单;在发送消息到这些设备上之前,您必须先验证它们。",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "您目前默认将未验证的设备列入黑名单;在发送消息到这些设备上之前,您必须先验证它们。",
"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.": "如果您之前使用过较新版本的 Riot则您的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。",
"To change the room's avatar, you must be a": "无法修改此聊天室的头像,因为您不是此聊天室的",
"To change the room's name, you must be a": "无法修改此聊天室的名称,因为您不是此聊天室的",
@ -906,7 +905,7 @@
"URL previews are enabled by default for participants in this room.": "此聊天室默认启用链接预览。",
"URL previews are disabled by default for participants in this room.": "此聊天室默认禁用链接预览。",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s 将聊天室的头像更改为 <img/>",
"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.": "您也可以自定义身份认证服务器,但这通常会阻止基于邮箱地址的与用户的交互。",
"Please enter the code it contains:": "请输入它包含的代码:",
"Flair will appear if enabled in room settings": "如果在聊天室设置中启用, flair 将会显示",
"Matrix ID": "Matrix ID",
@ -956,7 +955,7 @@
"Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,您正在登录的服务器是 %(hs)s不是 matrix.org。",
"This homeserver doesn't offer any login flows which are supported by this client.": "此主服务器不兼容本客户端支持的任何登录方式。",
"Sign in to get started": "登录以开始使用",
"Unbans user with given id": "使用 ID 解封特定的用户",
"Unbans user with given id": "按照 ID 解封特定的用户",
"Opens the Developer Tools dialog": "打开开发者工具窗口",
"Notify the whole room": "通知聊天室全体成员",
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许您将加密聊天室中收到的消息的密钥导出为本地文件。您可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。",
@ -966,7 +965,7 @@
"Ignores a user, hiding their messages from you": "忽略用户,隐藏他们的消息",
"Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息",
"To return to your account in future you need to set a password": "如果你想再次使用账号,您得为它设置一个密码",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "如果你在 GitHub 提交了一个 bug调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,这包括您的用户名、您访问的房间或社区的 ID 或名称以及其他用户的用户名,不包扩聊天记录。",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,这包括您的用户名、您访问的房间或社区的 ID 或名称以及其他用户的用户名,不包扩聊天记录。",
"Debug Logs Submission": "发送调试日志",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "密码修改成功。在您在其他设备上重新登录之前,其他设备不会收到推送通知",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "尝试加载此房间的时间线的特定时间点,但是无法找到。",
@ -990,7 +989,7 @@
"Friday": "星期五",
"Update": "更新",
"What's New": "新鲜事",
"Add an email address above to configure email notifications": "请在上方输入电子邮件地址以接收邮件通知",
"Add an email address above to configure email notifications": "请在上方输入邮箱地址以接收邮件通知",
"Expand panel": "展开面板",
"On": "打开",
"%(count)s Members|other": "%(count)s 位成员",
@ -1044,7 +1043,7 @@
"Tuesday": "星期二",
"Enter keywords separated by a comma:": "输入以逗号间隔的关键字:",
"Forward Message": "转发消息",
"You have successfully set a password and an email address!": "您已经成功设置了密码和电子邮件地址!",
"You have successfully set a password and an email address!": "您已经成功设置了密码和邮箱地址!",
"Remove %(name)s from the directory?": "从目录中移除 %(name)s 吗?",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot 使用了许多先进的浏览器功能,有些在你目前所用的浏览器上无法使用或仅为实验性的功能。",
"Developer Tools": "开发者工具",
@ -1129,5 +1128,9 @@
"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": "更多的通知并没有在此显示出来"
"There are advanced notifications which are not shown here": "更多的通知并没有在此显示出来",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "这里没有其他人了!你是想 <inviteText>邀请用户</inviteText> 还是 <nowarnText>不再提示</nowarnText>",
"You need to be able to invite users to do that.": "你需要有邀请用户的权限才能进行此操作。",
"Missing roomId.": "找不到此聊天室 ID 所对应的聊天室。",
"Tag Panel": "标签面板"
}

View file

@ -956,7 +956,6 @@
"Notify the whole room": "通知整個聊天室",
"Room Notification": "聊天室通知",
"The information being sent to us to help make Riot.im better includes:": "協助讓 Riot.im 變得更好的傳送給我們的資訊包含了:",
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "我們也紀錄了每個您在應用程式中使用的頁面(目前 <CurrentPageHash>),您的使用者代理(<CurrentUserAgent>)與您的裝置解析度(<CurrentDeviceResolution>)。",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "這個頁面包含了可識別的資訊,如聊天室、使用者或群組 ID這些資料會在傳到伺服器前被刪除。",
"The platform you're on": "您使用的平臺是",
"The version of Riot.im": "Riot.im 的版本",
@ -987,7 +986,7 @@
"%(user)s is a %(userRole)s": "%(user)s 是 %(userRole)s",
"Code": "代碼",
"Debug Logs Submission": "除錯訊息傳送",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "如果您透過 GitHub 來回報錯誤,除錯訊息可以用來追蹤問題。除錯訊息包含應用程式的使用資料,包括您的使用者名稱、您所造訪的房間/群組的 ID 或別名、其他使用者的使用者名稱等,其中不包含訊息本身。",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果您透過 GitHub 來回報錯誤,除錯訊息可以用來追蹤問題。除錯訊息包含應用程式的使用資料,包括您的使用者名稱、您所造訪的房間/群組的 ID 或別名、其他使用者的使用者名稱等,其中不包含訊息本身。",
"Submit debug logs": "傳送除錯訊息",
"Opens the Developer Tools dialog": "開啟開發者工具對話視窗",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "被 %(displayName)s (%(userName)s) 於 %(dateTime)s 看過",
@ -1157,5 +1156,7 @@
"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": "有些進階的通知並未在此顯示"
"There are advanced notifications which are not shown here": "有些進階的通知並未在此顯示",
"Missing roomId.": "缺少 roomid。",
"Picture": "圖片"
}

View file

@ -395,9 +395,6 @@ function selectQuery(store, keyRange, resultMapper) {
}
let store = null;
let logger = null;
let initPromise = null;
module.exports = {
/**
@ -406,11 +403,11 @@ module.exports = {
* @return {Promise} Resolves when set up.
*/
init: function() {
if (initPromise) {
return initPromise;
if (global.mx_rage_initPromise) {
return global.mx_rage_initPromise;
}
logger = new ConsoleLogger();
logger.monkeyPatch(window.console);
global.mx_rage_logger = new ConsoleLogger();
global.mx_rage_logger.monkeyPatch(window.console);
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
@ -420,19 +417,19 @@ module.exports = {
} catch(e) {}
if (indexedDB) {
store = new IndexedDBLogStore(indexedDB, logger);
initPromise = store.connect();
return initPromise;
global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger);
global.mx_rage_initPromise = global.mx_rage_store.connect();
return global.mx_rage_initPromise;
}
initPromise = Promise.resolve();
return initPromise;
global.mx_rage_initPromise = Promise.resolve();
return global.mx_rage_initPromise;
},
flush: function() {
if (!store) {
if (!global.mx_rage_store) {
return;
}
store.flush();
global.mx_rage_store.flush();
},
/**
@ -440,10 +437,10 @@ module.exports = {
* @return Promise Resolves if cleaned logs.
*/
cleanup: async function() {
if (!store) {
if (!global.mx_rage_store) {
return;
}
await store.consume();
await global.mx_rage_store.consume();
},
/**
@ -452,21 +449,21 @@ module.exports = {
* @return {Array<{lines: string, id, string}>} list of log data
*/
getLogsForReport: async function() {
if (!logger) {
if (!global.mx_rage_logger) {
throw new Error(
"No console logger, did you forget to call init()?"
);
}
// If in incognito mode, store is null, but we still want bug report
// sending to work going off the in-memory console logs.
if (store) {
if (global.mx_rage_store) {
// flush most recent logs
await store.flush();
return await store.consume();
await global.mx_rage_store.flush();
return await global.mx_rage_store.consume();
}
else {
return [{
lines: logger.flush(true),
lines: global.mx_rage_logger.flush(true),
id: "-",
}];
}

View file

@ -150,6 +150,11 @@ export const SETTINGS = {
displayName: _td('Autoplay GIFs and videos'),
default: false,
},
"alwaysShowEncryptionIcons": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Always show encryption icons'),
default: true,
},
"enableSyntaxHighlightLanguageDetection": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable automatic language detection for syntax highlighting'),

View file

@ -48,106 +48,110 @@ function checkBacklog() {
// Limit the maximum number of ongoing promises returned by fn to LIMIT and
// use a FIFO queue to handle the backlog.
function limitConcurrency(fn) {
return new Promise((resolve, reject) => {
const item = () => {
ongoingRequestCount++;
resolve();
};
if (ongoingRequestCount >= LIMIT) {
// Enqueue this request for later execution
backlogQueue.push(item);
} else {
item();
}
})
.then(fn)
.then((result) => {
async function limitConcurrency(fn) {
if (ongoingRequestCount >= LIMIT) {
// Enqueue this request for later execution
await new Promise((resolve, reject) => {
backlogQueue.push(resolve);
});
}
ongoingRequestCount++;
try {
return await fn();
} catch (err) {
// We explicitly do not handle the error here, but let it propogate.
throw err;
} finally {
ongoingRequestCount--;
checkBacklog();
return result;
});
}
}
/**
* Stores the group summary for a room and provides an API to change it and
* other useful group APIs that may have an effect on the group summary.
* Global store for tracking group summary, members, invited members and rooms.
*/
export default class GroupStore extends EventEmitter {
static STATE_KEY = {
class GroupStore extends EventEmitter {
STATE_KEY = {
GroupMembers: 'GroupMembers',
GroupInvitedMembers: 'GroupInvitedMembers',
Summary: 'Summary',
GroupRooms: 'GroupRooms',
};
constructor(groupId) {
constructor() {
super();
if (!groupId) {
throw new Error('GroupStore needs a valid groupId to be created');
}
this.groupId = groupId;
this._state = {};
this._state[GroupStore.STATE_KEY.Summary] = {};
this._state[GroupStore.STATE_KEY.GroupRooms] = [];
this._state[GroupStore.STATE_KEY.GroupMembers] = [];
this._state[GroupStore.STATE_KEY.GroupInvitedMembers] = [];
this._ready = {};
this._state[this.STATE_KEY.Summary] = {};
this._state[this.STATE_KEY.GroupRooms] = {};
this._state[this.STATE_KEY.GroupMembers] = {};
this._state[this.STATE_KEY.GroupInvitedMembers] = {};
this._ready = {};
this._ready[this.STATE_KEY.Summary] = {};
this._ready[this.STATE_KEY.GroupRooms] = {};
this._ready[this.STATE_KEY.GroupMembers] = {};
this._ready[this.STATE_KEY.GroupInvitedMembers] = {};
this._fetchResourcePromise = {
[this.STATE_KEY.Summary]: {},
[this.STATE_KEY.GroupRooms]: {},
[this.STATE_KEY.GroupMembers]: {},
[this.STATE_KEY.GroupInvitedMembers]: {},
};
this._fetchResourcePromise = {};
this._resourceFetcher = {
[GroupStore.STATE_KEY.Summary]: () => {
[this.STATE_KEY.Summary]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupSummary(this.groupId),
() => MatrixClientPeg.get().getGroupSummary(groupId),
);
},
[GroupStore.STATE_KEY.GroupRooms]: () => {
[this.STATE_KEY.GroupRooms]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupRooms(this.groupId).then(parseRoomsResponse),
() => MatrixClientPeg.get().getGroupRooms(groupId).then(parseRoomsResponse),
);
},
[GroupStore.STATE_KEY.GroupMembers]: () => {
[this.STATE_KEY.GroupMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupUsers(this.groupId).then(parseMembersResponse),
() => MatrixClientPeg.get().getGroupUsers(groupId).then(parseMembersResponse),
);
},
[GroupStore.STATE_KEY.GroupInvitedMembers]: () => {
[this.STATE_KEY.GroupInvitedMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then(parseMembersResponse),
() => MatrixClientPeg.get().getGroupInvitedUsers(groupId).then(parseMembersResponse),
);
},
};
this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err);
this.on('error', (err, groupId) => {
console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err);
});
}
_fetchResource(stateKey) {
_fetchResource(stateKey, groupId) {
// Ongoing request, ignore
if (this._fetchResourcePromise[stateKey]) return;
if (this._fetchResourcePromise[stateKey][groupId]) return;
const clientPromise = this._resourceFetcher[stateKey]();
const clientPromise = this._resourceFetcher[stateKey](groupId);
// Indicate ongoing request
this._fetchResourcePromise[stateKey] = clientPromise;
this._fetchResourcePromise[stateKey][groupId] = clientPromise;
clientPromise.then((result) => {
this._state[stateKey] = result;
this._ready[stateKey] = true;
this._state[stateKey][groupId] = result;
this._ready[stateKey][groupId] = true;
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
if (stateKey === this.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
return;
}
console.error("Failed to get resource " + stateKey + ":" + err);
this.emit('error', err);
console.error(`Failed to get resource ${stateKey} for ${groupId}`, err);
this.emit('error', err, groupId);
}).finally(() => {
// Indicate finished request, allow for future fetches
delete this._fetchResourcePromise[stateKey];
delete this._fetchResourcePromise[stateKey][groupId];
});
return clientPromise;
@ -162,25 +166,29 @@ export default class GroupStore extends EventEmitter {
* immediately triggers an update to send the current state of the
* store (which could be the initial state).
*
* This also causes a fetch of all group data, which might cause
* 4 separate HTTP requests, but only said requests aren't already
* ongoing.
* If a group ID is specified, this also causes a fetch of all data
* of the specified group, which might cause 4 separate HTTP
* requests, but only if said requests aren't already ongoing.
*
* @param {string?} groupId the ID of the group to fetch data for.
* Optional.
* @param {function} fn the function to call when the store updates.
* @return {Object} tok a registration "token" with a single
* property `unregister`, a function that can
* be called to unregister the listener such
* that it won't be called any more.
*/
registerListener(fn) {
registerListener(groupId, fn) {
this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
this._fetchResource(GroupStore.STATE_KEY.Summary);
this._fetchResource(GroupStore.STATE_KEY.GroupRooms);
this._fetchResource(GroupStore.STATE_KEY.GroupMembers);
this._fetchResource(GroupStore.STATE_KEY.GroupInvitedMembers);
if (groupId) {
this._fetchResource(this.STATE_KEY.Summary, groupId);
this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
this._fetchResource(this.STATE_KEY.GroupInvitedMembers, groupId);
}
// Similar to the Store of flux/utils, we return a "token" that
// can be used to unregister the listener.
@ -195,123 +203,137 @@ export default class GroupStore extends EventEmitter {
this.removeListener('update', fn);
}
isStateReady(id) {
return this._ready[id];
isStateReady(groupId, id) {
return this._ready[id][groupId];
}
getSummary() {
return this._state[GroupStore.STATE_KEY.Summary];
getSummary(groupId) {
return this._state[this.STATE_KEY.Summary][groupId] || {};
}
getGroupRooms() {
return this._state[GroupStore.STATE_KEY.GroupRooms];
getGroupRooms(groupId) {
return this._state[this.STATE_KEY.GroupRooms][groupId] || [];
}
getGroupMembers() {
return this._state[GroupStore.STATE_KEY.GroupMembers];
getGroupMembers(groupId) {
return this._state[this.STATE_KEY.GroupMembers][groupId] || [];
}
getGroupInvitedMembers() {
return this._state[GroupStore.STATE_KEY.GroupInvitedMembers];
getGroupInvitedMembers(groupId) {
return this._state[this.STATE_KEY.GroupInvitedMembers][groupId] || [];
}
getGroupPublicity() {
return this._state[GroupStore.STATE_KEY.Summary].user ?
this._state[GroupStore.STATE_KEY.Summary].user.is_publicised : null;
getGroupPublicity(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_publicised : null;
}
isUserPrivileged() {
return this._state[GroupStore.STATE_KEY.Summary].user ?
this._state[GroupStore.STATE_KEY.Summary].user.is_privileged : null;
isUserPrivileged(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_privileged : null;
}
addRoomToGroup(roomId, isPublic) {
refreshGroupRooms(groupId) {
return this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
}
refreshGroupMembers(groupId) {
return this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
}
addRoomToGroup(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.addRoomToGroup(this.groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
.addRoomToGroup(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
updateGroupRoomVisibility(roomId, isPublic) {
updateGroupRoomVisibility(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.updateGroupRoomVisibility(this.groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
.updateGroupRoomVisibility(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
removeRoomFromGroup(roomId) {
removeRoomFromGroup(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroup(this.groupId, roomId)
.removeRoomFromGroup(groupId, roomId)
// Room might be in the summary, refresh just in case
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
inviteUserToGroup(userId) {
return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
inviteUserToGroup(groupId, userId) {
return MatrixClientPeg.get().inviteUserToGroup(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
acceptGroupInvite() {
return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
acceptGroupInvite(groupId) {
return MatrixClientPeg.get().acceptGroupInvite(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
joinGroup() {
return MatrixClientPeg.get().joinGroup(this.groupId)
joinGroup(groupId) {
return MatrixClientPeg.get().joinGroup(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
leaveGroup() {
return MatrixClientPeg.get().leaveGroup(this.groupId)
leaveGroup(groupId) {
return MatrixClientPeg.get().leaveGroup(groupId)
// The user should now not be able to access group settings
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might only be able to see a subset of rooms now
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now not appear as a member
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers));
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId));
}
addRoomToGroupSummary(roomId, categoryId) {
addRoomToGroupSummary(groupId, roomId, categoryId) {
return MatrixClientPeg.get()
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
.addRoomToGroupSummary(groupId, roomId, categoryId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
addUserToGroupSummary(userId, roleId) {
addUserToGroupSummary(groupId, userId, roleId) {
return MatrixClientPeg.get()
.addUserToGroupSummary(this.groupId, userId, roleId)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
.addUserToGroupSummary(groupId, userId, roleId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeRoomFromGroupSummary(roomId) {
removeRoomFromGroupSummary(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroupSummary(this.groupId, roomId)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
.removeRoomFromGroupSummary(groupId, roomId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeUserFromGroupSummary(userId) {
removeUserFromGroupSummary(groupId, userId) {
return MatrixClientPeg.get()
.removeUserFromGroupSummary(this.groupId, userId)
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
.removeUserFromGroupSummary(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
setGroupPublicity(isPublished) {
setGroupPublicity(groupId, isPublished) {
return MatrixClientPeg.get()
.setGroupPublicity(this.groupId, isPublished)
.setGroupPublicity(groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
}
let singletonGroupStore = null;
if (!singletonGroupStore) {
singletonGroupStore = new GroupStore();
}
module.exports = singletonGroupStore;

View file

@ -1,38 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import GroupStore from './GroupStore';
class GroupStoreCache {
constructor() {
this.groupStore = null;
}
getGroupStore(groupId) {
if (!this.groupStore || this.groupStore.groupId !== groupId) {
// This effectively throws away the reference to any previous GroupStore,
// allowing it to be GCd once the components referencing it have stopped
// referencing it.
this.groupStore = new GroupStore(groupId);
}
return this.groupStore;
}
}
if (global.singletonGroupStoreCache === undefined) {
global.singletonGroupStoreCache = new GroupStoreCache();
}
export default global.singletonGroupStoreCache;

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,25 +23,62 @@ import 'isomorphic-fetch';
import MatrixClientPeg from '../MatrixClientPeg';
import Promise from 'bluebird';
// WARNING: We have to be very careful about what mime-types we allow into blobs,
// as for performance reasons these are now rendered via URL.createObjectURL()
// rather than by converting into data: URIs.
//
// This means that the content is rendered using the origin of the script which
// called createObjectURL(), and so if the content contains any scripting then it
// will pose a XSS vulnerability when the browser renders it. This is particularly
// bad if the user right-clicks the URI and pastes it into a new window or tab,
// as the blob will then execute with access to Riot's full JS environment(!)
//
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
// for details.
//
// We mitigate this by only allowing mime-types into blobs which we know don't
// contain any scripting, and instantiate all others as application/octet-stream
// regardless of what mime-type the event claimed. Even if the payload itself
// is some malicious HTML, the fact we instantiate it with a media mimetype or
// application/octet-stream means the browser doesn't try to render it as such.
//
// One interesting edge case is image/svg+xml, which empirically *is* rendered
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
// *even if the mimetype is application/octet-stream*. However, empirically JS
// in the SVG isn't executed in this scenario, so we seem to be okay.
//
// Tested on Chrome 65 and Firefox 60
//
// The list below is taken mainly from
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
// events, so we pick the ones which HTML5 browsers should be able to display
//
// For the record, mime-types which must NEVER enter this list below include:
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
/**
* Read blob as a data:// URI.
* @return {Promise} A promise that resolves with the data:// URI.
*/
export function readBlobAsDataUri(file) {
const deferred = Promise.defer();
const reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsDataURL(file);
return deferred.promise;
const ALLOWED_BLOB_MIMETYPES = {
'image/jpeg': true,
'image/gif': true,
'image/png': true,
'video/mp4': true,
'video/webm': true,
'video/ogg': true,
'audio/mp4': true,
'audio/webm': true,
'audio/aac': true,
'audio/mpeg': true,
'audio/ogg': true,
'audio/wave': true,
'audio/wav': true,
'audio/x-wav': true,
'audio/x-pn-wav': true,
'audio/flac': true,
'audio/x-flac': true,
}
/**
* Decrypt a file attached to a matrix event.
* @param file {Object} The json taken from the matrix event.
@ -61,7 +99,17 @@ export function decryptFile(file) {
return encrypt.decryptAttachment(responseData, file);
}).then(function(dataArray) {
// Turn the array into a Blob and give it the correct MIME-type.
const blob = new Blob([dataArray], {type: file.mimetype});
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
// they introduce XSS attacks if the Blob URI is viewed directly in the
// browser (e.g. by copying the URI into a new tab or window.)
// See warning at top of file.
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
mimetype = 'application/octet-stream';
}
const blob = new Blob([dataArray], {type: mimetype});
return blob;
});
}

View file

@ -18,7 +18,7 @@ limitations under the License.
import MatrixClientPeg from '../MatrixClientPeg';
import {getAddressType} from '../UserAddress';
import {inviteToRoom} from '../RoomInvite';
import GroupStoreCache from '../stores/GroupStoreCache';
import GroupStore from '../stores/GroupStore';
import Promise from 'bluebird';
/**
@ -118,9 +118,7 @@ export default class MultiInviter {
let doInvite;
if (this.groupId !== null) {
doInvite = GroupStoreCache
.getGroupStore(this.groupId)
.inviteUserToGroup(addr);
doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
} else {
doInvite = inviteToRoom(this.roomId, addr);
}

View file

@ -0,0 +1,378 @@
/*
Copyright 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import expect from 'expect';
import Promise from 'bluebird';
import MockHttpBackend from 'matrix-mock-request';
import MatrixClientPeg from '../../../src/MatrixClientPeg';
import sdk from 'matrix-react-sdk';
import Matrix from 'matrix-js-sdk';
import * as TestUtils from 'test-utils';
const GroupView = sdk.getComponent('structures.GroupView');
const WrappedGroupView = TestUtils.wrapInMatrixClientContext(GroupView);
const Spinner = sdk.getComponent('elements.Spinner');
/**
* Call fn before calling componentDidUpdate on a react component instance, inst.
* @param {React.Component} inst an instance of a React component.
* @returns {Promise} promise that resolves when componentDidUpdate is called on
* given component instance.
*/
function waitForUpdate(inst) {
return new Promise((resolve, reject) => {
const cdu = inst.componentDidUpdate;
inst.componentDidUpdate = (prevProps, prevState, snapshot) => {
resolve();
if (cdu) cdu(prevProps, prevState, snapshot);
inst.componentDidUpdate = cdu;
};
});
}
describe('GroupView', function() {
let root;
let rootElement;
let httpBackend;
let summaryResponse;
let summaryResponseWithComplicatedLongDesc;
let summaryResponseWithNoLongDesc;
let summaryResponseWithBadImg;
let groupId;
let groupIdEncoded;
// Summary response fields
const user = {
is_privileged: true, // can edit the group
is_public: true, // appear as a member to non-members
is_publicised: true, // display flair
};
const usersSection = {
roles: {},
total_user_count_estimate: 0,
users: [],
};
const roomsSection = {
categories: {},
rooms: [],
total_room_count_estimate: 0,
};
beforeEach(function() {
TestUtils.beforeEach(this);
httpBackend = new MockHttpBackend();
Matrix.request(httpBackend.requestFn);
MatrixClientPeg.get = () => Matrix.createClient({
baseUrl: 'https://my.home.server',
userId: '@me:here',
accessToken: '123456789',
});
summaryResponse = {
profile: {
avatar_url: "mxc://someavatarurl",
is_openly_joinable: true,
is_public: true,
long_description: "This is a <b>LONG</b> description.",
name: "The name of a community",
short_description: "This is a community",
},
user,
users_section: usersSection,
rooms_section: roomsSection,
};
summaryResponseWithNoLongDesc = {
profile: {
avatar_url: "mxc://someavatarurl",
is_openly_joinable: true,
is_public: true,
long_description: null,
name: "The name of a community",
short_description: "This is a community",
},
user,
users_section: usersSection,
rooms_section: roomsSection,
};
summaryResponseWithComplicatedLongDesc = {
profile: {
avatar_url: "mxc://someavatarurl",
is_openly_joinable: true,
is_public: true,
long_description: `
<h1>This is a more complicated group page</h1>
<p>With paragraphs</p>
<ul>
<li>And lists!</li>
<li>With list items.</li>
</ul>
<p>And also images: <img src="mxc://someimageurl"/></p>`,
name: "The name of a community",
short_description: "This is a community",
},
user,
users_section: usersSection,
rooms_section: roomsSection,
};
summaryResponseWithBadImg = {
profile: {
avatar_url: "mxc://someavatarurl",
is_openly_joinable: true,
is_public: true,
long_description: '<p>Evil image: <img src="http://evilimageurl"/></p>',
name: "The name of a community",
short_description: "This is a community",
},
user,
users_section: usersSection,
rooms_section: roomsSection,
};
groupId = "+" + Math.random().toString(16).slice(2) + ':domain';
groupIdEncoded = encodeURIComponent(groupId);
rootElement = document.createElement('div');
root = ReactDOM.render(<WrappedGroupView groupId={groupId} />, rootElement);
});
afterEach(function() {
ReactDOM.unmountComponentAtNode(rootElement);
});
it('should show a spinner when first displayed', function() {
ReactTestUtils.findRenderedComponentWithType(root, Spinner);
// If we don't respond here, the rate limiting done to ensure a maximum of
// 3 concurrent network requests for GroupStore will block subsequent requests
// in other tests.
//
// This is a good case for doing the rate limiting somewhere other than the module
// scope of GroupStore.js
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
return httpBackend.flush(undefined, undefined, 0);
});
it('should indicate failure after failed /summary', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error');
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(500, {});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a group avatar, name, id and short description after successful /summary', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar'));
const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img');
const avatarImgElement = ReactDOM.findDOMNode(img);
expect(avatarImgElement).toExist();
expect(avatarImgElement.src).toInclude(
'https://my.home.server/_matrix/media/v1/thumbnail/' +
'someavatarurl?width=48&height=48&method=crop',
);
const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name');
const nameElement = ReactDOM.findDOMNode(name);
expect(nameElement).toExist();
expect(nameElement.innerText).toInclude('The name of a community');
expect(nameElement.innerText).toInclude(groupId);
const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc');
const shortDescElement = ReactDOM.findDOMNode(shortDesc);
expect(shortDescElement).toExist();
expect(shortDescElement.innerText).toBe('This is a community');
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a simple long description after successful /summary', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
expect(longDescElement.innerText).toBe('This is a LONG description.');
expect(longDescElement.innerHTML).toBe('<div dir="auto">This is a <b>LONG</b> description.</div>');
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a placeholder if a long description is not set', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
const placeholder = ReactTestUtils
.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder');
const placeholderElement = ReactDOM.findDOMNode(placeholder);
expect(placeholderElement).toExist();
});
httpBackend
.when('GET', '/groups/' + groupIdEncoded + '/summary')
.respond(200, summaryResponseWithNoLongDesc);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a complicated long description after successful /summary', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
expect(longDescElement.innerHTML).toInclude('<h1>This is a more complicated group page</h1>');
expect(longDescElement.innerHTML).toInclude('<p>With paragraphs</p>');
expect(longDescElement.innerHTML).toInclude('<ul>');
expect(longDescElement.innerHTML).toInclude('<li>And lists!</li>');
const imgSrc = "https://my.home.server/_matrix/media/v1/thumbnail/someimageurl?width=800&amp;height=600";
expect(longDescElement.innerHTML).toInclude('<img src="' + imgSrc + '">');
});
httpBackend
.when('GET', '/groups/' + groupIdEncoded + '/summary')
.respond(200, summaryResponseWithComplicatedLongDesc);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should disallow images with non-mxc URLs', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
// If this fails, the URL could be in an img `src`, which is what we care about but
// there's no harm in keeping this simple and checking the entire HTML string.
expect(longDescElement.innerHTML).toExclude('evilimageurl');
});
httpBackend
.when('GET', '/groups/' + groupIdEncoded + '/summary')
.respond(200, summaryResponseWithBadImg);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
expect(roomDetailListElement).toExist();
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() {
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
const prom = waitForUpdate(groupView).then(() => {
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
expect(roomDetailListElement).toExist();
const roomDetailListRoomName = ReactTestUtils.findRenderedDOMComponentWithClass(
root,
'mx_RoomDirectory_name',
);
const roomDetailListRoomNameElement = ReactDOM.findDOMNode(roomDetailListRoomName);
expect(roomDetailListRoomNameElement).toExist();
expect(roomDetailListRoomNameElement.innerText).toEqual('Some room name');
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [{
avatar_url: "mxc://someroomavatarurl",
canonical_alias: "#somealias:domain",
guest_can_join: true,
is_public: true,
name: "Some room name",
num_joined_members: 123,
room_id: "!someroomid",
topic: "some topic",
world_readable: true,
}] });
httpBackend.flush(undefined, undefined, 0);
return prom;
});
});

View file

@ -92,6 +92,7 @@ export function createTestClient() {
content: {},
});
},
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
setAccountData: sinon.stub(),
sendTyping: sinon.stub().returns(Promise.resolve({})),
sendMessage: () => Promise.resolve({}),