Merge remote-tracking branch 'upstream/develop' into task/settings-ts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-09-21 19:35:42 +02:00
commit 7f8c0e99ea
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
175 changed files with 3015 additions and 16606 deletions

View file

@ -34,18 +34,43 @@ limitations under the License.
transition: opacity 300ms ease; transition: opacity 300ms ease;
} }
@keyframes mx--anim-pulse { @keyframes mx--anim-pulse {
0% { opacity: 1; } 0% { opacity: 1; }
50% { opacity: 0.7; } 50% { opacity: 0.7; }
100% { opacity: 1; } 100% { opacity: 1; }
} }
@keyframes mx_Dialog_lightbox_background_keyframes {
from {
opacity: 0;
}
to {
opacity: $lightbox-background-bg-opacity;
}
}
@keyframes mx_ImageView_panel_keyframes {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
@keyframes mx--anim-pulse { @keyframes mx--anim-pulse {
// Override all keyframes in reduced-motion // Override all keyframes in reduced-motion
} }
@keyframes mx_Dialog_lightbox_background_keyframes {
// Override all keyframes in reduced-motion
}
@keyframes mx_ImageView_panel_keyframes {
// Override all keyframes in reduced-motion
}
.mx_rtg--fade-enter-active { .mx_rtg--fade-enter-active {
transition: none; transition: none;
} }

View file

@ -318,6 +318,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_lightbox .mx_Dialog_background { .mx_Dialog_lightbox .mx_Dialog_background {
opacity: $lightbox-background-bg-opacity; opacity: $lightbox-background-bg-opacity;
background-color: $lightbox-background-bg-color; background-color: $lightbox-background-bg-color;
animation-name: mx_Dialog_lightbox_background_keyframes;
animation-duration: 300ms;
} }
.mx_Dialog_lightbox .mx_Dialog { .mx_Dialog_lightbox .mx_Dialog {

View file

@ -183,3 +183,40 @@ limitations under the License.
padding: 0; padding: 0;
} }
} }
@media screen and (max-width: 700px) {
.mx_RoomDirectory_roomMemberCount {
padding: 0px;
}
.mx_AccessibleButton_kind_secondary {
padding: 0px !important;
}
.mx_RoomDirectory_join {
margin-left: 0px;
}
.mx_RoomDirectory_alias {
margin-top: 10px;
margin-bottom: 10px;
}
.mx_RoomDirectory_roomDescription {
padding-bottom: 0px;
}
.mx_RoomDirectory_name {
margin-bottom: 5px;
}
.mx_RoomDirectory_roomAvatar {
margin-top: 10px;
}
.mx_RoomDirectory_table {
grid-template-columns: auto;
row-gap: 14px;
margin-top: 5px;
}
}

View file

@ -18,6 +18,10 @@ $button-size: 32px;
$icon-size: 22px; $icon-size: 22px;
$button-gap: 24px; $button-gap: 24px;
:root {
--image-view-panel-height: 68px;
}
.mx_ImageView { .mx_ImageView {
display: flex; display: flex;
width: 100%; width: 100%;
@ -36,14 +40,24 @@ $button-gap: 24px;
.mx_ImageView_image { .mx_ImageView_image {
flex-shrink: 0; flex-shrink: 0;
&.mx_ImageView_image_animating {
transition: transform 200ms ease 0s;
}
&.mx_ImageView_image_animatingLoading {
transition: transform 300ms ease 0s;
}
} }
.mx_ImageView_panel { .mx_ImageView_panel {
width: 100%; width: 100%;
height: 68px; height: var(--image-view-panel-height);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
animation-name: mx_ImageView_panel_keyframes;
animation-duration: 300ms;
} }
.mx_ImageView_info_wrapper { .mx_ImageView_info_wrapper {
@ -124,3 +138,13 @@ $button-gap: 24px;
mask-size: 40%; mask-size: 40%;
} }
} }
@media (prefers-reduced-motion) {
.mx_ImageView_image_animating {
transition: none !important;
}
.mx_ImageView_image_animatingLoading {
transition: none !important;
}
}

View file

@ -20,6 +20,8 @@ import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import { IDialogProps } from "./components/views/dialogs/IDialogProps"; import { IDialogProps } from "./components/views/dialogs/IDialogProps";
import { logger } from "matrix-js-sdk/src/logger";
type AsyncImport<T> = { default: T }; type AsyncImport<T> = { default: T };
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
@ -47,7 +49,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
componentDidMount() { componentDidMount() {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148 // https://github.com/vector-im/element-web/issues/3148
console.log('Starting load of AsyncWrapper for modal'); logger.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => { this.props.prom.then((result) => {
if (this.unmounted) return; if (this.unmounted) return;

View file

@ -286,9 +286,9 @@ export default class CallHandler extends EventEmitter {
dis.dispatch({ action: Action.VirtualRoomSupportUpdated }); dis.dispatch({ action: Action.VirtualRoomSupportUpdated });
} catch (e) { } catch (e) {
if (maxTries === 1) { if (maxTries === 1) {
console.log("Failed to check for protocol support and no retries remain: assuming no support", e); logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
} else { } else {
console.log("Failed to check for protocol support: will retry", e); logger.log("Failed to check for protocol support: will retry", e);
this.pstnSupportCheckTimer = setTimeout(() => { this.pstnSupportCheckTimer = setTimeout(() => {
this.checkProtocols(maxTries - 1); this.checkProtocols(maxTries - 1);
}, 10000); }, 10000);
@ -399,7 +399,7 @@ export default class CallHandler extends EventEmitter {
// or chrome doesn't think so and is denying the request. Not sure what // or chrome doesn't think so and is denying the request. Not sure what
// we can really do here... // we can really do here...
// https://github.com/vector-im/element-web/issues/7657 // https://github.com/vector-im/element-web/issues/7657
console.log("Unable to play audio clip", e); logger.log("Unable to play audio clip", e);
} }
}; };
if (this.audioPromises.has(audioId)) { if (this.audioPromises.has(audioId)) {
@ -477,7 +477,7 @@ export default class CallHandler extends EventEmitter {
call.on(CallEvent.Replaced, (newCall: MatrixCall) => { call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); logger.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
if (call.state === CallState.Ringing) { if (call.state === CallState.Ringing) {
this.pause(AudioID.Ring); this.pause(AudioID.Ring);
@ -493,7 +493,7 @@ export default class CallHandler extends EventEmitter {
call.on(CallEvent.AssertedIdentityChanged, async () => { call.on(CallEvent.AssertedIdentityChanged, async () => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
const newAssertedIdentity = call.getRemoteAssertedIdentity().id; const newAssertedIdentity = call.getRemoteAssertedIdentity().id;
let newNativeAssertedIdentity = newAssertedIdentity; let newNativeAssertedIdentity = newAssertedIdentity;
@ -503,7 +503,7 @@ export default class CallHandler extends EventEmitter {
newNativeAssertedIdentity = response[0].userid; newNativeAssertedIdentity = response[0].userid;
} }
} }
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
if (newNativeAssertedIdentity) { if (newNativeAssertedIdentity) {
this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity;
@ -516,11 +516,11 @@ export default class CallHandler extends EventEmitter {
await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity);
const newMappedRoomId = this.roomIdForCall(call); const newMappedRoomId = this.roomIdForCall(call);
console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); logger.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
if (newMappedRoomId !== mappedRoomId) { if (newMappedRoomId !== mappedRoomId) {
this.removeCallForRoom(mappedRoomId); this.removeCallForRoom(mappedRoomId);
mappedRoomId = newMappedRoomId; mappedRoomId = newMappedRoomId;
console.log("Moving call to room " + mappedRoomId); logger.log("Moving call to room " + mappedRoomId);
this.addCallForRoom(mappedRoomId, call, true); this.addCallForRoom(mappedRoomId, call, true);
} }
} }
@ -656,7 +656,7 @@ export default class CallHandler extends EventEmitter {
private setCallState(call: MatrixCall, status: CallState) { private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
console.log( logger.log(
`Call state in ${mappedRoomId} changed to ${status}`, `Call state in ${mappedRoomId} changed to ${status}`,
); );
@ -681,7 +681,7 @@ export default class CallHandler extends EventEmitter {
} }
private removeCallForRoom(roomId: string) { private removeCallForRoom(roomId: string) {
console.log("Removing call for room ", roomId); logger.log("Removing call for room ", roomId);
this.calls.delete(roomId); this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
} }
@ -752,7 +752,7 @@ export default class CallHandler extends EventEmitter {
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); logger.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
const call = MatrixClientPeg.get().createCall(mappedRoomId); const call = MatrixClientPeg.get().createCall(mappedRoomId);
try { try {
@ -862,7 +862,7 @@ export default class CallHandler extends EventEmitter {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) { if (this.getCallForRoom(mappedRoomId)) {
console.log( logger.log(
"Got incoming call for room " + mappedRoomId + "Got incoming call for room " + mappedRoomId +
" but there's already a call for this room: ignoring", " but there's already a call for this room: ignoring",
); );
@ -966,7 +966,7 @@ export default class CallHandler extends EventEmitter {
const nativeLookupResults = await this.sipNativeLookup(userId); const nativeLookupResults = await this.sipNativeLookup(userId);
const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success;
nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId;
console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); logger.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId);
} else { } else {
nativeUserId = userId; nativeUserId = userId;
} }
@ -1014,7 +1014,7 @@ export default class CallHandler extends EventEmitter {
try { try {
await call.transfer(destination); await call.transfer(destination);
} catch (e) { } catch (e) {
console.log("Failed to transfer call", e); logger.log("Failed to transfer call", e);
Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, { Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, {
title: _t('Transfer Failed'), title: _t('Transfer Failed'),
description: _t('Failed to transfer call'), description: _t('Failed to transfer call'),
@ -1104,7 +1104,7 @@ export default class CallHandler extends EventEmitter {
); );
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => { WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
console.log('Jitsi widget added'); logger.log('Jitsi widget added');
}).catch((e) => { }).catch((e) => {
if (e.errcode === 'M_FORBIDDEN') { if (e.errcode === 'M_FORBIDDEN') {
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
@ -1152,11 +1152,11 @@ export default class CallHandler extends EventEmitter {
private addCallForRoom(roomId: string, call: MatrixCall, changedRooms = false): void { private addCallForRoom(roomId: string, call: MatrixCall, changedRooms = false): void {
if (this.calls.has(roomId)) { if (this.calls.has(roomId)) {
console.log(`Couldn't add call to room ${roomId}: already have a call for this room`); logger.log(`Couldn't add call to room ${roomId}: already have a call for this room`);
throw new Error("Already have a call for room " + roomId); throw new Error("Already have a call for room " + roomId);
} }
console.log("setting call for room " + roomId); logger.log("setting call for room " + roomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
// Should we always emit CallsChanged too? // Should we always emit CallsChanged too?

View file

@ -42,6 +42,8 @@ import { BlurhashEncoder } from "./BlurhashEncoder";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
import { logger } from "matrix-js-sdk/src/logger";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -678,13 +680,13 @@ export default class ContentMessages {
private ensureMediaConfigFetched(matrixClient: MatrixClient) { private ensureMediaConfigFetched(matrixClient: MatrixClient) {
if (this.mediaConfig !== null) return; if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching"); logger.log("[Media Config] Fetching");
return matrixClient.getMediaConfig().then((config) => { return matrixClient.getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config); logger.log("[Media Config] Fetched config:", config);
return config; return config;
}).catch(() => { }).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits). // Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads."); logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {}; return {};
}).then((config) => { }).then((config) => {
this.mediaConfig = config; this.mediaConfig = config;

View file

@ -536,7 +536,7 @@ export default class CountlyAnalytics {
// sanitize the error from identifiers // sanitize the error from identifiers
error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => { error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => {
return glyph + await hashHex(substring.substring(1)); return glyph + (await hashHex(substring.substring(1)));
}); });
const metrics = this.getMetrics(); const metrics = this.getMetrics();

View file

@ -35,6 +35,8 @@ import { isLoggedIn } from './components/structures/MatrixChat';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ActionPayload } from "./dispatcher/payloads"; import { ActionPayload } from "./dispatcher/payloads";
import { logger } from "matrix-js-sdk/src/logger";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
export default class DeviceListener { export default class DeviceListener {
@ -100,7 +102,7 @@ export default class DeviceListener {
* @param {String[]} deviceIds List of device IDs to dismiss notifications for * @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/ */
async dismissUnverifiedSessions(deviceIds: Iterable<string>) { async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
console.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(',')); logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(','));
for (const d of deviceIds) { for (const d of deviceIds) {
this.dismissed.add(d); this.dismissed.add(d);
} }
@ -211,7 +213,7 @@ export default class DeviceListener {
private async recheck() { private async recheck() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; if (!(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))) return;
if (!cli.isCryptoEnabled()) return; if (!cli.isCryptoEnabled()) return;
// don't recheck until the initial sync is complete: lots of account data events will fire // don't recheck until the initial sync is complete: lots of account data events will fire
@ -286,8 +288,8 @@ export default class DeviceListener {
} }
} }
console.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(',')); logger.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(','));
console.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); logger.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
// Display or hide the batch toast for old unverified sessions // Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) { if (oldUnverifiedDeviceIds.size > 0) {

View file

@ -29,6 +29,8 @@ import {
} from './utils/IdentityServerUtils'; } from './utils/IdentityServerUtils';
import { abbreviateUrl } from './utils/UrlUtils'; import { abbreviateUrl } from './utils/UrlUtils';
import { logger } from "matrix-js-sdk/src/logger";
export class AbortedIdentityActionError extends Error {} export class AbortedIdentityActionError extends Error {}
export default class IdentityAuthClient { export default class IdentityAuthClient {
@ -127,7 +129,7 @@ export default class IdentityAuthClient {
await this._matrixClient.getIdentityAccount(token); await this._matrixClient.getIdentityAccount(token);
} catch (e) { } catch (e) {
if (e.errcode === "M_TERMS_NOT_SIGNED") { if (e.errcode === "M_TERMS_NOT_SIGNED") {
console.log("Identity server requires new terms to be agreed to"); logger.log("Identity server requires new terms to be agreed to");
await startTermsFlow([new Service( await startTermsFlow([new Service(
SERVICE_TYPES.IS, SERVICE_TYPES.IS,
identityServerUrl, identityServerUrl,
@ -141,7 +143,7 @@ export default class IdentityAuthClient {
if ( if (
!this.tempClient && !this.tempClient &&
!doesAccountDataHaveIdentityServer() && !doesAccountDataHaveIdentityServer() &&
!await doesIdentityServerHaveTerms(identityServerUrl) !(await doesIdentityServerHaveTerms(identityServerUrl))
) { ) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',

View file

@ -58,6 +58,8 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis
import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog";
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
import { logger } from "matrix-js-sdk/src/logger";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -118,7 +120,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token fragmentQueryParams.guest_access_token
) { ) {
console.log("Using guest access credentials"); logger.log("Using guest access credentials");
return doSetLoggedIn({ return doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id as string, userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token as string, accessToken: fragmentQueryParams.guest_access_token as string,
@ -204,7 +206,7 @@ export function attemptTokenLogin(
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
).then(function(creds) { ).then(function(creds) {
console.log("Logged in with token"); logger.log("Logged in with token");
return clearStorage().then(async () => { return clearStorage().then(async () => {
await persistCredentials(creds); await persistCredentials(creds);
// remember that we just logged in // remember that we just logged in
@ -273,7 +275,7 @@ function registerAsGuest(
isUrl: string, isUrl: string,
defaultDeviceDisplayName: string, defaultDeviceDisplayName: string,
): Promise<boolean> { ): Promise<boolean> {
console.log(`Doing guest login on ${hsUrl}`); logger.log(`Doing guest login on ${hsUrl}`);
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
const client = createClient({ const client = createClient({
@ -285,7 +287,7 @@ function registerAsGuest(
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
}).then((creds) => { }).then((creds) => {
console.log(`Registered as guest: ${creds.user_id}`); logger.log(`Registered as guest: ${creds.user_id}`);
return doSetLoggedIn({ return doSetLoggedIn({
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id, deviceId: creds.device_id,
@ -411,27 +413,27 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
if (accessToken && userId && hsUrl) { if (accessToken && userId && hsUrl) {
if (ignoreGuest && isGuest) { if (ignoreGuest && isGuest) {
console.log("Ignoring stored guest account: " + userId); logger.log("Ignoring stored guest account: " + userId);
return false; return false;
} }
let decryptedAccessToken = accessToken; let decryptedAccessToken = accessToken;
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
if (pickleKey) { if (pickleKey) {
console.log("Got pickle key"); logger.log("Got pickle key");
if (typeof accessToken !== "string") { if (typeof accessToken !== "string") {
const encrKey = await pickleKeyToAesKey(pickleKey); const encrKey = await pickleKeyToAesKey(pickleKey);
decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token");
encrKey.fill(0); encrKey.fill(0);
} }
} else { } else {
console.log("No pickle key available"); logger.log("No pickle key available");
} }
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true"; const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
sessionStorage.removeItem("mx_fresh_login"); sessionStorage.removeItem("mx_fresh_login");
console.log(`Restoring session for ${userId}`); logger.log(`Restoring session for ${userId}`);
await doSetLoggedIn({ await doSetLoggedIn({
userId: userId, userId: userId,
deviceId: deviceId, deviceId: deviceId,
@ -444,7 +446,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
}, false); }, false);
return true; return true;
} else { } else {
console.log("No previous session found."); logger.log("No previous session found.");
return false; return false;
} }
} }
@ -488,9 +490,9 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
: null; : null;
if (pickleKey) { if (pickleKey) {
console.log("Created pickle key"); logger.log("Created pickle key");
} else { } else {
console.log("Pickle key not created"); logger.log("Pickle key not created");
} }
return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true); return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true);
@ -544,7 +546,7 @@ async function doSetLoggedIn(
const softLogout = isSoftLogout(); const softLogout = isSoftLogout();
console.log( logger.log(
"setLoggedIn: mxid: " + credentials.userId + "setLoggedIn: mxid: " + credentials.userId +
" deviceId: " + credentials.deviceId + " deviceId: " + credentials.deviceId +
" guest: " + credentials.guest + " guest: " + credentials.guest +
@ -689,7 +691,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
SecurityCustomisations.persistCredentials?.(credentials); SecurityCustomisations.persistCredentials?.(credentials);
console.log(`Session persisted for ${credentials.userId}`); logger.log(`Session persisted for ${credentials.userId}`);
} }
let _isLoggingOut = false; let _isLoggingOut = false;
@ -726,7 +728,7 @@ export function logout(): void {
// token still valid, but we should fix this by having access // token still valid, but we should fix this by having access
// tokens expire (and if you really think you've been compromised, // tokens expire (and if you really think you've been compromised,
// change your password). // change your password).
console.log("Failed to call logout API: token will not be invalidated"); logger.log("Failed to call logout API: token will not be invalidated");
onLoggedOut(); onLoggedOut();
}, },
); );
@ -742,7 +744,7 @@ export function softLogout(): void {
// Dev note: please keep this log line around. It can be useful for track down // Dev note: please keep this log line around. It can be useful for track down
// random clients stopping in the middle of the logs. // random clients stopping in the middle of the logs.
console.log("Soft logout initiated"); logger.log("Soft logout initiated");
_isLoggingOut = true; // to avoid repeated flags _isLoggingOut = true; // to avoid repeated flags
// Ensure that we dispatch a view change **before** stopping the client so // Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes // so that React components unmount first. This avoids React soft crashes
@ -768,7 +770,7 @@ export function isLoggingOut(): boolean {
* syncing the client. * syncing the client.
*/ */
async function startMatrixClient(startSyncing = true): Promise<void> { async function startMatrixClient(startSyncing = true): Promise<void> {
console.log(`Lifecycle: Starting MatrixClient`); logger.log(`Lifecycle: Starting MatrixClient`);
// dispatch this before starting the matrix client: it's used // dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have // to add listeners for the 'sync' event so otherwise we'd have

View file

@ -21,6 +21,8 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { IMatrixClientCreds } from "./MatrixClientPeg"; import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
interface ILoginOptions { interface ILoginOptions {
defaultDeviceDisplayName?: string; defaultDeviceDisplayName?: string;
} }
@ -166,7 +168,7 @@ export default class Login {
return sendLoginRequest( return sendLoginRequest(
this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams, this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
).catch((fallbackError) => { ).catch((fallbackError) => {
console.log("fallback HS login failed", fallbackError); logger.log("fallback HS login failed", fallbackError);
// throw the original error // throw the original error
throw originalError; throw originalError;
}); });
@ -184,7 +186,7 @@ export default class Login {
} }
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
console.log("Login failed", error); logger.log("Login failed", error);
throw error; throw error;
}); });
} }
@ -218,12 +220,12 @@ export async function sendLoginRequest(
if (wellknown) { if (wellknown) {
if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) {
hsUrl = wellknown["m.homeserver"]["base_url"]; hsUrl = wellknown["m.homeserver"]["base_url"];
console.log(`Overrode homeserver setting with ${hsUrl} from login response`); logger.log(`Overrode homeserver setting with ${hsUrl} from login response`);
} }
if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) {
// TODO: should we prompt here? // TODO: should we prompt here?
isUrl = wellknown["m.identity_server"]["base_url"]; isUrl = wellknown["m.identity_server"]["base_url"];
console.log(`Overrode IS setting with ${isUrl} from login response`); logger.log(`Overrode IS setting with ${isUrl} from login response`);
} }
} }

View file

@ -36,6 +36,8 @@ import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } fro
import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string; homeserverUrl: string;
identityServerUrl: string; identityServerUrl: string;
@ -166,7 +168,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
for (const dbType of ['indexeddb', 'memory']) { for (const dbType of ['indexeddb', 'memory']) {
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
console.log("MatrixClientPeg: waiting for MatrixClient store to initialise"); logger.log("MatrixClientPeg: waiting for MatrixClient store to initialise");
await promise; await promise;
break; break;
} catch (err) { } catch (err) {
@ -225,9 +227,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
public async start(): Promise<any> { public async start(): Promise<any> {
const opts = await this.assign(); const opts = await this.assign();
console.log(`MatrixClientPeg: really starting MatrixClient`); logger.log(`MatrixClientPeg: really starting MatrixClient`);
await this.get().startClient(opts); await this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`); logger.log(`MatrixClientPeg: MatrixClient started`);
} }
public getCredentials(): IMatrixClientCreds { public getCredentials(): IMatrixClientCreds {

View file

@ -38,6 +38,8 @@ import UserActivity from "./UserActivity";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { logger } from "matrix-js-sdk/src/logger";
/* /*
* Dispatches: * Dispatches:
* { * {
@ -160,7 +162,7 @@ export const Notifier = {
_playAudioNotification: async function(ev: MatrixEvent, room: Room) { _playAudioNotification: async function(ev: MatrixEvent, room: Room) {
const sound = this.getSoundForRoom(room.roomId); const sound = this.getSoundForRoom(room.roomId);
console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); logger.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`);
try { try {
const selector = const selector =

View file

@ -21,6 +21,8 @@ import SettingsStore from './settings/SettingsStore';
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
/* Posthog analytics tracking. /* Posthog analytics tracking.
* *
* Anonymity behaviour is as follows: * Anonymity behaviour is as follows:
@ -175,7 +177,7 @@ export class PosthogAnalytics {
// $redacted_current_url is injected by this class earlier in capture(), as its generation // $redacted_current_url is injected by this class earlier in capture(), as its generation
// is async and can't be done in this non-async callback. // is async and can't be done in this non-async callback.
if (!properties['$redacted_current_url']) { if (!properties['$redacted_current_url']) {
console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely"); logger.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely");
} }
properties['$current_url'] = properties['$redacted_current_url']; properties['$current_url'] = properties['$redacted_current_url'];
delete properties['$redacted_current_url']; delete properties['$redacted_current_url'];
@ -291,7 +293,7 @@ export class PosthogAnalytics {
} catch (e) { } catch (e) {
// The above could fail due to network requests, but not essential to starting the application, // The above could fail due to network requests, but not essential to starting the application,
// so swallow it. // so swallow it.
console.log("Unable to identify user for tracking" + e.toString()); logger.log("Unable to identify user for tracking" + e.toString());
} }
} }
} }

View file

@ -20,6 +20,8 @@ import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { logger } from "matrix-js-sdk/src/logger";
export default class Resend { export default class Resend {
static resendUnsentEvents(room: Room): Promise<void[]> { static resendUnsentEvents(room: Room): Promise<void[]> {
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) { return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
@ -47,7 +49,7 @@ export default class Resend {
}, function(err: Error) { }, function(err: Error) {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148 // https://github.com/vector-im/element-web/issues/3148
console.log('Resend got send failure: ' + err.name + '(' + err + ')'); logger.log('Resend got send failure: ' + err.name + '(' + err + ')');
}); });
} }

View file

@ -25,6 +25,8 @@ import { WidgetType } from "./widgets/WidgetType";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
@ -136,7 +138,7 @@ export default class ScalarAuthClient {
return token; return token;
}).catch((e) => { }).catch((e) => {
if (e instanceof TermsNotSignedError) { if (e instanceof TermsNotSignedError) {
console.log("Integration manager requires new terms to be agreed to"); logger.log("Integration manager requires new terms to be agreed to");
// The terms endpoints are new and so live on standard _matrix prefixes, // The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the // but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk // path from the base URL before passing it to the js-sdk

View file

@ -245,6 +245,8 @@ import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { WidgetType } from "./widgets/WidgetType"; import { WidgetType } from "./widgets/WidgetType";
import { objectClone } from "./utils/objects"; import { objectClone } from "./utils/objects";
import { logger } from "matrix-js-sdk/src/logger";
function sendResponse(event, res) { function sendResponse(event, res) {
const data = objectClone(event.data); const data = objectClone(event.data);
data.response = res; data.response = res;
@ -266,7 +268,7 @@ function sendError(event, msg, nestedError) {
} }
function inviteUser(event, roomId, userId) { function inviteUser(event, roomId, userId) {
console.log(`Received request to invite ${userId} into room ${roomId}`); logger.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -400,7 +402,7 @@ function setPlumbingState(event, roomId, status) {
if (typeof status !== 'string') { if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string'); throw new Error('Plumbing state status should be a string');
} }
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); logger.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -416,7 +418,7 @@ function setPlumbingState(event, roomId, status) {
} }
function setBotOptions(event, roomId, userId) { function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`); logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -437,7 +439,7 @@ function setBotPower(event, roomId, userId, level) {
return; return;
} }
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); logger.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -463,17 +465,17 @@ function setBotPower(event, roomId, userId, level) {
} }
function getMembershipState(event, roomId, userId) { function getMembershipState(event, roomId, userId) {
console.log(`membership_state of ${userId} in room ${roomId} requested.`); logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.member", userId); returnStateEvent(event, roomId, "m.room.member", userId);
} }
function getJoinRules(event, roomId) { function getJoinRules(event, roomId) {
console.log(`join_rules of ${roomId} requested.`); logger.log(`join_rules of ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.join_rules", ""); returnStateEvent(event, roomId, "m.room.join_rules", "");
} }
function botOptions(event, roomId, userId) { function botOptions(event, roomId, userId) {
console.log(`bot_options of ${userId} in room ${roomId} requested.`); logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId); returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
} }

View file

@ -31,6 +31,8 @@ import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { logger } from "matrix-js-sdk/src/logger";
// This stores the secret storage private keys in memory for the JS SDK. This is // This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times // only meant to act as a cache to avoid prompting the user multiple times
// during the same single operation. Use `accessSecretStorage` below to scope a // during the same single operation. Use `accessSecretStorage` below to scope a
@ -136,7 +138,7 @@ async function getSecretStorageKey(
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Using key from security customisations (secret storage)"); logger.log("Using key from security customisations (secret storage)");
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations); cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
return [keyId, keyFromCustomisations]; return [keyId, keyFromCustomisations];
} }
@ -186,7 +188,7 @@ export async function getDehydrationKey(
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Using key from security customisations (dehydration)"); logger.log("Using key from security customisations (dehydration)");
return keyFromCustomisations; return keyFromCustomisations;
} }
@ -248,13 +250,13 @@ async function onSecretRequested(
name: string, name: string,
deviceTrust: DeviceTrustLevel, deviceTrust: DeviceTrustLevel,
): Promise<string> { ): Promise<string> {
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); logger.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (userId !== client.getUserId()) { if (userId !== client.getUserId()) {
return; return;
} }
if (!deviceTrust || !deviceTrust.isVerified()) { if (!deviceTrust || !deviceTrust.isVerified()) {
console.log(`Ignoring secret request from untrusted device ${deviceId}`); logger.log(`Ignoring secret request from untrusted device ${deviceId}`);
return; return;
} }
if ( if (
@ -267,7 +269,7 @@ async function onSecretRequested(
const keyId = name.replace("m.cross_signing.", ""); const keyId = name.replace("m.cross_signing.", "");
const key = await callbacks.getCrossSigningKeyCache(keyId); const key = await callbacks.getCrossSigningKeyCache(keyId);
if (!key) { if (!key) {
console.log( logger.log(
`${keyId} requested by ${deviceId}, but not found in cache`, `${keyId} requested by ${deviceId}, but not found in cache`,
); );
} }
@ -275,7 +277,7 @@ async function onSecretRequested(
} else if (name === "m.megolm_backup.v1") { } else if (name === "m.megolm_backup.v1") {
const key = await client.crypto.getSessionBackupPrivateKey(); const key = await client.crypto.getSessionBackupPrivateKey();
if (!key) { if (!key) {
console.log( logger.log(
`session backup key requested by ${deviceId}, but not found in cache`, `session backup key requested by ${deviceId}, but not found in cache`,
); );
} }
@ -329,7 +331,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
secretStorageBeingAccessed = true; secretStorageBeingAccessed = true;
try { try {
if (!await cli.hasSecretStorageKey() || forceReset) { if (!(await cli.hasSecretStorageKey()) || forceReset) {
// This dialog calls bootstrap itself after guiding the user through // This dialog calls bootstrap itself after guiding the user through
// passphrase creation. // passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
@ -383,12 +385,12 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) { if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) {
dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase }; dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase };
} }
console.log("Setting dehydration key"); logger.log("Setting dehydration key");
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
} else if (!keyId) { } else if (!keyId) {
console.warn("Not setting dehydration key: no SSSS key found"); console.warn("Not setting dehydration key: no SSSS key found");
} else { } else {
console.log("Not setting dehydration key: feature disabled"); logger.log("Not setting dehydration key: feature disabled");
} }
} }
@ -416,8 +418,8 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(
): Promise<void> { ): Promise<void> {
const key = dehydrationCache.key; const key = dehydrationCache.key;
let restoringBackup = false; let restoringBackup = false;
if (key && await client.isSecretStorageReady()) { if (key && (await client.isSecretStorageReady())) {
console.log("Trying to set up cross-signing using dehydration key"); logger.log("Trying to set up cross-signing using dehydration key");
secretStorageBeingAccessed = true; secretStorageBeingAccessed = true;
nonInteractive = true; nonInteractive = true;
try { try {

View file

@ -55,6 +55,8 @@ import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarn
import InfoDialog from "./components/views/dialogs/InfoDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog";
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
import { logger } from "matrix-js-sdk/src/logger";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
target: HTMLInputElement & EventTarget; target: HTMLInputElement & EventTarget;
@ -291,7 +293,7 @@ export const Commands = [
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId()); const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId());
const content = { const content = {
...ev ? ev.getContent() : { membership: 'join' }, ...(ev ? ev.getContent() : { membership: 'join' }),
displayname: args, displayname: args,
}; };
return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId())); return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId()));
@ -335,7 +337,7 @@ export const Commands = [
if (!url) return; if (!url) return;
const ev = room.currentState.getStateEvents('m.room.member', userId); const ev = room.currentState.getStateEvents('m.room.member', userId);
const content = { const content = {
...ev ? ev.getContent() : { membership: 'join' }, ...(ev ? ev.getContent() : { membership: 'join' }),
avatar_url: url, avatar_url: url,
}; };
return cli.sendStateEvent(roomId, 'm.room.member', content, userId); return cli.sendStateEvent(roomId, 'm.room.member', content, userId);
@ -801,7 +803,7 @@ export const Commands = [
const iframe = embed.childNodes[0] as ChildElement; const iframe = embed.childNodes[0] as ChildElement;
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) { if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
const srcAttr = iframe.attrs.find(a => a.name === 'src'); const srcAttr = iframe.attrs.find(a => a.name === 'src');
console.log("Pulling URL out of iframe (embed code)"); logger.log("Pulling URL out of iframe (embed code)");
widgetUrl = srcAttr.value; widgetUrl = srcAttr.value;
} }
} }
@ -821,7 +823,7 @@ export const Commands = [
// Make the widget a Jitsi widget if it looks like a Jitsi widget // Make the widget a Jitsi widget if it looks like a Jitsi widget
const jitsiData = Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl); const jitsiData = Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl);
if (jitsiData) { if (jitsiData) {
console.log("Making /addwidget widget a Jitsi conference"); logger.log("Making /addwidget widget a Jitsi conference");
type = WidgetType.JITSI; type = WidgetType.JITSI;
name = "Jitsi Conference"; name = "Jitsi Conference";
data = jitsiData; data = jitsiData;

View file

@ -21,6 +21,8 @@ import { MatrixClientPeg } from './MatrixClientPeg';
import * as sdk from '.'; import * as sdk from '.';
import Modal from './Modal'; import Modal from './Modal';
import { logger } from "matrix-js-sdk/src/logger";
export class TermsNotSignedError extends Error {} export class TermsNotSignedError extends Error {}
/** /**
@ -140,11 +142,11 @@ export async function startTermsFlow(
const numAcceptedBeforeAgreement = agreedUrlSet.size; const numAcceptedBeforeAgreement = agreedUrlSet.size;
if (unagreedPoliciesAndServicePairs.length > 0) { if (unagreedPoliciesAndServicePairs.length > 0) {
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
console.log("User has agreed to URLs", newlyAgreedUrls); logger.log("User has agreed to URLs", newlyAgreedUrls);
// Merge with previously agreed URLs // Merge with previously agreed URLs
newlyAgreedUrls.forEach(url => agreedUrlSet.add(url)); newlyAgreedUrls.forEach(url => agreedUrlSet.add(url));
} else { } else {
console.log("User has already agreed to all required policies"); logger.log("User has already agreed to all required policies");
} }
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length // We only ever add to the set of URLs, so if anything has changed then we'd see a different length
@ -188,7 +190,7 @@ export function dialogTermsInteractionCallback(
extraClassNames?: string, extraClassNames?: string,
): Promise<string[]> { ): Promise<string[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs); logger.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures // FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");

View file

@ -20,6 +20,8 @@ import DMRoomMap from "./utils/DMRoomMap";
import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { logger } from "matrix-js-sdk/src/logger";
// Functions for mapping virtual users & rooms. Currently the only lookup // Functions for mapping virtual users & rooms. Currently the only lookup
// is sip virtual: there could be others in the future. // is sip virtual: there could be others in the future.
@ -59,7 +61,7 @@ export default class VoipUserMapper {
public nativeRoomForVirtualRoom(roomId: string): string { public nativeRoomForVirtualRoom(roomId: string): string {
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
if (cachedNativeRoomId) { if (cachedNativeRoomId) {
console.log( logger.log(
"Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache",
); );
return cachedNativeRoomId; return cachedNativeRoomId;
@ -98,7 +100,7 @@ export default class VoipUserMapper {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
if (result.length === 0) { if (result.length === 0) {
return; return;

View file

@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from '../../../../components/views/elements/Field'; import Field from '../../../../components/views/elements/Field';
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (confirmed: boolean) => void;
}
interface IState { interface IState {
eventIndexSize: number; eventIndexSize: number;

View file

@ -34,6 +34,8 @@ import RestoreKeyBackupDialog from "../../../../components/views/dialogs/securit
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security"; import SecurityCustomisations from "../../../../customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1; const PHASE_LOADERROR = 1;
const PHASE_CHOOSE_KEY_PASSPHRASE = 2; const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
@ -122,7 +124,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_getInitialPhase() { _getInitialPhase() {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Created key via customisations, jumping to bootstrap step"); logger.log("Created key via customisations, jumping to bootstrap step");
this._recoveryKey = { this._recoveryKey = {
privateKey: keyFromCustomisations, privateKey: keyFromCustomisations,
}; };
@ -138,7 +140,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = ( const backupSigStatus = (
// we may not have started crypto yet, in which case we definitely don't trust the backup // we may not have started crypto yet, in which case we definitely don't trust the backup
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo))
); );
const { forceReset } = this.props; const { forceReset } = this.props;
@ -165,10 +167,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) { } catch (error) {
if (!error.data || !error.data.flows) { if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!"); logger.log("uploadDeviceSigningKeys advertised no flows!");
return; return;
} }
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
@ -304,7 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
try { try {
if (forceReset) { if (forceReset) {
console.log("Forcing secret storage reset"); logger.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true, setupNewKeyBackup: true,

View file

@ -23,6 +23,8 @@ import { PlaybackClock } from "./PlaybackClock";
import { createAudioContext, decodeOgg } from "./compat"; import { createAudioContext, decodeOgg } from "./compat";
import { clamp } from "../utils/numbers"; import { clamp } from "../utils/numbers";
import { logger } from "matrix-js-sdk/src/logger";
export enum PlaybackState { export enum PlaybackState {
Decoding = "decoding", Decoding = "decoding",
Stopped = "stopped", // no progress on timeline Stopped = "stopped", // no progress on timeline
@ -139,7 +141,7 @@ export class Playback extends EventEmitter implements IDestroyable {
// audio buffer in memory, as that can balloon to far greater than the input buffer's // audio buffer in memory, as that can balloon to far greater than the input buffer's
// byte length. // byte length.
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
console.log("Audio file too large: processing through <audio /> element"); logger.log("Audio file too large: processing through <audio /> element");
this.element = document.createElement("AUDIO") as HTMLAudioElement; this.element = document.createElement("AUDIO") as HTMLAudioElement;
const prom = new Promise((resolve, reject) => { const prom = new Promise((resolve, reject) => {
this.element.onloadeddata = () => resolve(null); this.element.onloadeddata = () => resolve(null);

View file

@ -21,6 +21,8 @@ import decoderWasmPath from 'opus-recorder/dist/decoderWorker.min.wasm';
import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js'; import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js';
import decoderPath from 'opus-recorder/dist/decoderWorker.min.js'; import decoderPath from 'opus-recorder/dist/decoderWorker.min.js';
import { logger } from "matrix-js-sdk/src/logger";
export function createAudioContext(opts?: AudioContextOptions): AudioContext { export function createAudioContext(opts?: AudioContextOptions): AudioContext {
if (window.AudioContext) { if (window.AudioContext) {
return new AudioContext(opts); return new AudioContext(opts);
@ -38,7 +40,7 @@ export function decodeOgg(audioBuffer: ArrayBuffer): Promise<ArrayBuffer> {
// Condensed version of decoder example, using a promise: // Condensed version of decoder example, using a promise:
// https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html // https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html
return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path
console.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake) logger.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake)
const typedArray = new Uint8Array(audioBuffer); const typedArray = new Uint8Array(audioBuffer);
const decoderWorker = new Worker(decoderPath); const decoderWorker = new Worker(decoderPath);
const wavWorker = new Worker(wavEncoderPath); const wavWorker = new Worker(wavEncoderPath);

View file

@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
<AppTile <AppTile
app={app} app={app}
fullWidth fullWidth
show
showMenubar={false} showMenubar={false}
userWidget userWidget
userId={cli.getUserId()} userId={cli.getUserId()}

View file

@ -110,6 +110,8 @@ import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from '../../PosthogAnalytics'; import { PosthogAnalytics } from '../../PosthogAnalytics';
import { initSentry } from "../../sentry"; import { initSentry } from "../../sentry";
import { logger } from "matrix-js-sdk/src/logger";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
// a special initial state which is only used at startup, while we are // a special initial state which is only used at startup, while we are
@ -893,12 +895,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.focusComposer = true; this.focusComposer = true;
if (roomInfo.room_alias) { if (roomInfo.room_alias) {
console.log( logger.log(
`Switching to room alias ${roomInfo.room_alias} at event ` + `Switching to room alias ${roomInfo.room_alias} at event ` +
roomInfo.event_id, roomInfo.event_id,
); );
} else { } else {
console.log(`Switching to room id ${roomInfo.room_id} at event ` + logger.log(`Switching to room id ${roomInfo.room_id} at event ` +
roomInfo.event_id, roomInfo.event_id,
); );
} }
@ -1407,7 +1409,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// such as when laptops unsleep. // such as when laptops unsleep.
// https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568 // https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568
cli.setCanResetTimelineCallback((roomId) => { cli.setCanResetTimelineCallback((roomId) => {
console.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId); logger.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId);
if (roomId !== this.state.currentRoomId) { if (roomId !== this.state.currentRoomId) {
// It is safe to remove events from rooms we are not viewing. // It is safe to remove events from rooms we are not viewing.
return true; return true;

View file

@ -448,7 +448,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
if (mxEv.replyInThread // Checking if the message has a "parentEventId" as we do not
// want to hide the root event of the thread
if (mxEv.replyInThread && mxEv.parentEventId
&& this.props.hideThreadedMessages && this.props.hideThreadedMessages
&& SettingsStore.getValue("feature_thread")) { && SettingsStore.getValue("feature_thread")) {
return false; return false;

View file

@ -91,6 +91,8 @@ import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -98,7 +100,7 @@ const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console); debuglog = logger.log.bind(console);
} }
interface IProps { interface IProps {
@ -380,7 +382,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307 // Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
console.log( logger.log(
'RVS update:', 'RVS update:',
newState.roomId, newState.roomId,
newState.roomAlias, newState.roomAlias,
@ -1399,7 +1401,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// As per the spec, an all rooms search can create this condition, // As per the spec, an all rooms search can create this condition,
// it happens with Seshat but not Synapse. // it happens with Seshat but not Synapse.
// It will make the result count not match the displayed count. // It will make the result count not match the displayed count.
console.log("Hiding search result from an unknown room", roomId); logger.log("Hiding search result from an unknown room", roomId);
continue; continue;
} }

View file

@ -21,6 +21,8 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import { logger } from "matrix-js-sdk/src/logger";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling. // The amount of extra scroll distance to allow prior to unfilling.
@ -38,7 +40,7 @@ const PAGE_SIZE = 400;
let debuglog; let debuglog;
if (DEBUG_SCROLL) { if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console, "ScrollPanel debuglog:"); debuglog = logger.log.bind(console, "ScrollPanel debuglog:");
} else { } else {
debuglog = function() {}; debuglog = function() {};
} }

View file

@ -80,6 +80,8 @@ import Spinner from "../views/elements/Spinner";
import GroupAvatar from "../views/avatars/GroupAvatar"; import GroupAvatar from "../views/avatars/GroupAvatar";
import { useDispatcher } from "../../hooks/useDispatcher"; import { useDispatcher } from "../../hooks/useDispatcher";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
space: Room; space: Room;
justCreatedOpts?: IOpts; justCreatedOpts?: IOpts;
@ -696,7 +698,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error"); const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
if (failedUsers.length > 0) { if (failedUsers.length > 0) {
console.log("Failed to invite users to space: ", result); logger.log("Failed to invite users to space: ", result);
setError(_t("Failed to invite the following users to your space: %(csvUsers)s", { setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
csvUsers: failedUsers.join(", "), csvUsers: failedUsers.join(", "),
})); }));

View file

@ -49,6 +49,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog'; import ErrorDialog from '../views/dialogs/ErrorDialog';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { logger } from "matrix-js-sdk/src/logger";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
const READ_RECEIPT_INTERVAL_MS = 500; const READ_RECEIPT_INTERVAL_MS = 500;
@ -60,7 +62,7 @@ const DEBUG = false;
let debuglog = function(...s: any[]) {}; let debuglog = function(...s: any[]) {};
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console); debuglog = logger.log.bind(console);
} }
interface IProps { interface IProps {
@ -316,7 +318,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const differentEventId = newProps.eventId != this.props.eventId; const differentEventId = newProps.eventId != this.props.eventId;
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId; const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
if (differentEventId || differentHighlightedEventId) { if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId + logger.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")"); " (was " + this.props.eventId + ")");
return this.initTimeline(newProps); return this.initTimeline(newProps);
} }
@ -1098,7 +1100,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// we're in a setState callback, and we know // we're in a setState callback, and we know
// timelineLoading is now false, so render() should have // timelineLoading is now false, so render() should have
// mounted the message panel. // mounted the message panel.
console.log("can't initialise scroll state because " + logger.log("can't initialise scroll state because " +
"messagePanel didn't load"); "messagePanel didn't load");
return; return;
} }

View file

@ -59,6 +59,7 @@ import RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton"; import TooltipButton from "../views/elements/TooltipButton";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
} }
@ -239,7 +240,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038 // TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
// Note: You'll need to uncomment the button too. // Note: You'll need to uncomment the button too.
console.log("TODO: Show archived rooms"); logger.log("TODO: Show archived rooms");
}; };
private onProvideFeedback = (ev: ButtonEvent) => { private onProvideFeedback = (ev: ButtonEvent) => {

View file

@ -38,6 +38,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import { logger } from "matrix-js-sdk/src/logger";
// These are used in several places, and come from the js-sdk's autodiscovery // These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n. // stuff. We define them here so that they'll be picked up by i18n.
_td("Invalid homeserver discovery response"); _td("Invalid homeserver discovery response");
@ -438,7 +440,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
// technically the flow can have multiple steps, but no one does this // technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it. // for login and loginLogic doesn't support it so we can ignore it.
if (!this.stepRendererMap[flow.type]) { if (!this.stepRendererMap[flow.type]) {
console.log("Skipping flow", flow, "due to unsupported login type", flow.type); logger.log("Skipping flow", flow, "due to unsupported login type", flow.type);
return false; return false;
} }
return true; return true;

View file

@ -37,6 +37,8 @@ import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth"; import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner"; import Spinner from "../../views/elements/Spinner";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
defaultDeviceDisplayName: string; defaultDeviceDisplayName: string;
@ -215,7 +217,7 @@ export default class Registration extends React.Component<IProps, IState> {
if (!this.state.doingUIAuth) { if (!this.state.doingUIAuth) {
await this.makeRegisterRequest(null); await this.makeRegisterRequest(null);
// This should never succeed since we specified no auth object. // This should never succeed since we specified no auth object.
console.log("Expecting 401 from register request but got success!"); logger.log("Expecting 401 from register request but got success!");
} }
} catch (e) { } catch (e) {
if (e.httpStatus === 401) { if (e.httpStatus === 401) {
@ -239,7 +241,7 @@ export default class Registration extends React.Component<IProps, IState> {
}); });
} }
} else { } else {
console.log("Unable to query for supported registration methods.", e); logger.log("Unable to query for supported registration methods.", e);
showGenericError(e); showGenericError(e);
} }
} }
@ -330,7 +332,7 @@ export default class Registration extends React.Component<IProps, IState> {
// the user had a separate guest session they didn't actually mean to replace. // the user had a separate guest session they didn't actually mean to replace.
const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner(); const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner();
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) { if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
console.log( logger.log(
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`, `Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
); );
newState.differentLoggedInUserId = sessionOwner; newState.differentLoggedInUserId = sessionOwner;
@ -366,7 +368,7 @@ export default class Registration extends React.Component<IProps, IState> {
const emailPusher = pushers[i]; const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand }; emailPusher.data = { brand: this.props.brand };
matrixClient.setPusher(emailPusher).then(() => { matrixClient.setPusher(emailPusher).then(() => {
console.log("Set email branding to " + this.props.brand); logger.log("Set email branding to " + this.props.brand);
}, (error) => { }, (error) => {
console.error("Couldn't set email branding: " + error); console.error("Couldn't set email branding: " + error);
}); });

View file

@ -28,6 +28,8 @@ import Spinner from '../../views/elements/Spinner';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { logger } from "matrix-js-sdk/src/logger";
function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
return Boolean( return Boolean(
keyInfo.passphrase && keyInfo.passphrase &&
@ -231,7 +233,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
} else if (phase === Phase.Busy || phase === Phase.Loading) { } else if (phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />; return <Spinner />;
} else { } else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`); logger.log(`SetupEncryptionBody: Unknown phase ${phase}`);
} }
} }
} }

View file

@ -32,6 +32,8 @@ import Spinner from "../../views/elements/Spinner";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import { logger } from "matrix-js-sdk/src/logger";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
PASSWORD: 2, PASSWORD: 2,
@ -103,7 +105,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
onFinished: (wipeData) => { onFinished: (wipeData) => {
if (!wipeData) return; if (!wipeData) return;
console.log("Clearing data from soft-logged-out session"); logger.log("Clearing data from soft-logged-out session");
Lifecycle.logout(); Lifecycle.logout();
}, },
}); });

View file

@ -19,6 +19,8 @@ import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
interface ICaptchaFormProps { interface ICaptchaFormProps {
@ -60,7 +62,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
// already loaded // already loaded
this.onCaptchaLoaded(); this.onCaptchaLoaded();
} else { } else {
console.log("Loading recaptcha script..."); logger.log("Loading recaptcha script...");
window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); }; window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');
scriptTag.setAttribute( scriptTag.setAttribute(
@ -109,7 +111,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
} }
private onCaptchaLoaded() { private onCaptchaLoaded() {
console.log("Loaded recaptcha script."); logger.log("Loaded recaptcha script.");
try { try {
this.renderRecaptcha(DIV_ID); this.renderRecaptcha(DIV_ID);
// clear error if re-rendered // clear error if re-rendered

View file

@ -29,6 +29,8 @@ import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field'; import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm"; import CaptchaForm from "./CaptchaForm";
import { logger } from "matrix-js-sdk/src/logger";
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
* for an auth stage. (The intention is that they could also be used for other * for an auth stage. (The intention is that they could also be used for other
@ -555,7 +557,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
} }
} catch (e) { } catch (e) {
this.props.fail(e); this.props.fail(e);
console.log("Failed to submit msisdn token"); logger.log("Failed to submit msisdn token");
} }
}; };

View file

@ -125,14 +125,14 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
setBusy(true); setBusy(true);
// require & validate the space name field // require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) { if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
setBusy(false); setBusy(false);
spaceNameField.current.focus(); spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true }); spaceNameField.current.validate({ allowEmpty: false, focused: true });
return; return;
} }
// validate the space name alias field but do not require it // validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
setBusy(false); setBusy(false);
spaceAliasField.current.focus(); spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true }); spaceAliasField.current.validate({ allowEmpty: true, focused: true });

View file

@ -64,14 +64,14 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
setBusy(true); setBusy(true);
// require & validate the space name field // require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) { if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
spaceNameField.current.focus(); spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true }); spaceNameField.current.validate({ allowEmpty: false, focused: true });
setBusy(false); setBusy(false);
return; return;
} }
// validate the space name alias field but do not require it // validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
spaceAliasField.current.focus(); spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true }); spaceAliasField.current.validate({ allowEmpty: true, focused: true });
setBusy(false); setBusy(false);

View file

@ -23,10 +23,9 @@ import Modal from '../../../Modal';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import QuestionDialog from "./QuestionDialog"; import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (success: boolean) => void;
}
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => { const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;

View file

@ -44,6 +44,8 @@ import { SettingLevel } from '../../../settings/SettingLevel';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import TruncatedList from "../elements/TruncatedList"; import TruncatedList from "../elements/TruncatedList";
import { logger } from "matrix-js-sdk/src/logger";
interface IGenericEditorProps { interface IGenericEditorProps {
onBack: () => void; onBack: () => void;
} }
@ -984,7 +986,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
const parsedExplicit = JSON.parse(this.state.explicitValues); const parsedExplicit = JSON.parse(this.state.explicitValues);
const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues); const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
for (const level of Object.keys(parsedExplicit)) { for (const level of Object.keys(parsedExplicit)) {
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); logger.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
try { try {
const val = parsedExplicit[level]; const val = parsedExplicit[level];
await SettingsStore.setValue(settingId, null, level as SettingLevel, val); await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
@ -994,7 +996,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
} }
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
for (const level of Object.keys(parsedExplicit)) { for (const level of Object.keys(parsedExplicit)) {
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); logger.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
try { try {
const val = parsedExplicitRoom[level]; const val = parsedExplicitRoom[level];
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val); await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);

View file

@ -22,6 +22,8 @@ import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_START = 0; const PHASE_START = 0;
const PHASE_SHOW_SAS = 1; const PHASE_SHOW_SAS = 1;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2; const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
@ -39,7 +41,7 @@ export default class IncomingSasDialog extends React.Component {
let phase = PHASE_START; let phase = PHASE_START;
if (this.props.verifier.cancelled) { if (this.props.verifier.cancelled) {
console.log("Verifier was cancelled in the background."); logger.log("Verifier was cancelled in the background.");
phase = PHASE_CANCELLED; phase = PHASE_CANCELLED;
} }
@ -90,7 +92,7 @@ export default class IncomingSasDialog extends React.Component {
this.props.verifier.verify().then(() => { this.props.verifier.verify().then(() => {
this.setState({ phase: PHASE_VERIFIED }); this.setState({ phase: PHASE_VERIFIED });
}).catch((e) => { }).catch((e) => {
console.log("Verification failed", e); logger.log("Verification failed", e);
}); });
} }

View file

@ -73,6 +73,8 @@ import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -775,7 +777,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
invitedUsers.push(addr); invitedUsers.push(addr);
} }
} }
console.log("Sharing history with", invitedUsers); logger.log("Sharing history with", invitedUsers);
cli.sendSharedHistoryKeys( cli.sendSharedHistoryKeys(
this.props.roomId, invitedUsers, this.props.roomId, invitedUsers,
); );

View file

@ -97,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
definitions={[ definitions={[
{ {
value: RoomsToLeave.None, value: RoomsToLeave.None,
label: _t("Don't leave any"), label: _t("Don't leave any rooms"),
}, { }, {
value: RoomsToLeave.All, value: RoomsToLeave.All,
label: _t("Leave all rooms and spaces"), label: _t("Leave all rooms"),
}, { }, {
value: RoomsToLeave.Specific, value: RoomsToLeave.Specific,
label: _t("Leave specific rooms and spaces"), label: _t("Leave some rooms"),
}, },
]} ]}
/> />
@ -166,11 +166,13 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
> >
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog"> <div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
<p> <p>
{ _t("Are you sure you want to leave <spaceName/>?", {}, { { _t("You are about to leave <spaceName/>.", {}, {
spaceName: () => <b>{ space.name }</b>, spaceName: () => <b>{ space.name }</b>,
}) } }) }
&nbsp; &nbsp;
{ rejoinWarning } { rejoinWarning }
{ rejoinWarning && (<>&nbsp;</>) }
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
</p> </p>
{ spaceChildren.length > 0 && <LeaveRoomsPicker { spaceChildren.length > 0 && <LeaveRoomsPicker

View file

@ -25,6 +25,8 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog'; import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
onFinished: (success: boolean) => void; onFinished: (success: boolean) => void;
} }
@ -68,7 +70,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
backupInfo, backupInfo,
}); });
} catch (e) { } catch (e) {
console.log("Unable to fetch key backup status", e); logger.log("Unable to fetch key backup status", e);
this.setState({ this.setState({
loading: false, loading: false,
error: e, error: e,

View file

@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon, { E2EState } from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
onFinished={onFinished} onFinished={onFinished}
className="mx_UntrustedDeviceDialog" className="mx_UntrustedDeviceDialog"
title={<> title={<>
<E2EIcon status="warning" size={24} hideTooltip={true} /> <E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
{ _t("Not Trusted") } { _t("Not Trusted") }
</>} </>}
> >

View file

@ -25,6 +25,8 @@ import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
widget: Widget; widget: Widget;
widgetKind: WidgetKind; widgetKind: WidgetKind;
@ -55,7 +57,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
private onPermissionSelection(allowed: boolean) { private onPermissionSelection(allowed: boolean) {
if (this.state.rememberSelection) { if (this.state.rememberSelection) {
console.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`); logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
WidgetPermissionStore.instance.setOIDCState( WidgetPermissionStore.instance.setOIDCState(
this.props.widget, this.props.widgetKind, this.props.inRoomId, this.props.widget, this.props.widgetKind, this.props.inRoomId,

View file

@ -28,6 +28,8 @@ import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog'; import InteractiveAuthDialog from '../InteractiveAuthDialog';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
accountPassword?: string; accountPassword?: string;
tokenLogin?: boolean; tokenLogin?: boolean;
@ -77,10 +79,10 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) { } catch (error) {
if (!error.data || !error.data.flows) { if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!"); logger.log("uploadDeviceSigningKeys advertised no flows!");
return; return;
} }
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {

View file

@ -23,6 +23,8 @@ import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager'; import { accessSecretStorage } from '../../../../SecurityManager';
import { logger } from "matrix-js-sdk/src/logger";
const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1; const RESTORE_TYPE_RECOVERYKEY = 1;
const RESTORE_TYPE_SECRET_STORAGE = 2; const RESTORE_TYPE_SECRET_STORAGE = 2;
@ -127,7 +129,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo, recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
loading: false, loading: false,
restoreError: e, restoreError: e,
@ -161,7 +163,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo, recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
loading: false, loading: false,
restoreError: e, restoreError: e,
@ -194,7 +196,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo, recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
restoreError: e, restoreError: e,
loading: false, loading: false,
@ -216,7 +218,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}); });
return true; return true;
} catch (e) { } catch (e) {
console.log("restoreWithCachedKey failed:", e); logger.log("restoreWithCachedKey failed:", e);
return false; return false;
} }
} }
@ -230,7 +232,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion(); const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey(); const has4S = await cli.hasSecretStorageKey();
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored(); const backupKeyStored = has4S && (await cli.isKeyBackupKeyStored());
this.setState({ this.setState({
backupInfo, backupInfo,
backupKeyStored, backupKeyStored,
@ -238,7 +240,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const gotCache = await this._restoreWithCachedKey(backupInfo); const gotCache = await this._restoreWithCachedKey(backupInfo);
if (gotCache) { if (gotCache) {
console.log("RestoreKeyBackupDialog: found cached backup key"); logger.log("RestoreKeyBackupDialog: found cached backup key");
this.setState({ this.setState({
loading: false, loading: false,
}); });
@ -255,7 +257,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loading: false, loading: false,
}); });
} catch (e) { } catch (e) {
console.log("Error loading backup status", e); logger.log("Error loading backup status", e);
this.setState({ this.setState({
loadError: e, loadError: e,
loading: false, loading: false,

View file

@ -19,7 +19,6 @@ limitations under the License.
import url from 'url'; import url from 'url';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -39,33 +38,97 @@ import { MatrixCapabilities } from "matrix-widget-api";
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu"; import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetAvatar from "../avatars/WidgetAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
interface IProps {
app: IApp;
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: Room;
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth?: boolean;
// Optional. If set, renders a smaller view of the widget
miniMode?: boolean;
// UserId of the current user
userId: string;
// UserId of the entity that added / modified the widget
creatorUserId: string;
waitForIframeLoad: boolean;
showMenubar?: boolean;
// Optional onEditClickHandler (overrides default behaviour)
onEditClick?: () => void;
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick?: () => void;
// Optionally hide the tile title
showTitle?: boolean;
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents?: boolean;
// Optionally hide the popout widget icon
showPopout?: boolean;
// Is this an instance of a user widget
userWidget: boolean;
// sets the pointer-events property on the iframe
pointerEvents?: string;
widgetPageTitle?: string;
}
interface IState {
initialising: boolean; // True while we are mangling the widget URL
// True while the iframe content is loading
loading: boolean;
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad: boolean;
error: Error;
menuDisplayed: boolean;
widgetPageTitle: string;
}
import { logger } from "matrix-js-sdk/src/logger";
@replaceableComponent("views.elements.AppTile") @replaceableComponent("views.elements.AppTile")
export default class AppTile extends React.Component { export default class AppTile extends React.Component<IProps, IState> {
constructor(props) { public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};
private contextMenuButton = createRef<any>();
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
private allowedWidgetsWatchRef: string;
private persistKey: string;
private sgWidget: StopGapWidget;
private dispatcherRef: string;
constructor(props: IProps) {
super(props); super(props);
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this.persistKey = getPersistKey(this.props.app.id);
try { try {
this._sgWidget = new StopGapWidget(this.props); this.sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this.getNewState(props);
this._contextMenuButton = createRef();
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
} }
// This is a function to make the impact of calling SettingsStore slightly less // This is a function to make the impact of calling SettingsStore slightly less
hasPermissionToLoad = (props) => { private hasPermissionToLoad = (props: IProps): boolean => {
if (this._usingLocalWidget()) return true; if (this.usingLocalWidget()) return true;
if (!props.room) return true; // user widgets always have permissions if (!props.room) return true; // user widgets always have permissions
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
@ -81,34 +144,34 @@ export default class AppTile extends React.Component {
* @param {Object} newProps The new properties of the component * @param {Object} newProps The new properties of the component
* @return {Object} Updated component state to be set with setState * @return {Object} Updated component state to be set with setState
*/ */
_getNewState(newProps) { private getNewState(newProps: IProps): IState {
return { return {
initialising: true, // True while we are mangling the widget URL initialising: true, // True while we are mangling the widget URL
// True while the iframe content is loading // True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
// Assume that widget has permission to load if we are the user who // Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user // added it to the room, or if explicitly granted by the user
hasPermissionToLoad: this.hasPermissionToLoad(newProps), hasPermissionToLoad: this.hasPermissionToLoad(newProps),
error: null, error: null,
widgetPageTitle: newProps.widgetPageTitle,
menuDisplayed: false, menuDisplayed: false,
widgetPageTitle: this.props.widgetPageTitle,
}; };
} }
onAllowedWidgetsChange = () => { private onAllowedWidgetsChange = (): void => {
const hasPermissionToLoad = this.hasPermissionToLoad(this.props); const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
if (this._sgWidget) this._sgWidget.stop(); if (this.sgWidget) this.sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
}; };
isMixedContent() { private isMixedContent(): boolean {
const parentContentProtocol = window.location.protocol; const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.app.url); const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol; const childContentProtocol = u.protocol;
@ -120,69 +183,70 @@ export default class AppTile extends React.Component {
return false; return false;
} }
componentDidMount() { public componentDidMount(): void {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this._sgWidget && this.state.hasPermissionToLoad) { if (this.sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this.startWidget();
} }
// Widget action listeners // Widget action listeners
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { public componentWillUnmount(): void {
// Widget action listeners // Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container // if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
} }
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
} }
_resetWidget(newProps) { private resetWidget(newProps: IProps): void {
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
try { try {
this._sgWidget = new StopGapWidget(newProps); this.sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
this._startWidget(); this.startWidget();
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
} }
_startWidget() { private startWidget(): void {
this._sgWidget.prepare().then(() => { this.sgWidget.prepare().then(() => {
this.setState({ initialising: false }); this.setState({ initialising: false });
}); });
} }
_iframeRefChange = (ref) => { private iframeRefChange = (ref: HTMLIFrameElement): void => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
if (this._sgWidget) this._sgWidget.start(ref); if (this.sgWidget) this.sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this.resetWidget(this.props);
} }
}; };
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
if (nextProps.app.url !== this.props.app.url) { if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps); this.getNewState(nextProps);
if (this.state.hasPermissionToLoad) { if (this.state.hasPermissionToLoad) {
this._resetWidget(nextProps); this.resetWidget(nextProps);
} }
} }
@ -198,7 +262,7 @@ export default class AppTile extends React.Component {
* @private * @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/ */
async _endWidgetActions() { // widget migration dev note: async to maintain signature private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
// HACK: This is a really dirty way to ensure that Jitsi cleans up // HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media // its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
@ -217,27 +281,27 @@ export default class AppTile extends React.Component {
} }
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }
_onWidgetPrepared = () => { private onWidgetPrepared = (): void => {
this.setState({ loading: false }); this.setState({ loading: false });
}; };
_onWidgetReady = () => { private onWidgetReady = (): void => {
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {}); this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
} }
}; };
_onAction = payload => { private onAction = (payload): void => {
if (payload.widgetId === this.props.app.id) { if (payload.widgetId === this.props.app.id) {
switch (payload.action) { switch (payload.action) {
case 'm.sticker': case 'm.sticker':
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
dis.dispatch({ action: 'post_sticker_message', data: payload.data }); dis.dispatch({ action: 'post_sticker_message', data: payload.data });
dis.dispatch({ action: 'stickerpicker_close' }); dis.dispatch({ action: 'stickerpicker_close' });
} else { } else {
@ -248,7 +312,7 @@ export default class AppTile extends React.Component {
} }
}; };
_grantWidgetPermission = () => { private grantWidgetPermission = (): void => {
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.app.eventId); console.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId); const current = SettingsStore.getValue("allowedWidgets", roomId);
@ -258,14 +322,14 @@ export default class AppTile extends React.Component {
this.setState({ hasPermissionToLoad: true }); this.setState({ hasPermissionToLoad: true });
// Fetch a token for the integration manager, now that we're allowed to // Fetch a token for the integration manager, now that we're allowed to
this._startWidget(); this.startWidget();
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
// We don't really need to do anything about this - the user will just hit the button again. // We don't really need to do anything about this - the user will just hit the button again.
}); });
}; };
formatAppTileName() { private formatAppTileName(): string {
let appTileName = "No name"; let appTileName = "No name";
if (this.props.app.name && this.props.app.name.trim()) { if (this.props.app.name && this.props.app.name.trim()) {
appTileName = this.props.app.name.trim(); appTileName = this.props.app.name.trim();
@ -278,11 +342,11 @@ export default class AppTile extends React.Component {
* actual widget URL * actual widget URL
* @returns {bool} true If using a local version of the widget * @returns {bool} true If using a local version of the widget
*/ */
_usingLocalWidget() { private usingLocalWidget(): boolean {
return WidgetType.JITSI.matches(this.props.app.type); return WidgetType.JITSI.matches(this.props.app.type);
} }
_getTileTitle() { private getTileTitle(): JSX.Element {
const name = this.formatAppTileName(); const name = this.formatAppTileName();
const titleSpacer = <span>&nbsp;-&nbsp;</span>; const titleSpacer = <span>&nbsp;-&nbsp;</span>;
let title = ''; let title = '';
@ -300,32 +364,32 @@ export default class AppTile extends React.Component {
} }
// TODO replace with full screen interactions // TODO replace with full screen interactions
_onPopoutWidgetClick = () => { private onPopoutWidgetClick = (): void => {
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._endWidgetActions().then(() => { this.endWidgetActions().then(() => {
if (this.iframe) { if (this.iframe) {
// Reload iframe // Reload iframe
this.iframe.src = this._sgWidget.embedUrl; this.iframe.src = this.sgWidget.embedUrl;
} }
}); });
} }
// Using Object.assign workaround as the following opens in a new window instead of a new tab. // Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'), Object.assign(document.createElement('a'),
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click(); { target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
}; };
_onContextMenuClick = () => { private onContextMenuClick = (): void => {
this.setState({ menuDisplayed: true }); this.setState({ menuDisplayed: true });
}; };
_closeContextMenu = () => { private closeContextMenu = (): void => {
this.setState({ menuDisplayed: false }); this.setState({ menuDisplayed: false });
}; };
render() { public render(): JSX.Element {
let appTileBody; let appTileBody;
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
@ -351,7 +415,7 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (this._sgWidget === null) { if (this.sgWidget === null) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} /> <AppWarning errorMsg={_t("Error loading Widget")} />
@ -365,9 +429,9 @@ export default class AppTile extends React.Component {
<AppPermission <AppPermission
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId} creatorUserId={this.props.creatorUserId}
url={this._sgWidget.embedUrl} url={this.sgWidget.embedUrl}
isRoomEncrypted={isEncrypted} isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission} onPermissionGranted={this.grantWidgetPermission}
/> />
</div> </div>
); );
@ -390,8 +454,8 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement } { this.state.loading && loadingElement }
<iframe <iframe
allow={iframeFeatures} allow={iframeFeatures}
ref={this._iframeRefChange} ref={this.iframeRefChange}
src={this._sgWidget.embedUrl} src={this.sgWidget.embedUrl}
allowFullScreen={true} allowFullScreen={true}
sandbox={sandboxFlags} sandbox={sandboxFlags}
/> />
@ -407,7 +471,7 @@ export default class AppTile extends React.Component {
// Also wrap the PersistedElement in a div to fix the height, otherwise // Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place // AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper"> appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}> <PersistedElement persistKey={this.persistKey}>
{ appTileBody } { appTileBody }
</PersistedElement> </PersistedElement>
</div>; </div>;
@ -429,9 +493,9 @@ export default class AppTile extends React.Component {
if (this.state.menuDisplayed) { if (this.state.menuDisplayed) {
contextMenu = ( contextMenu = (
<RoomWidgetContextMenu <RoomWidgetContextMenu
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)} {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
app={this.props.app} app={this.props.app}
onFinished={this._closeContextMenu} onFinished={this.closeContextMenu}
showUnpin={!this.props.userWidget} showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget} userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick} onEditClick={this.props.onEditClick}
@ -444,21 +508,21 @@ export default class AppTile extends React.Component {
<div className={appTileClasses} id={this.props.app.id}> <div className={appTileClasses} id={this.props.app.id}>
{ this.props.showMenubar && { this.props.showMenubar &&
<div className="mx_AppTileMenuBar"> <div className="mx_AppTileMenuBar">
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}> <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
{ this.props.showTitle && this._getTileTitle() } { this.props.showTitle && this.getTileTitle() }
</span> </span>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ this.props.showPopout && <AccessibleButton { this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')} title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick} onClick={this.onPopoutWidgetClick}
/> } /> }
<ContextMenuButton <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
label={_t("Options")} label={_t("Options")}
isExpanded={this.state.menuDisplayed} isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton} inputRef={this.contextMenuButton}
onClick={this._onContextMenuClick} onClick={this.onContextMenuClick}
/> />
</span> </span>
</div> } </div> }
@ -469,49 +533,3 @@ export default class AppTile extends React.Component {
</React.Fragment>; </React.Fragment>;
} }
} }
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
app: PropTypes.object.isRequired,
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: PropTypes.object,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: PropTypes.bool,
// Optional. If set, renders a smaller view of the widget
miniMode: PropTypes.bool,
// UserId of the current user
userId: PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget
creatorUserId: PropTypes.string,
waitForIframeLoad: PropTypes.bool,
showMenubar: PropTypes.bool,
// Optional onEditClickHandler (overrides default behaviour)
onEditClick: PropTypes.func,
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick: PropTypes.func,
// Optional onMinimiseClickHandler
onMinimiseClick: PropTypes.func,
// Optionally hide the tile title
showTitle: PropTypes.bool,
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
// sets the pointer-events property on the iframe
pointerEvents: PropTypes.string,
};
AppTile.defaultProps = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};

View file

@ -1,24 +1,20 @@
import React from 'react'; // eslint-disable-line no-unused-vars import React from 'react';
import PropTypes from 'prop-types';
const AppWarning = (props) => { interface IProps {
errorMsg?: string;
}
const AppWarning: React.FC<IProps> = (props) => {
return ( return (
<div className='mx_AppPermissionWarning'> <div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'> <div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/warning.svg")} alt='' /> <img src={require("../../../../res/img/warning.svg")} alt='' />
</div> </div>
<div className='mx_AppPermissionWarningText'> <div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span> <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
</div> </div>
</div> </div>
); );
}; };
AppWarning.propTypes = {
errorMsg: PropTypes.string,
};
AppWarning.defaultProps = {
errorMsg: 'Error',
};
export default AppWarning; export default AppWarning;

View file

@ -17,60 +17,61 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// The primary button which is styled differently and has default focus.
primaryButton: React.ReactNode;
// A node to insert into the cancel button instead of default "Cancel"
cancelButton?: React.ReactNode;
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit?: boolean;
// onClick handler for the primary button.
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
// should there be a cancel button? default: true
hasCancel?: boolean;
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass?: string;
// onClick handler for the cancel button.
onCancel?: (...args: any[]) => void;
focus?: boolean;
// disables the primary and cancel buttons
disabled?: boolean;
// disables only the primary button
primaryDisabled?: boolean;
// something to stick next to the buttons, optionally
additive?: React.ReactNode;
primaryButtonClass?: string;
}
/** /**
* Basic container for buttons in modal dialogs. * Basic container for buttons in modal dialogs.
*/ */
@replaceableComponent("views.elements.DialogButtons") @replaceableComponent("views.elements.DialogButtons")
export default class DialogButtons extends React.Component { export default class DialogButtons extends React.Component<IProps> {
static propTypes = { public static defaultProps: Partial<IProps> = {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// A node to insert into the cancel button instead of default "Cancel"
cancelButton: PropTypes.node,
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit: PropTypes.bool,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func,
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass: PropTypes.node,
// onClick handler for the cancel button.
onCancel: PropTypes.func,
focus: PropTypes.bool,
// disables the primary and cancel buttons
disabled: PropTypes.bool,
// disables only the primary button
primaryDisabled: PropTypes.bool,
// something to stick next to the buttons, optionally
additive: PropTypes.element,
};
static defaultProps = {
hasCancel: true, hasCancel: true,
disabled: false, disabled: false,
}; };
_onCancelClick = () => { private onCancelClick = (event: React.MouseEvent): void => {
this.props.onCancel(); this.props.onCancel(event);
}; };
render() { public render(): JSX.Element {
let primaryButtonClassName = "mx_Dialog_primary"; let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) { if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass; primaryButtonClassName += " " + this.props.primaryButtonClass;
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
// important: the default type is 'submit' and this button comes before the // important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit. // primary in the DOM so will get form submissions unless we make it not a submit.
type="button" type="button"
onClick={this._onCancelClick} onClick={this.onCancelClick}
className={this.props.cancelButtonClass} className={this.props.cancelButtonClass}
disabled={this.props.disabled} disabled={this.props.disabled}
> >

View file

@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ChangeEvent, createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "./AccessibleButton";
interface IProps {
className?: string;
onChange?: (value: string) => void;
onClear?: () => void;
onJoinClick?: (value: string) => void;
placeholder?: string;
showJoinButton?: boolean;
initialText?: string;
}
interface IState {
value: string;
}
@replaceableComponent("views.elements.DirectorySearchBox") @replaceableComponent("views.elements.DirectorySearchBox")
export default class DirectorySearchBox extends React.Component { export default class DirectorySearchBox extends React.Component<IProps, IState> {
constructor(props) { private input = createRef<HTMLInputElement>();
super(props);
this._collectInput = this._collectInput.bind(this);
this._onClearClick = this._onClearClick.bind(this);
this._onChange = this._onChange.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
this.input = null; constructor(props: IProps) {
super(props);
this.state = { this.state = {
value: this.props.initialText || '', value: this.props.initialText || '',
}; };
} }
_collectInput(e) { private onClearClick = (): void => {
this.input = e;
}
_onClearClick() {
this.setState({ value: '' }); this.setState({ value: '' });
if (this.input) { if (this.input.current) {
this.input.focus(); this.input.current.focus();
if (this.props.onClear) { if (this.props.onClear) {
this.props.onClear(); this.props.onClear();
} }
} }
} };
_onChange(ev) { private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
if (!this.input) return; if (!this.input.current) return;
this.setState({ value: ev.target.value }); this.setState({ value: ev.target.value });
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(ev.target.value); this.props.onChange(ev.target.value);
} }
} };
_onKeyUp(ev) { private onKeyUp = (ev: React.KeyboardEvent): void => {
if (ev.key == 'Enter' && this.props.showJoinButton) { if (ev.key == 'Enter' && this.props.showJoinButton) {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} }
} };
_onJoinButtonClick() { private onJoinButtonClick = (): void => {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} };
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
public render(): JSX.Element {
const searchboxClasses = { const searchboxClasses = {
mx_DirectorySearchBox: true, mx_DirectorySearchBox: true,
}; };
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
let joinButton; let joinButton;
if (this.props.showJoinButton) { if (this.props.showJoinButton) {
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton" joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
onClick={this._onJoinButtonClick} onClick={this.onJoinButtonClick}
>{ _t("Join") }</AccessibleButton>; >{ _t("Join") }</AccessibleButton>;
} }
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
name="dirsearch" name="dirsearch"
value={this.state.value} value={this.state.value}
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
ref={this._collectInput} ref={this.input}
onChange={this._onChange} onChange={this.onChange}
onKeyUp={this._onKeyUp} onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoFocus autoFocus
/> />
{ joinButton } { joinButton }
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
</div>; </div>;
} }
} }
DirectorySearchBox.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
initialText: PropTypes.string,
};

View file

@ -16,33 +16,42 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.EditableText") enum Phases {
export default class EditableText extends React.Component { Display = "display",
static propTypes = { Edit = "edit",
onValueChanged: PropTypes.func, }
initialValue: PropTypes.string,
label: PropTypes.string, interface IProps {
placeholder: PropTypes.string, onValueChanged?: (value: string, shouldSubmit: boolean) => void;
className: PropTypes.string, initialValue?: string;
labelClassName: PropTypes.string, label?: string;
placeholderClassName: PropTypes.string, placeholder?: string;
className?: string;
labelClassName?: string;
placeholderClassName?: string;
// Overrides blurToSubmit if true // Overrides blurToSubmit if true
blurToCancel: PropTypes.bool, blurToCancel?: boolean;
// Will cause onValueChanged(value, true) to fire on blur // Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: PropTypes.bool, blurToSubmit?: boolean;
editable: PropTypes.bool, editable?: boolean;
}; }
static Phases = { interface IState {
Display: "display", phase: Phases;
Edit: "edit", }
};
static defaultProps = { @replaceableComponent("views.elements.EditableText")
export default class EditableText extends React.Component<IProps, IState> {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
public value = '';
private placeholder = false;
private editableDiv = createRef<HTMLDivElement>();
public static defaultProps: Partial<IProps> = {
onValueChanged() {}, onValueChanged() {},
initialValue: '', initialValue: '',
label: '', label: '',
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
blurToSubmit: false, blurToSubmit: false,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
// we track value as an JS object field rather than in React state this.state = {
// as React doesn't play nice with contentEditable. phase: Phases.Display,
this.value = '';
this.placeholder = false;
this._editable_div = createRef();
}
state = {
phase: EditableText.Phases.Display,
}; };
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
UNSAFE_componentWillReceiveProps(nextProps) { public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
if (nextProps.initialValue !== this.props.initialValue) { if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue; this.value = nextProps.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
} }
componentDidMount() { public componentDidMount(): void {
this.value = this.props.initialValue; this.value = this.props.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
showPlaceholder = show => { private showPlaceholder = (show: boolean): void => {
if (show) { if (show) {
this._editable_div.current.textContent = this.props.placeholder; this.editableDiv.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className this.editableDiv.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName); + " " + this.props.placeholderClassName);
this.placeholder = true; this.placeholder = true;
this.value = ''; this.value = '';
} else { } else {
this._editable_div.current.textContent = this.value; this.editableDiv.current.textContent = this.value;
this._editable_div.current.setAttribute("class", this.props.className); this.editableDiv.current.setAttribute("class", this.props.className);
this.placeholder = false; this.placeholder = false;
} }
}; };
getValue = () => this.value; private cancelEdit = (): void => {
setValue = value => {
this.value = value;
this.showPlaceholder(!this.value);
};
edit = () => {
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Display,
});
};
cancelEdit = () => {
this.setState({
phase: EditableText.Phases.Display,
}); });
this.value = this.props.initialValue; this.value = this.props.initialValue;
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
this.onValueChanged(false); this.onValueChanged(false);
this._editable_div.current.blur(); this.editableDiv.current.blur();
}; };
onValueChanged = shouldSubmit => { private onValueChanged = (shouldSubmit: boolean): void => {
this.props.onValueChanged(this.value, shouldSubmit); this.props.onValueChanged(this.value, shouldSubmit);
}; };
onKeyDown = ev => { private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) { if (this.placeholder) {
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onKeyUp = ev => { private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (!ev.target.textContent) { if (!(ev.target as HTMLDivElement).textContent) {
this.showPlaceholder(true); this.showPlaceholder(true);
} else if (!this.placeholder) { } else if (!this.placeholder) {
this.value = ev.target.textContent; this.value = (ev.target as HTMLDivElement).textContent;
} }
if (ev.key === Key.ENTER) { if (ev.key === Key.ENTER) {
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onClickDiv = ev => { private onClickDiv = (): void => {
if (!this.props.editable) return; if (!this.props.editable) return;
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Edit,
}); });
}; };
onFocus = ev => { private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
//ev.target.setSelectionRange(0, ev.target.textContent.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
const node = ev.target.childNodes[0]; const node = ev.target.childNodes[0];
if (node) { if (node) {
const range = document.createRange(); const range = document.createRange();
range.setStart(node, 0); range.setStart(node, 0);
range.setEnd(node, node.length); range.setEnd(node, ev.target.childNodes.length);
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
} }
}; };
onFinish = (ev, shouldSubmit) => { private onFinish = (
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
shouldSubmit?: boolean,
): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; const self = this;
const submit = (ev.key === Key.ENTER) || shouldSubmit; const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
this.setState({ this.setState({
phase: EditableText.Phases.Display, phase: Phases.Display,
}, () => { }, () => {
if (this.value !== this.props.initialValue) { if (this.value !== this.props.initialValue) {
self.onValueChanged(submit); self.onValueChanged(submit);
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
}); });
}; };
onBlur = ev => { private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
}; };
render() { public render(): JSX.Element {
const { className, editable, initialValue, label, labelClassName } = this.props; const { className, editable, initialValue, label, labelClassName } = this.props;
let editableEl; let editableEl;
if (!editable || (this.state.phase === EditableText.Phases.Display && if (!editable || (this.state.phase === Phases.Display &&
(label || labelClassName) && !this.value) (label || labelClassName) && !this.value)
) { ) {
// show the label // show the label
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
} else { } else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div editableEl = <div
ref={this._editable_div} ref={this.editableDiv}
contentEditable={true} contentEditable={true}
className={className} className={className}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View file

@ -15,9 +15,34 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import EditableText from "./EditableText";
interface IProps {
/* callback to retrieve the initial value. */
getInitialValue?: () => Promise<string>;
/* initial value; used if getInitialValue is not given */
initialValue?: string;
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder?: string;
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit?: (value: string) => Promise<{} | void>;
/* should the input submit when focus is lost? */
blurToSubmit?: boolean;
}
interface IState {
busy: boolean;
errorString: string;
value: string;
}
/** /**
* A component which wraps an EditableText, with a spinner while updates take * A component which wraps an EditableText, with a spinner while updates take
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* taken from the 'initialValue' property. * taken from the 'initialValue' property.
*/ */
@replaceableComponent("views.elements.EditableTextContainer") @replaceableComponent("views.elements.EditableTextContainer")
export default class EditableTextContainer extends React.Component { export default class EditableTextContainer extends React.Component<IProps, IState> {
constructor(props) { private unmounted = false;
public static defaultProps: Partial<IProps> = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: () => { return Promise.resolve(); },
};
constructor(props: IProps) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
busy: false, busy: false,
errorString: null, errorString: null,
value: props.initialValue, value: props.initialValue,
}; };
this._onValueChanged = this._onValueChanged.bind(this);
} }
componentDidMount() { public async componentDidMount(): Promise<void> {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property. // use whatever was given in the initialValue property.
return; if (this.props.getInitialValue === undefined) return;
}
this.setState({ busy: true }); this.setState({ busy: true });
try {
this.props.getInitialValue().then( const initialValue = await this.props.getInitialValue();
(result) => { if (this.unmounted) return;
if (this._unmounted) { return; }
this.setState({ this.setState({
busy: false, busy: false,
value: result, value: initialValue,
}); });
}, } catch (error) {
(error) => { if (this.unmounted) return;
if (this._unmounted) { return; }
this.setState({ this.setState({
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
}, }
);
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
} }
_onValueChanged(value, shouldSubmit) { private onValueChanged = (value: string, shouldSubmit: boolean): void => {
if (!shouldSubmit) { if (!shouldSubmit) {
return; return;
} }
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
this.props.onSubmit(value).then( this.props.onSubmit(value).then(
() => { () => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
busy: false, busy: false,
value: value, value: value,
}); });
}, },
(error) => { (error) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
}, },
); );
} };
render() { public render(): JSX.Element {
if (this.state.busy) { if (this.state.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return ( return (
<Loader /> <Spinner />
); );
} else if (this.state.errorString) { } else if (this.state.errorString) {
return ( return (
<div className="error">{ this.state.errorString }</div> <div className="error">{ this.state.errorString }</div>
); );
} else { } else {
const EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText initialValue={this.state.value} <EditableText initialValue={this.state.value}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onValueChanged={this._onValueChanged} onValueChanged={this.onValueChanged}
blurToSubmit={this.props.blurToSubmit} blurToSubmit={this.props.blurToSubmit}
/> />
); );
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
} }
} }
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: PropTypes.bool,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: function(v) {return Promise.resolve(); },
};

View file

@ -34,6 +34,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse"; import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps'; import { IDialogProps } from '../dialogs/IDialogProps';
import UIStore from '../../../stores/UIStore';
// Max scale to keep gaps around the image // Max scale to keep gaps around the image
const MAX_SCALE = 0.95; const MAX_SCALE = 0.95;
@ -44,6 +45,13 @@ const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom // If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10; const ZOOM_DISTANCE = 10;
// Height of mx_ImageView_panel
const getPanelHeight = (): number => {
const value = getComputedStyle(document.documentElement).getPropertyValue("--image-view-panel-height");
// Return the value as a number without the unit
return parseInt(value.slice(0, value.length - 2));
};
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
src: string; // the source of the image being displayed src: string; // the source of the image being displayed
name?: string; // the main title ('name') for the image name?: string; // the main title ('name') for the image
@ -56,8 +64,15 @@ interface IProps extends IDialogProps {
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit // redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated // properties above, which let us use lightboxes to display images which aren't associated
// with events. // with events.
mxEvent: MatrixEvent; mxEvent?: MatrixEvent;
permalinkCreator: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
thumbnailInfo?: {
positionX: number;
positionY: number;
width: number;
height: number;
};
} }
interface IState { interface IState {
@ -75,13 +90,25 @@ interface IState {
export default class ImageView extends React.Component<IProps, IState> { export default class ImageView extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
const { thumbnailInfo } = this.props;
this.state = { this.state = {
zoom: 0, zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
minZoom: MAX_SCALE, minZoom: MAX_SCALE,
maxZoom: MAX_SCALE, maxZoom: MAX_SCALE,
rotation: 0, rotation: 0,
translationX: 0, translationX: (
translationY: 0, thumbnailInfo?.positionX +
(thumbnailInfo?.width / 2) -
(UIStore.instance.windowWidth / 2)
) ?? 0,
translationY: (
thumbnailInfo?.positionY +
(thumbnailInfo?.height / 2) -
(UIStore.instance.windowHeight / 2) -
(getPanelHeight() / 2)
) ?? 0,
moving: false, moving: false,
contextMenuDisplayed: false, contextMenuDisplayed: false,
}; };
@ -98,6 +125,9 @@ export default class ImageView extends React.Component<IProps, IState> {
private previousX = 0; private previousX = 0;
private previousY = 0; private previousY = 0;
private animatingLoading = false;
private imageIsLoaded = false;
componentDidMount() { componentDidMount() {
// We have to use addEventListener() because the listener // We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium // needs to be passive in order to work with Chromium
@ -105,15 +135,37 @@ export default class ImageView extends React.Component<IProps, IState> {
// We want to recalculate zoom whenever the window's size changes // We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.recalculateZoom); window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom // After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.recalculateZoom); this.image.current.addEventListener("load", this.imageLoaded);
} }
componentWillUnmount() { componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel); this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.recalculateZoom); window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.recalculateZoom); this.image.current.removeEventListener("load", this.imageLoaded);
} }
private imageLoaded = () => {
// First, we calculate the zoom, so that the image has the same size as
// the thumbnail
const { thumbnailInfo } = this.props;
if (thumbnailInfo?.width) {
this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth });
}
// Once the zoom is set, we the image is considered loaded and we can
// start animating it into the center of the screen
this.imageIsLoaded = true;
this.animatingLoading = true;
this.setZoomAndRotation();
this.setState({
translationX: 0,
translationY: 0,
});
// Once the position is set, there is no need to animate anymore
this.animatingLoading = false;
};
private recalculateZoom = () => { private recalculateZoom = () => {
this.setZoomAndRotation(); this.setZoomAndRotation();
}; };
@ -360,16 +412,17 @@ export default class ImageView extends React.Component<IProps, IState> {
const showEventMeta = !!this.props.mxEvent; const showEventMeta = !!this.props.mxEvent;
const zoomingDisabled = this.state.maxZoom === this.state.minZoom; const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
let transitionClassName;
if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading";
else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
else transitionClassName = "mx_ImageView_image_animating";
let cursor; let cursor;
if (this.state.moving) { if (this.state.moving) cursor = "grabbing";
cursor= "grabbing"; else if (zoomingDisabled) cursor = "default";
} else if (zoomingDisabled) { else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in";
cursor = "default"; else cursor = "zoom-out";
} else if (this.state.zoom === this.state.minZoom) {
cursor = "zoom-in";
} else {
cursor = "zoom-out";
}
const rotationDegrees = this.state.rotation + "deg"; const rotationDegrees = this.state.rotation + "deg";
const zoom = this.state.zoom; const zoom = this.state.zoom;
const translatePixelsX = this.state.translationX + "px"; const translatePixelsX = this.state.translationX + "px";
@ -380,7 +433,6 @@ export default class ImageView extends React.Component<IProps, IState> {
// image causing it translate in the wrong direction. // image causing it translate in the wrong direction.
const style = { const style = {
cursor: cursor, cursor: cursor,
transition: this.state.moving ? null : "transform 200ms ease 0s",
transform: `translateX(${translatePixelsX}) transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY}) translateY(${translatePixelsY})
scale(${zoom}) scale(${zoom})
@ -528,7 +580,7 @@ export default class ImageView extends React.Component<IProps, IState> {
style={style} style={style}
alt={this.props.name} alt={this.props.name}
ref={this.image} ref={this.image}
className="mx_ImageView_image" className={`mx_ImageView_image ${transitionClassName}`}
draggable={true} draggable={true}
onMouseDown={this.onStartMoving} onMouseDown={this.onStartMoving}
/> />

View file

@ -16,13 +16,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import Dropdown from "./Dropdown";
function languageMatchesSearchQuery(query, language) { function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true; if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
return false; return false;
} }
interface IProps {
className?: string;
onOptionChange: (language: string) => void;
value?: string;
disabled?: boolean;
}
interface IState {
searchQuery: string;
langs: string[];
}
@replaceableComponent("views.elements.LanguageDropdown") @replaceableComponent("views.elements.LanguageDropdown")
export default class LanguageDropdown extends React.Component { export default class LanguageDropdown extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = { this.state = {
searchQuery: '', searchQuery: '',
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
languageHandler.getAllLanguagesFromJson().then((langs) => { languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) { langs.sort(function(a, b) {
if (a.label < b.label) return -1; if (a.label < b.label) return -1;
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
} }
} }
_onSearchChange(search) { private onSearchChange = (search: string): void => {
this.setState({ this.setState({
searchQuery: search, searchQuery: search,
}); });
} };
render() { public render(): JSX.Element {
if (this.state.langs === null) { if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />; return <Spinner />;
} }
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages; let displayedLanguages;
if (this.state.searchQuery) { if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => { displayedLanguages = this.state.langs.filter((lang) => {
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
id="mx_LanguageDropdown" id="mx_LanguageDropdown"
className={this.props.className} className={this.props.className}
onOptionChange={this.props.onOptionChange} onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange} onSearchChange={this.onSearchChange}
searchEnabled={true} searchEnabled={true}
value={value} value={value}
label={_t("Language Dropdown")} label={_t("Language Dropdown")}
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
} }
} }
LanguageDropdown.propTypes = {
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -15,17 +15,16 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
class ItemRange { class ItemRange {
constructor(topCount, renderCount, bottomCount) { constructor(
this.topCount = topCount; public topCount: number,
this.renderCount = renderCount; public renderCount: number,
this.bottomCount = bottomCount; public bottomCount: number,
} ) { }
contains(range) { public contains(range: ItemRange): boolean {
// don't contain empty ranges // don't contain empty ranges
// as it will prevent clearing the list // as it will prevent clearing the list
// once it is scrolled far enough out of view // once it is scrolled far enough out of view
@ -36,7 +35,7 @@ class ItemRange {
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount); (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
} }
expand(amount) { public expand(amount: number): ItemRange {
// don't expand ranges that won't render anything // don't expand ranges that won't render anything
if (this.renderCount === 0) { if (this.renderCount === 0) {
return this; return this;
@ -51,20 +50,55 @@ class ItemRange {
); );
} }
totalSize() { public totalSize(): number {
return this.topCount + this.renderCount + this.bottomCount; return this.topCount + this.renderCount + this.bottomCount;
} }
} }
interface IProps<T> {
// height in pixels of the component returned by `renderItem`
itemHeight: number;
// function to turn an element of `items` into a react component
renderItem: (item: T) => JSX.Element;
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: number;
// the height of the viewport this content is scrolled in
height: number;
// all items for the list. These should not be react components, see `renderItem`.
items?: T[];
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin?: number;
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems?: number;
element?: string;
className?: string;
}
interface IState {
renderRange: ItemRange;
}
@replaceableComponent("views.elements.LazyRenderList") @replaceableComponent("views.elements.LazyRenderList")
export default class LazyRenderList extends React.Component { export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
constructor(props) { public static defaultProps: Partial<IProps<unknown>> = {
overflowItems: 20,
overflowMargin: 5,
};
constructor(props: IProps<T>) {
super(props); super(props);
this.state = {}; this.state = {
renderRange: null,
};
} }
static getDerivedStateFromProps(props, state) { public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
const range = LazyRenderList.getVisibleRangeFromProps(props); const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(props.overflowMargin); const intersectRange = range.expand(props.overflowMargin);
const renderRange = range.expand(props.overflowItems); const renderRange = range.expand(props.overflowItems);
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
return null; return null;
} }
static getVisibleRangeFromProps(props) { private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
const { items, itemHeight, scrollTop, height } = props; const { items, itemHeight, scrollTop, height } = props;
const length = items ? items.length : 0; const length = items ? items.length : 0;
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
return new ItemRange(topCount, renderCount, bottomCount); return new ItemRange(topCount, renderCount, bottomCount);
} }
render() { public render(): JSX.Element {
const { itemHeight, items, renderItem } = this.props; const { itemHeight, items, renderItem } = this.props;
const { renderRange } = this.state; const { renderRange } = this.state;
const { topCount, renderCount, bottomCount } = renderRange; const { topCount, renderCount, bottomCount } = renderRange;
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
} }
} }
LazyRenderList.defaultProps = {
overflowItems: 20,
overflowMargin: 5,
};
LazyRenderList.propTypes = {
// height in pixels of the component returned by `renderItem`
itemHeight: PropTypes.number.isRequired,
// function to turn an element of `items` into a react component
renderItem: PropTypes.func.isRequired,
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: PropTypes.number.isRequired,
// the height of the viewport this content is scrolled in
height: PropTypes.number.isRequired,
// all items for the list. These should not be react components, see `renderItem`.
items: PropTypes.array,
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin: PropTypes.number,
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems: PropTypes.number,
};

View file

@ -16,25 +16,26 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { throttle } from "lodash"; import { throttle } from "lodash";
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from "../../../dispatcher/payloads";
export const getPersistKey = (appId: string) => 'widget_' + appId;
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body. // pass in a custom control as the actual body.
function getContainer(containerId) { function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId); return document.getElementById(containerId) as HTMLDivElement;
} }
function getOrCreateContainer(containerId) { function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId); let container = getContainer(containerId);
if (!container) { if (!container) {
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
return container; return container;
} }
/* interface IProps {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use
// the same DOM container.
persistKey: string;
// z-index for the element. Defaults to 9.
zIndex?: number;
style?: React.StyleHTMLAttributes<HTMLDivElement>;
}
/**
* Class of component that renders its children in a separate ReactDOM virtual tree * Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body. * in a container element appended to document.body.
* *
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE. * bounding rect as the parent of PE.
*/ */
@replaceableComponent("views.elements.PersistedElement") @replaceableComponent("views.elements.PersistedElement")
export default class PersistedElement extends React.Component { export default class PersistedElement extends React.Component<IProps> {
static propTypes = { private resizeObserver: ResizeObserver;
// Unique identifier for this PersistedElement instance private dispatcherRef: string;
// Any PersistedElements with the same persistKey will use private childContainer: HTMLDivElement;
// the same DOM container. private child: HTMLDivElement;
persistKey: PropTypes.string.isRequired,
// z-index for the element. Defaults to 9. constructor(props: IProps) {
zIndex: PropTypes.number, super(props);
};
constructor() { this.resizeObserver = new ResizeObserver(this.repositionChild);
super();
this.collectChildContainer = this.collectChildContainer.bind(this);
this.collectChild = this.collectChild.bind(this);
this._repositionChild = this._repositionChild.bind(this);
this._onAction = this._onAction.bind(this);
this.resizeObserver = new ResizeObserver(this._repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care // Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its // about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent // dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and // for this, so we bodge it by listening for document resize and
// the timeline_resize action. // the timeline_resize action.
window.addEventListener('resize', this._repositionChild); window.addEventListener('resize', this.repositionChild);
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
/** /**
* Removes the DOM elements created when a PersistedElement with the given * Removes the DOM elements created when a PersistedElement with the given
* persistKey was mounted. The DOM elements will be re-added if another * persistKey was mounted. The DOM elements will be re-added if another
* PeristedElement is mounted in the future. * PersistedElement is mounted in the future.
* *
* @param {string} persistKey Key used to uniquely identify this PersistedElement * @param {string} persistKey Key used to uniquely identify this PersistedElement
*/ */
static destroyElement(persistKey) { public static destroyElement(persistKey: string): void {
const container = getContainer('mx_persistedElement_' + persistKey); const container = getContainer('mx_persistedElement_' + persistKey);
if (container) { if (container) {
container.remove(); container.remove();
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
return Boolean(getContainer('mx_persistedElement_' + persistKey)); return Boolean(getContainer('mx_persistedElement_' + persistKey));
} }
collectChildContainer(ref) { private collectChildContainer = (ref: HTMLDivElement): void => {
if (this.childContainer) { if (this.childContainer) {
this.resizeObserver.unobserve(this.childContainer); this.resizeObserver.unobserve(this.childContainer);
} }
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
if (ref) { if (ref) {
this.resizeObserver.observe(ref); this.resizeObserver.observe(ref);
} }
} };
collectChild(ref) { private collectChild = (ref: HTMLDivElement): void => {
this.child = ref; this.child = ref;
this.updateChild(); this.updateChild();
} };
componentDidMount() { public componentDidMount(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentDidUpdate() { public componentDidUpdate(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentWillUnmount() { public componentWillUnmount(): void {
this.updateChildVisibility(this.child, false); this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect(); this.resizeObserver.disconnect();
window.removeEventListener('resize', this._repositionChild); window.removeEventListener('resize', this.repositionChild);
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction(payload) { private onAction = (payload: ActionPayload): void => {
if (payload.action === 'timeline_resize') { if (payload.action === 'timeline_resize') {
this._repositionChild(); this.repositionChild();
} else if (payload.action === 'logout') { } else if (payload.action === 'logout') {
PersistedElement.destroyElement(this.props.persistKey); PersistedElement.destroyElement(this.props.persistKey);
} }
} };
_repositionChild() { private repositionChild = (): void => {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
} };
updateChild() { private updateChild(): void {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
this.updateChildVisibility(this.child, true); this.updateChildVisibility(this.child, true);
} }
renderApp() { private renderApp(): void {
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}> const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}> <div ref={this.collectChild} style={this.props.style}>
{ this.props.children } { this.props.children }
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey)); ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
} }
updateChildVisibility(child, visible) { private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
if (!child) return; if (!child) return;
child.style.display = visible ? 'block' : 'none'; child.style.display = visible ? 'block' : 'none';
} }
updateChildPosition = throttle((child, parent) => { private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return; if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect(); const parentRect = parent.getBoundingClientRect();
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
}); });
}, 100, { trailing: true, leading: true }); }, 100, { trailing: true, leading: true });
render() { public render(): JSX.Element {
return <div ref={this.collectChildContainer} />; return <div ref={this.collectChildContainer} />;
} }
} }
export const getPersistKey = (appId) => 'widget_' + appId;

View file

@ -19,57 +19,70 @@ import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { EventSubscription } from 'fbemitter';
import AppTile from "./AppTile";
import { Room } from "matrix-js-sdk/src/models/room";
interface IState {
roomId: string;
persistentWidgetId: string;
}
@replaceableComponent("views.elements.PersistentApp") @replaceableComponent("views.elements.PersistentApp")
export default class PersistentApp extends React.Component { export default class PersistentApp extends React.Component<{}, IState> {
state = { private roomStoreToken: EventSubscription;
constructor() {
super({});
this.state = {
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
}; };
componentDidMount() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
} }
componentWillUnmount() { public componentDidMount(): void {
if (this._roomStoreToken) { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this._roomStoreToken.remove(); ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
} }
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
public componentWillUnmount(): void {
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
} }
} }
_onRoomViewStoreUpdate = payload => { private onRoomViewStoreUpdate = (): void => {
if (RoomViewStore.getRoomId() === this.state.roomId) return; if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({ this.setState({
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
}); });
}; };
_onActiveWidgetStoreUpdate = () => { private onActiveWidgetStoreUpdate = (): void => {
this.setState({ this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
}); });
}; };
_onMyMembership = async (room, membership) => { private onMyMembership = async (room: Room, membership: string): Promise<void> => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") { if (membership !== "join") {
// we're not in the room anymore - delete // we're not in the room anymore - delete
if (room.roomId === persistentWidgetInRoomId) { if (room .roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
} }
} }
}; };
render() { public render(): JSX.Element {
if (this.state.persistentWidgetId) { if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(), persistentWidgetInRoomId, appEvent.getId(),
); );
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile return <AppTile
key={app.id} key={app.id}
app={app} app={app}

View file

@ -15,40 +15,52 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "./Field"; import Field from "./Field";
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.PowerSelector") const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
export default class PowerSelector extends React.Component {
static propTypes = { interface IProps {
value: PropTypes.number.isRequired, value: number;
// The maximum value that can be set with the power selector // The maximum value that can be set with the power selector
maxValue: PropTypes.number.isRequired, maxValue: number;
// Default user power level for the room // Default user power level for the room
usersDefault: PropTypes.number.isRequired, usersDefault: number;
// should the user be able to change the value? false by default. // should the user be able to change the value? false by default.
disabled: PropTypes.bool, disabled?: boolean;
onChange: PropTypes.func, onChange?: (value: number, powerLevelKey: string) => void;
// Optional key to pass as the second argument to `onChange` // Optional key to pass as the second argument to `onChange`
powerLevelKey: PropTypes.string, powerLevelKey?: string;
// The name to annotate the selector with // The name to annotate the selector with
label: PropTypes.string, label?: string;
} }
static defaultProps = { interface IState {
levelRoleMap: {};
// List of power levels to show in the drop-down
options: number[];
customValue: number;
selectValue: number | string;
custom?: boolean;
customLevel?: number;
}
@replaceableComponent("views.elements.PowerSelector")
export default class PowerSelector extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
maxValue: Infinity, maxValue: Infinity,
usersDefault: 0, usersDefault: 0,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillMount() { public UNSAFE_componentWillMount(): void {
this._initStateFromProps(this.props); this.initStateFromProps(this.props);
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillReceiveProps(newProps) { public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
this._initStateFromProps(newProps); this.initStateFromProps(newProps);
} }
_initStateFromProps(newProps) { private initStateFromProps(newProps: IProps): void {
// This needs to be done now because levelRoleMap has translated strings // This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => { const options = Object.keys(levelRoleMap).filter(level => {
return ( return (
level === undefined || level === undefined ||
level <= newProps.maxValue || parseInt(level) <= newProps.maxValue ||
level == newProps.value parseInt(level) == newProps.value
); );
}); }).map(level => parseInt(level));
const isCustom = levelRoleMap[newProps.value] === undefined; const isCustom = levelRoleMap[newProps.value] === undefined;
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
options, options,
custom: isCustom, custom: isCustom,
customLevel: newProps.value, customLevel: newProps.value,
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
}); });
} }
onSelectChange = event => { private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; const isCustom = event.target.value === CUSTOM_VALUE;
if (isCustom) { if (isCustom) {
this.setState({ custom: true }); this.setState({ custom: true });
} else { } else {
this.props.onChange(event.target.value, this.props.powerLevelKey); const powerLevel = parseInt(event.target.value);
this.setState({ selectValue: event.target.value }); this.props.onChange(powerLevel, this.props.powerLevelKey);
this.setState({ selectValue: powerLevel });
} }
}; };
onCustomChange = event => { private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ customValue: event.target.value }); this.setState({ customValue: parseInt(event.target.value) });
}; };
onCustomBlur = event => { private onCustomBlur = (event: React.FocusEvent): void => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); this.props.onChange(this.state.customValue, this.props.powerLevelKey);
}; };
onCustomKeyDown = event => { private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === Key.ENTER) { if (event.key === Key.ENTER) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
// raising a dialog which causes a blur which causes a dialog which causes a blur and // raising a dialog which causes a blur which causes a dialog which causes a blur and
// so on. By not causing the onChange to be called here, we avoid the loop because we // so on. By not causing the onChange to be called here, we avoid the loop because we
// handle the onBlur safely. // handle the onBlur safely.
event.target.blur(); (event.target as HTMLInputElement).blur();
} }
}; };
render() { public render(): JSX.Element {
let picker; let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) { if (this.state.custom) {
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
); );
} else { } else {
// Each level must have a definition in this.state.levelRoleMap // Each level must have a definition in this.state.levelRoleMap
let options = this.state.options.map((level) => { const options = this.state.options.map((level) => {
return { return {
value: level, value: String(level),
text: Roles.textualPowerLevel(level, this.props.usersDefault), text: Roles.textualPowerLevel(level, this.props.usersDefault),
}; };
}); });
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
options = options.map((op) => { const optionsElements = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
value={String(this.state.selectValue)} value={String(this.state.selectValue)}
disabled={this.props.disabled} disabled={this.props.disabled}
> >
{ options } { optionsElements }
</Field> </Field>
); );
} }

View file

@ -17,25 +17,34 @@
import React from 'react'; import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason?: string;
contentHtml: string;
}
interface IState {
visible: boolean;
}
@replaceableComponent("views.elements.Spoiler") @replaceableComponent("views.elements.Spoiler")
export default class Spoiler extends React.Component { export default class Spoiler extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
visible: false, visible: false,
}; };
} }
toggleVisible(e) { private toggleVisible = (e: React.MouseEvent): void => {
if (!this.state.visible) { if (!this.state.visible) {
// we are un-blurring, we don't want this click to propagate to potential child pills // we are un-blurring, we don't want this click to propagate to potential child pills
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
this.setState({ visible: !this.state.visible }); this.setState({ visible: !this.state.visible });
} };
render() { public render(): JSX.Element {
const reason = this.props.reason ? ( const reason = this.props.reason ? (
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span> <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
) : null; ) : null;
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
// as such, we pass the this.props.contentHtml instead and then set the raw // as such, we pass the this.props.contentHtml instead and then set the raw
// HTML content. This is secure as the contents have already been parsed previously // HTML content. This is secure as the contents have already been parsed previously
return ( return (
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}> <span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
{ reason } { reason }
&nbsp; &nbsp;
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} /> <span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />

View file

@ -15,40 +15,40 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { highlightBlock } from 'highlight.js'; import { highlightBlock } from 'highlight.js';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
className?: string;
children?: React.ReactNode;
}
@replaceableComponent("views.elements.SyntaxHighlight") @replaceableComponent("views.elements.SyntaxHighlight")
export default class SyntaxHighlight extends React.Component { export default class SyntaxHighlight extends React.Component<IProps> {
static propTypes = { private el: HTMLPreElement = null;
className: PropTypes.string,
children: PropTypes.node,
};
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._ref = this._ref.bind(this);
} }
// componentDidUpdate used here for reusability // componentDidUpdate used here for reusability
componentDidUpdate() { public componentDidUpdate(): void {
if (this._el) highlightBlock(this._el); if (this.el) highlightBlock(this.el);
} }
// call componentDidUpdate because _ref is fired on initial render // call componentDidUpdate because _ref is fired on initial render
// which does not fire componentDidUpdate // which does not fire componentDidUpdate
_ref(el) { private ref = (el: HTMLPreElement): void => {
this._el = el; this.el = el;
this.componentDidUpdate(); this.componentDidUpdate();
} };
render() { public render(): JSX.Element {
const { className, children } = this.props; const { className, children } = this.props;
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}> return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
<code>{ children }</code> <code>{ children }</code>
</pre>; </pre>;
} }
} }

View file

@ -15,42 +15,44 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "./Tooltip";
interface IProps {
class?: string;
tooltipClass?: string;
tooltip: React.ReactNode;
tooltipProps?: {};
onClick?: (ev?: React.MouseEvent) => void;
}
interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.TextWithTooltip") @replaceableComponent("views.elements.TextWithTooltip")
export default class TextWithTooltip extends React.Component { export default class TextWithTooltip extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
class: PropTypes.string, super(props);
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
tooltipProps: PropTypes.object,
};
constructor() {
super();
this.state = { this.state = {
hover: false, hover: false,
}; };
} }
onMouseOver = () => { private onMouseOver = (): void => {
this.setState({ hover: true }); this.setState({ hover: true });
}; };
onMouseLeave = () => { private onMouseLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
}; };
render() { public render(): JSX.Element {
const Tooltip = sdk.getComponent("elements.Tooltip");
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
return ( return (
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}> <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
{ children } { children }
{ this.state.hover && <Tooltip { this.state.hover && <Tooltip
{...tooltipProps} {...tooltipProps}

View file

@ -15,20 +15,20 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import QRCode from "../QRCode"; import QRCode from "../QRCode";
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
interface IProps {
qrCodeData: QRCodeData;
}
@replaceableComponent("views.elements.crypto.VerificationQRCode") @replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent { export default class VerificationQRCode extends React.PureComponent<IProps> {
static propTypes = { public render(): JSX.Element {
qrCodeData: PropTypes.object.isRequired,
};
render() {
return ( return (
<QRCode <QRCode
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]} data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
className="mx_VerificationQRCode" className="mx_VerificationQRCode"
width={196} /> width={196} />
); );

View file

@ -29,6 +29,8 @@ import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader"; import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import { logger } from "matrix-js-sdk/src/logger";
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
async function cacheDownloadIcon() { async function cacheDownloadIcon() {
@ -283,7 +285,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
if (["application/pdf"].includes(fileType) && !fileTooBig) { if (["application/pdf"].includes(fileType) && !fileTooBig) {
// We want to force a download on this type, so use an onClick handler. // We want to force a download on this type, so use an onClick handler.
downloadProps["onClick"] = (e) => { downloadProps["onClick"] = (e) => {
console.log(`Downloading ${fileType} as blob (unencrypted)`); logger.log(`Downloading ${fileType} as blob (unencrypted)`);
// Avoid letting the <a> do its thing // Avoid letting the <a> do its thing
e.preventDefault(); e.preventDefault();

View file

@ -117,6 +117,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
params.fileSize = content.info.size; params.fileSize = content.info.size;
} }
if (this.image.current) {
const clientRect = this.image.current.getBoundingClientRect();
params.thumbnailInfo = {
width: clientRect.width,
height: clientRect.height,
positionX: clientRect.x,
positionY: clientRect.y,
};
}
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
} }
}; };

View file

@ -27,6 +27,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody"; import MFileBody from "./MFileBody";
import { logger } from "matrix-js-sdk/src/logger";
interface IState { interface IState {
decryptedUrl?: string; decryptedUrl?: string;
decryptedThumbnailUrl?: string; decryptedThumbnailUrl?: string;
@ -152,7 +154,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
try { try {
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
if (autoplay) { if (autoplay) {
console.log("Preloading video"); logger.log("Preloading video");
this.setState({ this.setState({
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
decryptedThumbnailUrl: thumbnailUrl, decryptedThumbnailUrl: thumbnailUrl,
@ -160,7 +162,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}); });
this.props.onHeightChanged(); this.props.onHeightChanged();
} else { } else {
console.log("NOT preloading video"); logger.log("NOT preloading video");
const content = this.props.mxEvent.getContent<IMediaEventContent>(); const content = this.props.mxEvent.getContent<IMediaEventContent>();
this.setState({ this.setState({
// For Chrome and Electron, we need to set some non-empty `src` to // For Chrome and Electron, we need to set some non-empty `src` to

View file

@ -71,6 +71,8 @@ import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
export interface IDevice { export interface IDevice {
deviceId: string; deviceId: string;
ambiguous?: boolean; ambiguous?: boolean;
@ -557,7 +559,7 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
cli.kick(member.roomId, member.userId, reason || undefined).then(() => { cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Kick success"); logger.log("Kick success");
}, function(err) { }, function(err) {
console.error("Kick error: " + err); console.error("Kick error: " + err);
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
@ -684,7 +686,7 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
promise.then(() => { promise.then(() => {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Ban success"); logger.log("Ban success");
}, function(err) { }, function(err) {
console.error("Ban error: " + err); console.error("Ban error: " + err);
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
@ -757,7 +759,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Mute toggle success"); logger.log("Mute toggle success");
}, function(err) { }, function(err) {
console.error("Mute error: " + err); console.error("Mute error: " + err);
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
@ -917,7 +919,7 @@ const GroupAdminToolsSection: React.FC<{
_t('Failed to withdraw invitation') : _t('Failed to withdraw invitation') :
_t('Failed to remove user from community'), _t('Failed to remove user from community'),
}); });
console.log(e); logger.log(e);
}).finally(() => { }).finally(() => {
stopUpdating(); stopUpdating();
}); });
@ -1052,8 +1054,7 @@ const PowerLevelEditor: React.FC<{
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
const onPowerChange = useCallback(async (powerLevelStr: string) => { const onPowerChange = useCallback(async (powerLevel: number) => {
const powerLevel = parseInt(powerLevelStr, 10);
setSelectedPowerLevel(powerLevel); setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
@ -1061,7 +1062,7 @@ const PowerLevelEditor: React.FC<{
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Power change success"); logger.log("Power change success");
}, function(err) { }, function(err) {
console.error("Failed to change power level " + err); console.error("Failed to change power level " + err);
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {

View file

@ -28,7 +28,7 @@ import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon, { E2EState } from "../rooms/E2EIcon";
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -189,7 +189,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
// Element Web doesn't support scanning yet, so assume here we're the client being scanned. // Element Web doesn't support scanning yet, so assume here we're the client being scanned.
body = <React.Fragment> body = <React.Fragment>
<p>{ description }</p> <p>{ description }</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} /> <E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
<div className="mx_VerificationPanel_reciprocateButtons"> <div className="mx_VerificationPanel_reciprocateButtons">
<AccessibleButton <AccessibleButton
kind="danger" kind="danger"
@ -252,7 +252,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section"> <div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
<h3>{ _t("Verified") }</h3> <h3>{ _t("Verified") }</h3>
<p>{ description }</p> <p>{ description }</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} /> <E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
{ text ? <p>{ text }</p> : null } { text ? <p>{ text }</p> : null }
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}> <AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{ _t("Got it") } { _t("Got it") }

View file

@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
<AppTile <AppTile
app={app} app={app}
fullWidth fullWidth
show
showMenubar={false} showMenubar={false}
room={room} room={room}
userId={cli.getUserId()} userId={cli.getUserId()}

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Resizable } from "re-resizable"; import { Resizable } from "re-resizable";
@ -26,8 +25,6 @@ import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging'; import * as ScalarMessaging from '../../../ScalarMessaging';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeNotifier from "../../../utils/ResizeNotifier";
import ResizeHandle from "../elements/ResizeHandle"; import ResizeHandle from "../elements/ResizeHandle";
import Resizer from "../../../resizer/resizer"; import Resizer from "../../../resizer/resizer";
@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
import { useStateCallback } from "../../../hooks/useStateCallback"; import { useStateCallback } from "../../../hooks/useStateCallback";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
import { ActionPayload } from "../../../dispatcher/payloads";
interface IProps {
userId: string;
room: Room;
resizeNotifier: ResizeNotifier;
showApps?: boolean; // Should apps be rendered
maxHeight: number;
}
interface IState {
apps: IApp[];
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
resizing: boolean;
}
@replaceableComponent("views.rooms.AppsDrawer") @replaceableComponent("views.rooms.AppsDrawer")
export default class AppsDrawer extends React.Component { export default class AppsDrawer extends React.Component<IProps, IState> {
static propTypes = { private resizeContainer: HTMLDivElement;
userId: PropTypes.string.isRequired, private resizer: Resizer;
room: PropTypes.object.isRequired, private dispatcherRef: string;
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired, public static defaultProps: Partial<IProps> = {
showApps: PropTypes.bool, // Should apps be rendered
};
static defaultProps = {
showApps: true, showApps: true,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
apps: this._getApps(), apps: this.getApps(),
resizingVertical: false, // true when changing the height of the apps drawer resizingVertical: false,
resizingHorizontal: false, // true when chagning the distribution of the width between widgets resizingHorizontal: false,
resizing: false,
}; };
this._resizeContainer = null; this.resizer = this.createResizer();
this.resizer = this._createResizer();
this.props.resizeNotifier.on("isResizing", this.onIsResizing); this.props.resizeNotifier.on("isResizing", this.onIsResizing);
} }
componentDidMount() { public componentDidMount(): void {
ScalarMessaging.startListening(); ScalarMessaging.startListening();
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps); WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { public componentWillUnmount(): void {
ScalarMessaging.stopListening(); ScalarMessaging.stopListening();
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps); WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
if (this._resizeContainer) { if (this.resizeContainer) {
this.resizer.detach(); this.resizer.detach();
} }
this.props.resizeNotifier.off("isResizing", this.onIsResizing); this.props.resizeNotifier.off("isResizing", this.onIsResizing);
} }
onIsResizing = (resizing) => { private onIsResizing = (resizing: boolean): void => {
// This one is the vertical, ie. change height of apps drawer // This one is the vertical, ie. change height of apps drawer
this.setState({ resizingVertical: resizing }); this.setState({ resizingVertical: resizing });
if (!resizing) { if (!resizing) {
this._relaxResizer(); this.relaxResizer();
} }
}; };
_createResizer() { private createResizer(): Resizer {
// This is the horizontal one, changing the distribution of the width between the app tiles // This is the horizontal one, changing the distribution of the width between the app tiles
// (ie. a vertical resize handle because, the handle itself is vertical...) // (ie. a vertical resize handle because, the handle itself is vertical...)
const classNames = { const classNames = {
@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
}; };
const collapseConfig = { const collapseConfig = {
onResizeStart: () => { onResizeStart: () => {
this._resizeContainer.classList.add("mx_AppsDrawer_resizing"); this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
this.setState({ resizingHorizontal: true }); this.setState({ resizingHorizontal: true });
}, },
onResizeStop: () => { onResizeStop: () => {
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing"); this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
WidgetLayoutStore.instance.setResizerDistributions( WidgetLayoutStore.instance.setResizerDistributions(
this.props.room, Container.Top, this.props.room, Container.Top,
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size), this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
}, },
}; };
// pass a truthy container for now, we won't call attach until we update it // pass a truthy container for now, we won't call attach until we update it
const resizer = new Resizer({}, PercentageDistributor, collapseConfig); const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
resizer.setClassNames(classNames); resizer.setClassNames(classNames);
return resizer; return resizer;
} }
_collectResizer = (ref) => { private collectResizer = (ref: HTMLDivElement): void => {
if (this._resizeContainer) { if (this.resizeContainer) {
this.resizer.detach(); this.resizer.detach();
} }
@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
this.resizer.container = ref; this.resizer.container = ref;
this.resizer.attach(); this.resizer.attach();
} }
this._resizeContainer = ref; this.resizeContainer = ref;
this._loadResizerPreferences(); this.loadResizerPreferences();
}; };
_getAppsHash = (apps) => apps.map(app => app.id).join("~"); private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
componentDidUpdate(prevProps, prevState) { public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) { if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
// Room has changed, update apps // Room has changed, update apps
this._updateApps(); this.updateApps();
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { } else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
this._loadResizerPreferences(); this.loadResizerPreferences();
} }
} }
_relaxResizer = () => { private relaxResizer = (): void => {
const distributors = this.resizer.getDistributors(); const distributors = this.resizer.getDistributors();
// relax all items if they had any overconstrained flexboxes // relax all items if they had any overconstrained flexboxes
@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
distributors.forEach(d => d.finish()); distributors.forEach(d => d.finish());
}; };
_loadResizerPreferences = () => { private loadResizerPreferences = (): void => {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top); const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) { if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
distributions.forEach((size, i) => { distributions.forEach((size, i) => {
@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
} }
}; };
isResizing() { private isResizing(): boolean {
return this.state.resizingVertical || this.state.resizingHorizontal; return this.state.resizingVertical || this.state.resizingHorizontal;
} }
onAction = (action) => { private onAction = (action: ActionPayload): void => {
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer'; const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) { switch (action.action) {
case 'appsDrawer': case 'appsDrawer':
@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
} }
}; };
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top); private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
_updateApps = () => { private updateApps = (): void => {
this.setState({ this.setState({
apps: this._getApps(), apps: this.getApps(),
}); });
}; };
_launchManageIntegrations() { public render(): JSX.Element {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
}
}
render() {
if (!this.props.showApps) return <div />; if (!this.props.showApps) return <div />;
const apps = this.state.apps.map((app, index, arr) => { const apps = this.state.apps.map((app, index, arr) => {
@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
className="mx_AppsContainer_resizer" className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
> >
<div className="mx_AppsContainer" ref={this._collectResizer}> <div className="mx_AppsContainer" ref={this.collectResizer}>
{ apps.map((app, i) => { { apps.map((app, i) => {
if (i < 1) return app; if (i < 1) return app;
return <React.Fragment key={app.key}> return <React.Fragment key={app.key}>
@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
} }
} }
const PersistentVResizer = ({ interface IPersistentResizerProps {
room: Room;
minHeight: number;
maxHeight: number;
className: string;
handleWrapperClass: string;
handleClass: string;
resizeNotifier: ResizeNotifier;
children: React.ReactNode;
}
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
room, room,
minHeight, minHeight,
maxHeight, maxHeight,
@ -303,7 +317,7 @@ const PersistentVResizer = ({
}); });
return <Resizable return <Resizable
size={{ height: Math.min(height, maxHeight) }} size={{ height: Math.min(height, maxHeight), width: null }}
minHeight={minHeight} minHeight={minHeight}
maxHeight={maxHeight} maxHeight={maxHeight}
onResizeStart={() => { onResizeStart={() => {

View file

@ -499,9 +499,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
handled = true; handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this.formatBarRef.current.hide(); this.formatBarRef.current.hide();
if (!event.ctrlKey && !event.metaKey) {
handled = this.fakeDeletion(event.key === Key.BACKSPACE);
}
} }
if (handled) { if (handled) {
@ -567,29 +564,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
}; };
/**
* TODO: Remove when Debian moves to newer version of Firefox
* On Firefox 78 no event emitted when the user tries to delete pills.
* Therefore we need to fake what would normally happen
* @param direction in which to delete
* @returns handled
*/
private fakeDeletion(backward: boolean): boolean {
const selection = document.getSelection();
// Use the default handling for ranges
if (selection.type === "Range") return false;
this.modifiedFlag = true;
const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection);
// Do the deletion itself
if (backward) caret.offset--;
const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1);
this.props.model.update(newText, backward ? "deleteContentBackward" : "deleteContentForward", caret);
return true;
}
private async tabCompleteName(): Promise<void> { private async tabCompleteName(): Promise<void> {
try { try {
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve)); await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));

View file

@ -16,41 +16,51 @@ limitations under the License.
*/ */
import React, { useState } from "react"; import React, { useState } from "react";
import PropTypes from "prop-types";
import classNames from 'classnames'; import classNames from 'classnames';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip"; import Tooltip from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils";
export const E2E_STATE = { export enum E2EState {
VERIFIED: "verified", Verified = "verified",
WARNING: "warning", Warning = "warning",
UNKNOWN: "unknown", Unknown = "unknown",
NORMAL: "normal", Normal = "normal",
UNAUTHENTICATED: "unauthenticated", Unauthenticated = "unauthenticated",
}
const crossSigningUserTitles: { [key in E2EState]?: string } = {
[E2EState.Warning]: _td("This user has not verified all of their sessions."),
[E2EState.Normal]: _td("You have not verified this user."),
[E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
};
const crossSigningRoomTitles: { [key in E2EState]?: string } = {
[E2EState.Warning]: _td("Someone is using an unknown session"),
[E2EState.Normal]: _td("This room is end-to-end encrypted"),
[E2EState.Verified]: _td("Everyone in this room is verified"),
}; };
const crossSigningUserTitles = { interface IProps {
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."), isUser?: boolean;
[E2E_STATE.NORMAL]: _td("You have not verified this user."), status?: E2EState | E2EStatus;
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."), className?: string;
}; size?: number;
const crossSigningRoomTitles = { onClick?: () => void;
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"), hideTooltip?: boolean;
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"), bordered?: boolean;
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"), }
};
const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => { const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const classes = classNames({ const classes = classNames({
mx_E2EIcon: true, mx_E2EIcon: true,
mx_E2EIcon_bordered: bordered, mx_E2EIcon_bordered: bordered,
mx_E2EIcon_warning: status === E2E_STATE.WARNING, mx_E2EIcon_warning: status === E2EState.Warning,
mx_E2EIcon_normal: status === E2E_STATE.NORMAL, mx_E2EIcon_normal: status === E2EState.Normal,
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED, mx_E2EIcon_verified: status === E2EState.Verified,
}, className); }, className);
let e2eTitle; let e2eTitle;
@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
</div>; </div>;
}; };
E2EIcon.propTypes = {
isUser: PropTypes.bool,
status: PropTypes.oneOf(Object.values(E2E_STATE)),
className: PropTypes.string,
size: PropTypes.number,
onClick: PropTypes.func,
};
export default E2EIcon; export default E2EIcon;

View file

@ -44,6 +44,8 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { logger } from "matrix-js-sdk/src/logger";
function getHtmlReplyFallback(mxEvent: MatrixEvent): string { function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body; const html = mxEvent.getContent().formatted_body;
if (!html) { if (!html) {
@ -308,7 +310,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
description: errText, description: errText,
}); });
} else { } else {
console.log("Command success."); logger.log("Command success.");
if (messageContent) return messageContent; if (messageContent) return messageContent;
} }
} }

View file

@ -20,7 +20,7 @@ import React from 'react';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { _td } from '../../../languageHandler'; import { _td } from '../../../languageHandler';
import classNames from "classnames"; import classNames from "classnames";
import E2EIcon from './E2EIcon'; import E2EIcon, { E2EState } from './E2EIcon';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseAvatar from '../avatars/BaseAvatar'; import BaseAvatar from '../avatars/BaseAvatar';
import PresenceLabel from "./PresenceLabel"; import PresenceLabel from "./PresenceLabel";
@ -75,7 +75,7 @@ interface IProps {
suppressOnHover?: boolean; suppressOnHover?: boolean;
showPresence?: boolean; showPresence?: boolean;
subtextLabel?: string; subtextLabel?: string;
e2eStatus?: string; e2eStatus?: E2EState;
powerStatus?: PowerStatus; powerStatus?: PowerStatus;
} }

View file

@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList"; import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { E2E_STATE } from "./E2EIcon"; import { E2EState } from "./E2EIcon";
import { toRem } from "../../../utils/units"; import { toRem } from "../../../utils/units";
import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetType } from "../../../widgets/WidgetType";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
@ -521,7 +521,7 @@ export default class EventTile extends React.Component<IProps, IState> {
const thread = this.state.thread; const thread = this.state.thread;
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (!thread || this.props.showThreadInfo === false) { if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
return null; return null;
} }
@ -605,7 +605,7 @@ export default class EventTile extends React.Component<IProps, IState> {
if (encryptionInfo.mismatchedSender) { if (encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here // something definitely wrong is going on here
this.setState({ this.setState({
verified: E2E_STATE.WARNING, verified: E2EState.Warning,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
return; return;
} }
@ -613,7 +613,7 @@ export default class EventTile extends React.Component<IProps, IState> {
if (!userTrust.isCrossSigningVerified()) { if (!userTrust.isCrossSigningVerified()) {
// user is not verified, so default to everything is normal // user is not verified, so default to everything is normal
this.setState({ this.setState({
verified: E2E_STATE.NORMAL, verified: E2EState.Normal,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
return; return;
} }
@ -623,27 +623,27 @@ export default class EventTile extends React.Component<IProps, IState> {
); );
if (!eventSenderTrust) { if (!eventSenderTrust) {
this.setState({ this.setState({
verified: E2E_STATE.UNKNOWN, verified: E2EState.Unknown,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
return; return;
} }
if (!eventSenderTrust.isVerified()) { if (!eventSenderTrust.isVerified()) {
this.setState({ this.setState({
verified: E2E_STATE.WARNING, verified: E2EState.Warning,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
return; return;
} }
if (!encryptionInfo.authenticated) { if (!encryptionInfo.authenticated) {
this.setState({ this.setState({
verified: E2E_STATE.UNAUTHENTICATED, verified: E2EState.Unauthenticated,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
return; return;
} }
this.setState({ this.setState({
verified: E2E_STATE.VERIFIED, verified: E2EState.Verified,
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
} }
@ -850,13 +850,13 @@ export default class EventTile extends React.Component<IProps, IState> {
// event is encrypted, display padlock corresponding to whether or not it is verified // event is encrypted, display padlock corresponding to whether or not it is verified
if (ev.isEncrypted()) { if (ev.isEncrypted()) {
if (this.state.verified === E2E_STATE.NORMAL) { if (this.state.verified === E2EState.Normal) {
return; // no icon if we've not even cross-signed the user return; // no icon if we've not even cross-signed the user
} else if (this.state.verified === E2E_STATE.VERIFIED) { } else if (this.state.verified === E2EState.Verified) {
return; // no icon for verified return; // no icon for verified
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) { } else if (this.state.verified === E2EState.Unauthenticated) {
return (<E2ePadlockUnauthenticated />); return (<E2ePadlockUnauthenticated />);
} else if (this.state.verified === E2E_STATE.UNKNOWN) { } else if (this.state.verified === E2EState.Unknown) {
return (<E2ePadlockUnknown />); return (<E2ePadlockUnknown />);
} else { } else {
return (<E2ePadlockUnverified />); return (<E2ePadlockUnverified />);
@ -961,9 +961,9 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_lastInSection: this.props.lastInSection, mx_EventTile_lastInSection: this.props.lastInSection,
mx_EventTile_contextual: this.props.contextual, mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused, mx_EventTile_actionBarFocused: this.state.actionBarFocused,
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED, mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING, mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN, mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
mx_EventTile_bad: isEncryptionFailure, mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender, mx_EventTile_noSender: this.props.hideSender,

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { ComponentProps, createRef } from 'react';
import { AllHtmlEntities } from 'html-entities'; import { AllHtmlEntities } from 'html-entities';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client'; import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
@ -36,6 +36,7 @@ interface IProps {
@replaceableComponent("views.rooms.LinkPreviewWidget") @replaceableComponent("views.rooms.LinkPreviewWidget")
export default class LinkPreviewWidget extends React.Component<IProps> { export default class LinkPreviewWidget extends React.Component<IProps> {
private readonly description = createRef<HTMLDivElement>(); private readonly description = createRef<HTMLDivElement>();
private image = createRef<HTMLImageElement>();
componentDidMount() { componentDidMount() {
if (this.description.current) { if (this.description.current) {
@ -59,7 +60,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
src = mediaFromMxc(src).srcHttp; src = mediaFromMxc(src).srcHttp;
} }
const params = { const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
src: src, src: src,
width: p["og:image:width"], width: p["og:image:width"],
height: p["og:image:height"], height: p["og:image:height"],
@ -68,6 +69,17 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
link: this.props.link, link: this.props.link,
}; };
if (this.image.current) {
const clientRect = this.image.current.getBoundingClientRect();
params.thumbnailInfo = {
width: clientRect.width,
height: clientRect.height,
positionX: clientRect.x,
positionY: clientRect.y,
};
}
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
}; };
@ -100,7 +112,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
let img; let img;
if (image) { if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}> img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} /> <img ref={this.image} style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
</div>; </div>;
} }

View file

@ -56,6 +56,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics"; import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import { logger } from "matrix-js-sdk/src/logger";
function addReplyToMessageContent( function addReplyToMessageContent(
content: IContent, content: IContent,
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
@ -341,7 +343,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
description: errText, description: errText,
}); });
} else { } else {
console.log("Command success."); logger.log("Command success.");
if (messageContent) return messageContent; if (messageContent) return messageContent;
} }
} }

View file

@ -32,6 +32,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads'; import { ActionPayload } from '../../../dispatcher/payloads';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
import { IApp } from "../../../stores/WidgetStore";
import { logger } from "matrix-js-sdk/src/logger";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu. // We sit in a context menu, so this should be given to the context menu.
@ -98,11 +101,11 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private removeStickerpickerWidgets = async (): Promise<void> => { private removeStickerpickerWidgets = async (): Promise<void> => {
const scalarClient = await this.acquireScalarClient(); const scalarClient = await this.acquireScalarClient();
console.log('Removing Stickerpicker widgets'); logger.log('Removing Stickerpicker widgets');
if (this.state.widgetId) { if (this.state.widgetId) {
if (scalarClient) { if (scalarClient) {
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => { scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
console.log('Assets disabled'); logger.log('Assets disabled');
}).catch((err) => { }).catch((err) => {
console.error('Failed to disable assets'); console.error('Failed to disable assets');
}); });
@ -256,12 +259,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack"); stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
// FIXME: could this use the same code as other apps? // FIXME: could this use the same code as other apps?
const stickerApp = { const stickerApp: IApp = {
id: stickerpickerWidget.id, id: stickerpickerWidget.id,
url: stickerpickerWidget.content.url, url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name, name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type, type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data, data: stickerpickerWidget.content.data,
roomId: stickerpickerWidget.content.roomId,
eventId: stickerpickerWidget.content.eventId,
avatar_url: stickerpickerWidget.content.avatar_url,
creatorUserId: stickerpickerWidget.content.creatorUserId,
}; };
stickersContent = ( stickersContent = (
@ -287,9 +294,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
onEditClick={this.launchManageIntegrations} onEditClick={this.launchManageIntegrations}
onDeleteClick={this.removeStickerpickerWidgets} onDeleteClick={this.removeStickerpickerWidgets}
showTitle={false} showTitle={false}
showCancel={false}
showPopout={false} showPopout={false}
onMinimiseClick={this.onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
userWidget={true} userWidget={true}
/> />
@ -345,16 +350,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
}); });
}; };
/**
* Trigger hiding of the sticker picker overlay
* @param {Event} ev Event that triggered the function call
*/
private onHideStickersClick = (ev: React.MouseEvent): void => {
if (this.props.showStickers) {
this.props.setShowStickers(false);
}
};
/** /**
* Called when the window is resized * Called when the window is resized
*/ */

View file

@ -97,9 +97,9 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
const secretStorage = cli.crypto.secretStorage; const secretStorage = cli.crypto.secretStorage;
const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId()); const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId());
const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage)); const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage));
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); const masterPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("master")));
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing")); const selfSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("self_signing")));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing")); const userSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("user_signing")));
const homeserverSupportsCrossSigning = const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const crossSigningReady = await cli.isCrossSigningReady(); const crossSigningReady = await cli.isCrossSigningReady();

View file

@ -262,7 +262,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
} }
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
if (beforeChange && !await beforeChange(joinRule)) return; if (beforeChange && !(await beforeChange(joinRule))) return;
const newContent: IJoinRuleEventContent = { const newContent: IJoinRuleEventContent = {
join_rule: joinRule, join_rule: joinRule,

View file

@ -27,6 +27,8 @@ import { mediaFromMxc } from "../../../customisations/Media";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import AvatarSetting from './AvatarSetting'; import AvatarSetting from './AvatarSetting';
import { logger } from "matrix-js-sdk/src/logger";
interface IState { interface IState {
userId?: string; userId?: string;
originalDisplayName?: string; originalDisplayName?: string;
@ -104,7 +106,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
} }
if (this.state.avatarFile) { if (this.state.avatarFile) {
console.log( logger.log(
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` + `Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
` (${this.state.avatarFile.size}) bytes`); ` (${this.state.avatarFile.size}) bytes`);
const uri = await client.uploadContent(this.state.avatarFile); const uri = await client.uploadContent(this.state.avatarFile);
@ -116,7 +118,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
} }
} catch (err) { } catch (err) {
console.log("Failed to save profile", err); logger.log("Failed to save profile", err);
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, { Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
title: _t("Failed to save your profile"), title: _t("Failed to save your profile"),
description: ((err && err.message) ? err.message : _t("The operation could not be completed")), description: ((err && err.message) ? err.message : _t("The operation could not be completed")),

View file

@ -43,6 +43,8 @@ interface IState {
sessionsRemaining: number; sessionsRemaining: number;
} }
import { logger } from "matrix-js-sdk/src/logger";
@replaceableComponent("views.settings.SecureBackupPanel") @replaceableComponent("views.settings.SecureBackupPanel")
export default class SecureBackupPanel extends React.PureComponent<{}, IState> { export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private unmounted = false; private unmounted = false;
@ -109,7 +111,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
backupSigStatus: trustInfo, backupSigStatus: trustInfo,
}); });
} catch (e) { } catch (e) {
console.log("Unable to fetch check backup status", e); logger.log("Unable to fetch check backup status", e);
if (this.unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,
@ -134,7 +136,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
backupSigStatus, backupSigStatus,
}); });
} catch (e) { } catch (e) {
console.log("Unable to fetch key backup status", e); logger.log("Unable to fetch key backup status", e);
if (this.unmounted) return; if (this.unmounted) return;
this.setState({ this.setState({
loading: false, loading: false,

View file

@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
} }
} }
private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => { private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const eventsLevelPrefix = "event_levels_"; const eventsLevelPrefix = "event_levels_";
const value = parseInt(inputValue);
if (powerLevelKey.startsWith(eventsLevelPrefix)) { if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy // deep copy "events" object, Object.assign itself won't deep copy
plContent["events"] = Object.assign({}, plContent["events"] || {}); plContent["events"] = Object.assign({}, plContent["events"] || {});
@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
}); });
}; };
private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => { private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');

View file

@ -32,6 +32,8 @@ import { toRightOf } from "../../../../structures/ContextMenu";
import BugReportDialog from '../../../dialogs/BugReportDialog'; import BugReportDialog from '../../../dialogs/BugReportDialog';
import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu"; import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
closeSettingsFn: () => void; closeSettingsFn: () => void;
} }
@ -88,7 +90,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
// Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
// stopping in the middle of the logs. // stopping in the middle of the logs.
console.log("Clear cache & reload clicked"); logger.log("Clear cache & reload clicked");
MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().then(() => { MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload(); PlatformPeg.get().reload();

View file

@ -233,7 +233,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar(); const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
let alwaysShowMenuBar = true; let alwaysShowMenuBar = true;
if (alwaysShowMenuBarSupported) { if (alwaysShowMenuBarSupported) {
alwaysShowMenuBar = !await platform.getAutoHideMenuBarEnabled(); alwaysShowMenuBar = !(await platform.getAutoHideMenuBarEnabled());
} }
const minimizeToTraySupported = await platform.supportsMinimizeToTray(); const minimizeToTraySupported = await platform.supportsMinimizeToTray();

View file

@ -28,6 +28,8 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import SettingsFlag from '../../../elements/SettingsFlag'; import SettingsFlag from '../../../elements/SettingsFlag';
import ErrorDialog from '../../../dialogs/ErrorDialog'; import ErrorDialog from '../../../dialogs/ErrorDialog';
import { logger } from "matrix-js-sdk/src/logger";
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => { const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
// Note we're looking for a device with deviceId 'default' but adding a device // Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device // with deviceId == the empty string: this is because Chrome gives us a device
@ -101,7 +103,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
} }
} }
if (error) { if (error) {
console.log("Failed to list userMedia devices", error); logger.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'), title: _t('No media permissions'),

View file

@ -30,6 +30,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { EventSubscription } from 'fbemitter'; import { EventSubscription } from 'fbemitter';
import PictureInPictureDragger from './PictureInPictureDragger'; import PictureInPictureDragger from './PictureInPictureDragger';
import { logger } from "matrix-js-sdk/src/logger";
const SHOW_CALL_IN_STATES = [ const SHOW_CALL_IN_STATES = [
CallState.Connected, CallState.Connected,
CallState.InviteSent, CallState.InviteSent,
@ -78,7 +80,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[
if (secondaries.length > 1) { if (secondaries.length > 1) {
// We should never be in more than two calls so this shouldn't happen // We should never be in more than two calls so this shouldn't happen
console.log("Found more than 1 secondary call! Other calls will not be shown."); logger.log("Found more than 1 secondary call! Other calls will not be shown.");
} }
return [primary, secondaries]; return [primary, secondaries];

View file

@ -214,6 +214,8 @@ export default class CallView extends React.Component<IProps, IState> {
this.setState({ this.setState({
primaryFeed: primary, primaryFeed: primary,
secondaryFeeds: secondary, secondaryFeeds: secondary,
micMuted: this.props.call.isMicrophoneMuted(),
vidMuted: this.props.call.isLocalVideoMuted(),
}); });
}; };
@ -258,18 +260,14 @@ export default class CallView extends React.Component<IProps, IState> {
return { primary, secondary }; return { primary, secondary };
} }
private onMicMuteClick = (): void => { private onMicMuteClick = async (): Promise<void> => {
const newVal = !this.state.micMuted; const newVal = !this.state.micMuted;
this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
this.props.call.setMicrophoneMuted(newVal);
this.setState({ micMuted: newVal });
}; };
private onVidMuteClick = (): void => { private onVidMuteClick = async (): Promise<void> => {
const newVal = !this.state.vidMuted; const newVal = !this.state.vidMuted;
this.setState({ vidMuted: await this.props.call.setLocalVideoMuted(newVal) });
this.props.call.setLocalVideoMuted(newVal);
this.setState({ vidMuted: newVal });
}; };
private onScreenshareClick = async (): Promise<void> => { private onScreenshareClick = async (): Promise<void> => {

View file

@ -1,12 +1,9 @@
{ {
"Continue": "واصِل", "Continue": "واصِل",
"Username available": "اسم المستخدم متاح",
"Username not available": "الإسم المستخدم غير موجود",
"Something went wrong!": "هناك خطأ ما!", "Something went wrong!": "هناك خطأ ما!",
"Cancel": "إلغاء", "Cancel": "إلغاء",
"Close": "إغلاق", "Close": "إغلاق",
"Create new room": "إنشاء غرفة جديدة", "Create new room": "إنشاء غرفة جديدة",
"Custom Server Options": "الإعدادات الشخصية للخادوم",
"Dismiss": "أهمِل", "Dismiss": "أهمِل",
"Failed to change password. Is your password correct?": "فشلت عملية تعديل الكلمة السرية. هل كلمتك السرية صحيحة ؟", "Failed to change password. Is your password correct?": "فشلت عملية تعديل الكلمة السرية. هل كلمتك السرية صحيحة ؟",
"Warning": "تنبيه", "Warning": "تنبيه",
@ -26,36 +23,24 @@
"Unavailable": "غير متوفر", "Unavailable": "غير متوفر",
"All Rooms": "كل الغُرف", "All Rooms": "كل الغُرف",
"All messages": "كل الرسائل", "All messages": "كل الرسائل",
"All notifications are currently disabled for all targets.": "كل التنبيهات غير مفعلة حالياً للجميع.",
"Direct Chat": "دردشة مباشرة",
"Please set a password!": "يرجى تعيين كلمة مرور !",
"You have successfully set a password!": "تم تعيين كلمة السر بنجاح !",
"Can't update user notification settings": "لا يمكن تحديث إعدادات الإشعارات الخاصة بالمستخدم",
"Explore Room State": "إكتشاف حالة الغرفة", "Explore Room State": "إكتشاف حالة الغرفة",
"All messages (noisy)": "كل الرسائل (صوت مرتفع)",
"Update": "تحديث", "Update": "تحديث",
"What's New": "آخِر المُستجدّات", "What's New": "آخِر المُستجدّات",
"Toolbox": "علبة الأدوات", "Toolbox": "علبة الأدوات",
"Collecting logs": "تجميع السجلات", "Collecting logs": "تجميع السجلات",
"No update available.": "لا يوجد هناك أي تحديث.", "No update available.": "لا يوجد هناك أي تحديث.",
"An error occurred whilst saving your email notification preferences.": "حدث خطأ ما أثناء عملية حفظ إعدادات الإشعارات عبر البريد الإلكتروني.",
"Collecting app version information": "تجميع المعلومات حول نسخة التطبيق", "Collecting app version information": "تجميع المعلومات حول نسخة التطبيق",
"Changelog": "سِجل التغييرات", "Changelog": "سِجل التغييرات",
"Send Account Data": "إرسال بيانات الحساب", "Send Account Data": "إرسال بيانات الحساب",
"Waiting for response from server": "في انتظار الرد مِن الخادوم", "Waiting for response from server": "في انتظار الرد مِن الخادوم",
"Send logs": "إرسال السِجلات", "Send logs": "إرسال السِجلات",
"Download this file": "تنزيل هذا الملف",
"Thank you!": "شكرًا !", "Thank you!": "شكرًا !",
"Advanced notification settings": "الإعدادات المتقدمة للإشعارات",
"Call invitation": "دعوة لمحادثة", "Call invitation": "دعوة لمحادثة",
"Developer Tools": "أدوات التطوير", "Developer Tools": "أدوات التطوير",
"Downloading update...": "عملية تنزيل التحديث جارية …", "Downloading update...": "عملية تنزيل التحديث جارية …",
"State Key": "مفتاح الحالة", "State Key": "مفتاح الحالة",
"Back": "العودة", "Back": "العودة",
"What's new?": "ما الجديد ؟", "What's new?": "ما الجديد ؟",
"You have successfully set a password and an email address!": "لقد قمت بتعيين كلمة سرية و إدخال عنوان للبريد الإلكتروني بنجاح !",
"Cancel Sending": "إلغاء الإرسال",
"Set Password": "تعيين كلمة سرية",
"Checking for an update...": "البحث عن تحديث …", "Checking for an update...": "البحث عن تحديث …",
"powered by Matrix": "مشغل بواسطة Matrix", "powered by Matrix": "مشغل بواسطة Matrix",
"The platform you're on": "المنصة التي أنت عليها", "The platform you're on": "المنصة التي أنت عليها",
@ -65,12 +50,12 @@
"Confirm adding this email address by using Single Sign On to prove your identity.": "أكّد إضافتك لعنوان البريد هذا باستعمال الولوج الموحّد لإثبات هويّتك.", "Confirm adding this email address by using Single Sign On to prove your identity.": "أكّد إضافتك لعنوان البريد هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
"Single Sign On": "الولوج الموحّد", "Single Sign On": "الولوج الموحّد",
"Confirm adding email": "أكّد إضافة البريد الإلكتروني", "Confirm adding email": "أكّد إضافة البريد الإلكتروني",
"Click the button below to confirm adding this email address.": "انقر الزر أسفله لتأكيد إضافة عنوان البريد الإلكتروني هذا.", "Click the button below to confirm adding this email address.": "انقر الزر بالأسفل لتأكيد إضافة عنوان البريد الإلكتروني هذا.",
"Confirm": "أكّد", "Confirm": "أكّد",
"Add Email Address": "أضِف بريدًا إلكترونيًا", "Add Email Address": "أضِف بريدًا إلكترونيًا",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "أكّد إضافتك لرقم الهاتف هذا باستعمال الولوج الموحّد لإثبات هويّتك.", "Confirm adding this phone number by using Single Sign On to prove your identity.": "أكّد إضافتك لرقم الهاتف هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
"Confirm adding phone number": "أكّد إضافة رقم الهاتف", "Confirm adding phone number": "أكّد إضافة رقم الهاتف",
"Click the button below to confirm adding this phone number.": "انقر الزر أسفله لتأكيد إضافة رقم الهاتف هذا.", "Click the button below to confirm adding this phone number.": "انقر الزر بالأسفل لتأكيد إضافة رقم الهاتف هذا.",
"Add Phone Number": "أضِف رقم الهاتف", "Add Phone Number": "أضِف رقم الهاتف",
"Which officially provided instance you are using, if any": "السيرورة المقدّمة رسميًا التي تستعملها، لو وُجدت", "Which officially provided instance you are using, if any": "السيرورة المقدّمة رسميًا التي تستعملها، لو وُجدت",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "فيما إذا كنت تستعمل %(brand)s على جهاز اللمس فيه هو طريقة الإدخال الرئيسة", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "فيما إذا كنت تستعمل %(brand)s على جهاز اللمس فيه هو طريقة الإدخال الرئيسة",
@ -78,19 +63,14 @@
"Whether you're using %(brand)s as an installed Progressive Web App": "ما إذا كنت تستعمل %(brand)s كتطبيق وِب تدرّجي", "Whether you're using %(brand)s as an installed Progressive Web App": "ما إذا كنت تستعمل %(brand)s كتطبيق وِب تدرّجي",
"Your user agent": "وكيل المستخدم الذي تستعمله", "Your user agent": "وكيل المستخدم الذي تستعمله",
"Unable to load! Check your network connectivity and try again.": "تعذر التحميل! افحص اتصالك بالشبكة وأعِد المحاولة.", "Unable to load! Check your network connectivity and try again.": "تعذر التحميل! افحص اتصالك بالشبكة وأعِد المحاولة.",
"Call Timeout": "انتهت مهلة الاتصال",
"Call failed due to misconfigured server": "فشل الاتصال بسبب سوء ضبط الخادوم", "Call failed due to misconfigured server": "فشل الاتصال بسبب سوء ضبط الخادوم",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "من فضلك اطلب من مسؤول الخادوم المنزل الذي تستعمله (<code>%(homeserverDomain)s</code>) أن يضبط خادوم TURN كي تعمل الاتصالات بنحوٍ يكون محط ثقة.", "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "من فضلك اطلب من مسؤول الخادوم المنزل الذي تستعمله (<code>%(homeserverDomain)s</code>) أن يضبط خادوم TURN كي تعمل الاتصالات بنحوٍ يكون محط ثقة.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "أو يمكنك محاولة الخادوم العمومي <code>turn.matrix.org</code> إلا أنه لن يكون محطّ ثقة إذ سيُشارك عنوان IP لديك بذاك الخادوم. يمكنك أيضًا إدارة هذا من الإعدادات.", "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "أو يمكنك محاولة الخادوم العمومي <code>turn.matrix.org</code> إلا أنه لن يكون محطّ ثقة إذ سيُشارك عنوان IP لديك بذاك الخادوم. يمكنك أيضًا إدارة هذا من الإعدادات.",
"Try using turn.matrix.org": "جرّب استعمال turn.matrix.org", "Try using turn.matrix.org": "جرّب استعمال turn.matrix.org",
"OK": "حسنًا", "OK": "حسنًا",
"Unable to capture screen": "تعذر التقاط الشاشة",
"Call Failed": "فشل الاتصال", "Call Failed": "فشل الاتصال",
"You are already in a call.": "تُجري مكالمة الآن.",
"VoIP is unsupported": "تقنية VoIP غير مدعومة", "VoIP is unsupported": "تقنية VoIP غير مدعومة",
"You cannot place VoIP calls in this browser.": "لا يمكنك إجراء مكالمات VoIP عبر هذا المتصفح.", "You cannot place VoIP calls in this browser.": "لا يمكنك إجراء مكالمات VoIP عبر هذا المتصفح.",
"A call is currently being placed!": "يجري إجراء المكالمة!",
"A call is already in progress!": "تُجري مكالمة الآن فعلًا!",
"Permission Required": "التصريح مطلوب", "Permission Required": "التصريح مطلوب",
"You do not have permission to start a conference call in this room": "ينقصك تصريح بدء مكالمة جماعية في هذه الغرفة", "You do not have permission to start a conference call in this room": "ينقصك تصريح بدء مكالمة جماعية في هذه الغرفة",
"Replying With Files": "الرد مع ملفات", "Replying With Files": "الرد مع ملفات",
@ -127,7 +107,7 @@
"PM": "م", "PM": "م",
"AM": "ص", "AM": "ص",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "%(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 %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s %(time)s",
"Who would you like to add to this community?": "مَن تريد إضافته إلى هذا المجتمع؟", "Who would you like to add to this community?": "مَن تريد إضافته إلى هذا المجتمع؟",
@ -155,10 +135,6 @@
"Unable to enable Notifications": "تعذر تفعيل التنبيهات", "Unable to enable Notifications": "تعذر تفعيل التنبيهات",
"This email address was not found": "لم يوجد عنوان البريد الإلكتروني هذا", "This email address was not found": "لم يوجد عنوان البريد الإلكتروني هذا",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "لا يظهر بأن عنوان بريدك مرتبط بمعرّف «ماترِكس» على الخادوم المنزل هذا.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "لا يظهر بأن عنوان بريدك مرتبط بمعرّف «ماترِكس» على الخادوم المنزل هذا.",
"Use your account to sign in to the latest version": "استخدم حسابك للدخول الى الاصدار الاخير",
"Were excited to announce Riot is now Element": "نحن سعيدون باعلان ان Riot اصبح الان Element",
"Riot is now Element!": "Riot اصبح الان Element!",
"Learn More": "تعلم المزيد",
"Sign In or Create Account": "لِج أو أنشِئ حسابًا", "Sign In or Create Account": "لِج أو أنشِئ حسابًا",
"Use your account or create a new one to continue.": "استعمل حسابك أو أنشِئ واحدًا جديدًا للمواصلة.", "Use your account or create a new one to continue.": "استعمل حسابك أو أنشِئ واحدًا جديدًا للمواصلة.",
"Create Account": "أنشِئ حسابًا", "Create Account": "أنشِئ حسابًا",
@ -171,7 +147,6 @@
"Failed to invite": "فشلت الدعوة", "Failed to invite": "فشلت الدعوة",
"Operation failed": "فشلت العملية", "Operation failed": "فشلت العملية",
"Failed to invite users to the room:": "فشلت دعوة المستخدمين إلى الغرفة:", "Failed to invite users to the room:": "فشلت دعوة المستخدمين إلى الغرفة:",
"Failed to invite the following users to the %(roomName)s room:": "فشلت دعوة المستخدمين الآتية أسمائهم إلى غرفة %(roomName)s:",
"You need to be logged in.": "عليك الولوج.", "You need to be logged in.": "عليك الولوج.",
"You need to be able to invite users to do that.": "يجب أن تكون قادرًا على دعوة المستخدمين للقيام بذلك.", "You need to be able to invite users to do that.": "يجب أن تكون قادرًا على دعوة المستخدمين للقيام بذلك.",
"Unable to create widget.": "غير قادر على إنشاء Widget.", "Unable to create widget.": "غير قادر على إنشاء Widget.",
@ -193,9 +168,6 @@
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "ادخل احد الرموز ¯\\_(ツ)_/¯ قبل نص الرسالة", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "ادخل احد الرموز ¯\\_(ツ)_/¯ قبل نص الرسالة",
"Sends a message as plain text, without interpreting it as markdown": "ارسال رسالة كنص، دون تفسيرها على انها معلمات", "Sends a message as plain text, without interpreting it as markdown": "ارسال رسالة كنص، دون تفسيرها على انها معلمات",
"Sends a message as html, without interpreting it as markdown": "ارسال رسالة بشكل HTML، دون تفسيرها على انها معلمات", "Sends a message as html, without interpreting it as markdown": "ارسال رسالة بشكل HTML، دون تفسيرها على انها معلمات",
"Searches DuckDuckGo for results": "البحث في DuckDuckGo للحصول على نتائج",
"/ddg is not a command": "/ddg ليس امر",
"To use it, just wait for autocomplete results to load and tab through them.": "لاستخدامها، فقط انتظر حتى يكتمل تحميل النتائج والمرور عليها.",
"Upgrades a room to a new version": "ترقية الغرفة الى الاصدار الجديد", "Upgrades a room to a new version": "ترقية الغرفة الى الاصدار الجديد",
"You do not have the required permissions to use this command.": "ليس لديك الأذونات المطلوبة لاستخدام هذا الأمر.", "You do not have the required permissions to use this command.": "ليس لديك الأذونات المطلوبة لاستخدام هذا الأمر.",
"Error upgrading room": "خطأ في ترقية الغرفة", "Error upgrading room": "خطأ في ترقية الغرفة",
@ -252,26 +224,6 @@
"Sends a message to the given user": "يرسل رسالة الى المستخدم المعطى", "Sends a message to the given user": "يرسل رسالة الى المستخدم المعطى",
"Displays action": "يعرض إجراءً", "Displays action": "يعرض إجراءً",
"Reason": "السبب", "Reason": "السبب",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s قبل دعوة %(displayName)s.",
"%(targetName)s accepted an invitation.": "%(targetName)s قبل الدعوة.",
"%(senderName)s requested a VoIP conference.": "%(senderName)s طلب مكالمة VoIP جماعية.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s دعا %(targetName)s.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s حظر %(targetName)s.",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s غير اسم العرض الخاص به الى %(displayName)s.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s حدد اسم العرض:%(displayName)s.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ازال اسم العرض (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s ازال صورة البروفايل الخاصة به.",
"%(senderName)s changed their profile picture.": "%(senderName)s غير صورة البروفايل الخاصة به.",
"%(senderName)s set a profile picture.": "%(senderName)s غير صورة البروفايل الخاصة به.",
"%(senderName)s made no change.": "%(senderName)s لم يقم باية تعديلات.",
"VoIP conference started.": "بدأ اجتماع VoIP.",
"%(targetName)s joined the room.": "%(targetName)s انضم الى الغرفة.",
"VoIP conference finished.": "انتهى اجتماع VoIP.",
"%(targetName)s rejected the invitation.": "%(targetName)s رفض الدعوة.",
"%(targetName)s left the room.": "%(targetName)s غادر الغرفة.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s الغى الحظر على %(targetName)s.",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s سحب دعوة %(targetName)s.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s طرد %(targetName)s.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s غير الموضوع الى \"%(topic)s\".", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s غير الموضوع الى \"%(topic)s\".",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ازال اسم الغرفة.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ازال اسم الغرفة.",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s غير اسم الغرفة من %(oldRoomName)s الى %(newRoomName)s.", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s غير اسم الغرفة من %(oldRoomName)s الى %(newRoomName)s.",
@ -297,12 +249,6 @@
"%(senderName)s changed the main and alternative addresses for this room.": "قام %(senderName)s بتعديل العناوين الرئيسية و البديلة لهذه الغرفة.", "%(senderName)s changed the main and alternative addresses for this room.": "قام %(senderName)s بتعديل العناوين الرئيسية و البديلة لهذه الغرفة.",
"%(senderName)s changed the addresses for this room.": "قام %(senderName)s بتعديل عناوين هذه الغرفة.", "%(senderName)s changed the addresses for this room.": "قام %(senderName)s بتعديل عناوين هذه الغرفة.",
"Someone": "شخص ما", "Someone": "شخص ما",
"(not supported by this browser)": "(غير مدعوم في هذا المتصفح)",
"%(senderName)s answered the call.": "%(senderName)s رد على المكالمة.",
"(could not connect media)": "(غير قادر على الاتصال بالوسيط)",
"(no answer)": "(لايوجد رد)",
"(unknown failure: %(reason)s)": "(فشل غير معروف:%(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s انهى المكالمة.",
"%(senderName)s placed a voice call.": "أجرى %(senderName)s مكالمة صوتية.", "%(senderName)s placed a voice call.": "أجرى %(senderName)s مكالمة صوتية.",
"%(senderName)s placed a voice call. (not supported by this browser)": "أجرى %(senderName)s مكالمة صوتية. (غير متوافقة مع هذا المتصفح)", "%(senderName)s placed a voice call. (not supported by this browser)": "أجرى %(senderName)s مكالمة صوتية. (غير متوافقة مع هذا المتصفح)",
"%(senderName)s placed a video call.": "أجرى %(senderName)s مكالمة فيديو.", "%(senderName)s placed a video call.": "أجرى %(senderName)s مكالمة فيديو.",
@ -325,11 +271,7 @@
"e.g. <CurrentPageURL>": "مثال: <عنوان_الصفحة_الحالية>", "e.g. <CurrentPageURL>": "مثال: <عنوان_الصفحة_الحالية>",
"Your device resolution": "ميز الجهاز لديك", "Your device resolution": "ميز الجهاز لديك",
"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.": "على الرغم من احتواء هذه الصفحة على معلومات تُحدّد الهويّة (مثل معرّف الغرفة والمستخدم والمجموعة) إلّا أن هذه البيانات تُحذف قبل إرسالها إلى الخادوم.",
"The remote side failed to pick up": "لم يردّ الطرف الآخر",
"Existing Call": "مكالمة جارية",
"You cannot place a call with yourself.": "لا يمكنك الاتصال بنفسك.", "You cannot place a call with yourself.": "لا يمكنك الاتصال بنفسك.",
"Call in Progress": "إجراء المكالمة جارٍ",
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "يرجى تثبيت <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(اسم المرسل)S إزالة القاعدة التي تحظر المستخدمين المتطابقين %(عام)s", "%(senderName)s removed the rule banning users matching %(glob)s": "%(اسم المرسل)S إزالة القاعدة التي تحظر المستخدمين المتطابقين %(عام)s",
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(اسم المرسل)s إزالة القاعدة التي تحظر الغرف المتطابقة %(عام)s", "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(اسم المرسل)s إزالة القاعدة التي تحظر الغرف المتطابقة %(عام)s",
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(اسم المرسل)s إزالة القاعدة التي تحظر الغرف المتطابقة %(عام)s", "%(senderName)s removed the rule banning servers matching %(glob)s": "%(اسم المرسل)s إزالة القاعدة التي تحظر الغرف المتطابقة %(عام)s",
@ -346,9 +288,9 @@
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s قاعدة حظر محدثة التي طابقت %(oldGlob)s لتطابق %(newGlob)s من أجل %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s قاعدة حظر محدثة التي طابقت %(oldGlob)s لتطابق %(newGlob)s من أجل %(reason)s",
"Light": "ضوء", "Light": "ضوء",
"Dark": "مظلم", "Dark": "مظلم",
"You signed in to a new session without verifying it:": "قمت بتسجيل الدخول لجلسة جديدة من غير التحقق منها", "You signed in to a new session without verifying it:": "قمت بتسجيل الدخول لجلسة جديدة من غير التحقق منها:",
"Verify your other session using one of the options below.": "تحقق من جلستك الأخرى باستخدام أحد الخيارات في الأسفل", "Verify your other session using one of the options below.": "تحقق من جلستك الأخرى باستخدام أحد الخيارات في الأسفل",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s تم تسجيل الدخول لجلسة جديدة من غير التحقق منها", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s تم تسجيل الدخول لجلسة جديدة من غير التحقق منها:",
"Ask this user to verify their session, or manually verify it below.": "اطلب من هذا المستخدم التحقق من جلسته أو تحقق منها بشكل يدوي في الأسفل", "Ask this user to verify their session, or manually verify it below.": "اطلب من هذا المستخدم التحقق من جلسته أو تحقق منها بشكل يدوي في الأسفل",
"Not Trusted": "غير موثوقة", "Not Trusted": "غير موثوقة",
"Manually Verify by Text": "التحقق بشكل يدوي عبر نص", "Manually Verify by Text": "التحقق بشكل يدوي عبر نص",
@ -365,15 +307,10 @@
"Cannot reach identity server": "لا يمكن الوصول لهوية السيرفر", "Cannot reach identity server": "لا يمكن الوصول لهوية السيرفر",
"You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "يمكنك التسجيل , لكن بعض الميزات ستكون غير متوفرة حتى يتم التعرف على هوية السيرفر بشكل متصل . إن كنت ما تزال ترى هذا التحذير , تأكد من إعداداتك أو تواصل مع مدير السيرفر", "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "يمكنك التسجيل , لكن بعض الميزات ستكون غير متوفرة حتى يتم التعرف على هوية السيرفر بشكل متصل . إن كنت ما تزال ترى هذا التحذير , تأكد من إعداداتك أو تواصل مع مدير السيرفر",
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "يمكنك إعادة ضبط كلمة السر لكن بعض الميزات ستكون غير متوفرة حتى عودة السيرفر للإنترنت . إذا كنت لا تزال ترى هذا التحذير تأكد من إعداداتك أو تواصل مع مدير السيرفر", "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "يمكنك إعادة ضبط كلمة السر لكن بعض الميزات ستكون غير متوفرة حتى عودة السيرفر للإنترنت . إذا كنت لا تزال ترى هذا التحذير تأكد من إعداداتك أو تواصل مع مدير السيرفر",
"I understand the risks and wish to continue": "ادرك المخاطر وارغب في الاستمرار",
"Language Dropdown": "قائمة اللغة المنسدلة", "Language Dropdown": "قائمة اللغة المنسدلة",
"Information": "المعلومات", "Information": "المعلومات",
"Rotate clockwise": "أدر باتجاه عقارب الساعة",
"Rotate Right": "أدر لليمين", "Rotate Right": "أدر لليمين",
"Rotate counter-clockwise": "أدر عكس اتجاه عقارب الساعة",
"Rotate Left": "أدر لليسار", "Rotate Left": "أدر لليسار",
"Uploaded on %(date)s by %(user)s": "رفعه %(user)s في %(date)s",
"You cannot delete this image. (%(code)s)": "لا يمكنك حذف هذه الصورة. (%(code)s)",
"expand": "توسيع", "expand": "توسيع",
"collapse": "تضييق", "collapse": "تضييق",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "الرجاء <newIssueLink> إنشاء إشكال جديد</newIssueLink> على GitHub حتى نتمكن من التحقيق في هذا الخطأ.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "الرجاء <newIssueLink> إنشاء إشكال جديد</newIssueLink> على GitHub حتى نتمكن من التحقيق في هذا الخطأ.",
@ -388,7 +325,6 @@
"Widget added by": "عنصر واجهة أضافه", "Widget added by": "عنصر واجهة أضافه",
"Widgets do not use message encryption.": "عناصر الواجهة لا تستخدم تشفير الرسائل.", "Widgets do not use message encryption.": "عناصر الواجهة لا تستخدم تشفير الرسائل.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "قد يؤدي استخدام هذه الأداة إلى مشاركة البيانات <helpIcon /> مع%(widgetDomain)s.", "Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "قد يؤدي استخدام هذه الأداة إلى مشاركة البيانات <helpIcon /> مع%(widgetDomain)s.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات <helpIcon /> مع %(widgetDomain)s ومدير التكامل الخاص بك.",
"Widget ID": "معرّف عنصر واجهة", "Widget ID": "معرّف عنصر واجهة",
"Room ID": "معرّف الغرفة", "Room ID": "معرّف الغرفة",
"%(brand)s URL": "رابط %(brand)s", "%(brand)s URL": "رابط %(brand)s",
@ -444,7 +380,6 @@
"Message deleted by %(name)s": "حذف الرسالة %(name)s", "Message deleted by %(name)s": "حذف الرسالة %(name)s",
"Message deleted": "حُذفت الرسالة", "Message deleted": "حُذفت الرسالة",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>تفاعلو ب%(shortName)s</reactedWith>", "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>تفاعلو ب%(shortName)s</reactedWith>",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> تفاعلوا ب%(content)s</reactedWith>",
"Reactions": "التفاعلات", "Reactions": "التفاعلات",
"Show all": "أظهر الكل", "Show all": "أظهر الكل",
"Error decrypting video": "تعذر فك تشفير الفيديو", "Error decrypting video": "تعذر فك تشفير الفيديو",
@ -477,7 +412,6 @@
"Message Actions": "إجراءات الرسائل", "Message Actions": "إجراءات الرسائل",
"Reply": "رد", "Reply": "رد",
"React": "تفاعل", "React": "تفاعل",
"Error decrypting audio": "تعذر فك تشفير الصوت",
"The encryption used by this room isn't supported.": "التشفير الذي تستخدمه هذه الغرفة غير مدعوم.", "The encryption used by this room isn't supported.": "التشفير الذي تستخدمه هذه الغرفة غير مدعوم.",
"Encryption not enabled": "التشفير غير مفعل", "Encryption not enabled": "التشفير غير مفعل",
"Ignored attempt to disable encryption": "تم تجاهل محاولة تعطيل التشفير", "Ignored attempt to disable encryption": "تم تجاهل محاولة تعطيل التشفير",
@ -512,7 +446,6 @@
"New published address (e.g. #alias:server)": "عنوان منشور جديد (على سبيل المثال #alias:server)", "New published address (e.g. #alias:server)": "عنوان منشور جديد (على سبيل المثال #alias:server)",
"No other published addresses yet, add one below": "لا توجد عناوين أخرى منشورة بعد ، أضف واحدًا أدناه", "No other published addresses yet, add one below": "لا توجد عناوين أخرى منشورة بعد ، أضف واحدًا أدناه",
"Other published addresses:": "عناوين منشورة أخرى:", "Other published addresses:": "عناوين منشورة أخرى:",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "يمكن لأي شخص استخدام العناوين المنشورة على أي خادم للانضمام إلى غرفتك. لنشر عنوان ، يجب تعيينه كعنوان محلي أولاً.",
"Published Addresses": "العناوين المنشورة", "Published Addresses": "العناوين المنشورة",
"Local address": "العنوان المحلي", "Local address": "العنوان المحلي",
"This room has no local addresses": "هذه الغرفة ليس لها عناوين محلية", "This room has no local addresses": "هذه الغرفة ليس لها عناوين محلية",
@ -661,11 +594,7 @@
"%(duration)sh": "%(duration)sس", "%(duration)sh": "%(duration)sس",
"%(duration)sm": "%(duration)sد", "%(duration)sm": "%(duration)sد",
"%(duration)ss": "%(duration)sث", "%(duration)ss": "%(duration)sث",
"Jump to message": "انتقل إلى الرسالة",
"Unpin Message": "إلغاء تثبيت الرسالة",
"Pinned Messages": "الرسائل المثبتة",
"Loading...": "جارٍ الحمل...", "Loading...": "جارٍ الحمل...",
"No pinned messages.": "لا رسائل مثبَّتة.",
"This is the start of <roomName/>.": "هذه بداية <roomName/>.", "This is the start of <roomName/>.": "هذه بداية <roomName/>.",
"Add a photo, so people can easily spot your room.": "أضف صورة ، حتى يسهل على الناس تمييز غرفتك.", "Add a photo, so people can easily spot your room.": "أضف صورة ، حتى يسهل على الناس تمييز غرفتك.",
"You created this room.": "أنت أنشأت هذه الغرفة.", "You created this room.": "أنت أنشأت هذه الغرفة.",
@ -701,7 +630,6 @@
"and %(count)s others...|other": "و %(count)s أخر...", "and %(count)s others...|other": "و %(count)s أخر...",
"Close preview": "إغلاق المعاينة", "Close preview": "إغلاق المعاينة",
"Scroll to most recent messages": "انتقل إلى أحدث الرسائل", "Scroll to most recent messages": "انتقل إلى أحدث الرسائل",
"Please select the destination room for this message": "الرجاء تحديد الغرفة التي توجه لها هذه الرسالة",
"The authenticity of this encrypted message can't be guaranteed on this device.": "لا يمكن ضمان موثوقية هذه الرسالة المشفرة على هذا الجهاز.", "The authenticity of this encrypted message can't be guaranteed on this device.": "لا يمكن ضمان موثوقية هذه الرسالة المشفرة على هذا الجهاز.",
"Encrypted by a deleted session": "مشفرة باتصال محذوف", "Encrypted by a deleted session": "مشفرة باتصال محذوف",
"Unencrypted": "غير مشفر", "Unencrypted": "غير مشفر",
@ -729,13 +657,8 @@
"Error adding ignored user/server": "تعذر إضافة مستخدم/خادم مُتجاهَل", "Error adding ignored user/server": "تعذر إضافة مستخدم/خادم مُتجاهَل",
"Ignored/Blocked": "المُتجاهل/المحظور", "Ignored/Blocked": "المُتجاهل/المحظور",
"Labs": "معامل", "Labs": "معامل",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "خصص تجربتك مع ميزات المعامل التجريبية. <a> اعرف المزيد </a>.",
"Clear cache and reload": "محو مخزن الجيب وإعادة التحميل", "Clear cache and reload": "محو مخزن الجيب وإعادة التحميل",
"click to reveal": "انقر للكشف",
"Access Token:": "رمز الوصول:",
"Identity Server is": "خادم الهوية هو",
"Homeserver is": "الخادم الوسيط هو", "Homeserver is": "الخادم الوسيط هو",
"olm version:": "إصدار olm:",
"%(brand)s version:": "إصدار %(brand)s:", "%(brand)s version:": "إصدار %(brand)s:",
"Versions": "الإصدارات", "Versions": "الإصدارات",
"Keyboard Shortcuts": "اختصارات لوحة المفاتيح", "Keyboard Shortcuts": "اختصارات لوحة المفاتيح",
@ -743,7 +666,6 @@
"Help & About": "المساعدة وعن البرنامج", "Help & About": "المساعدة وعن البرنامج",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "للإبلاغ عن مشكلة أمنية متعلقة بMatrix ، يرجى قراءة <a>سياسة الإفصاح الأمني</a> في Matrix.org.", "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "للإبلاغ عن مشكلة أمنية متعلقة بMatrix ، يرجى قراءة <a>سياسة الإفصاح الأمني</a> في Matrix.org.",
"Submit debug logs": "إرسال سجلات تصحيح الأخطاء", "Submit debug logs": "إرسال سجلات تصحيح الأخطاء",
"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 reporting": "الإبلاغ عن مشاكل في البرنامج", "Bug reporting": "الإبلاغ عن مشاكل في البرنامج",
"Chat with %(brand)s Bot": "تخاطب مع الروبوت الخاص ب%(brand)s", "Chat with %(brand)s Bot": "تخاطب مع الروبوت الخاص ب%(brand)s",
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "للمساعدة في استخدام %(brand)s ، انقر <a> هنا </a> أو ابدأ محادثة مع برنامج الروبوت الخاص بنا باستخدام الزر أدناه.", "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "للمساعدة في استخدام %(brand)s ، انقر <a> هنا </a> أو ابدأ محادثة مع برنامج الروبوت الخاص بنا باستخدام الزر أدناه.",
@ -768,7 +690,6 @@
"Customise your appearance": "تخصيص مظهرك", "Customise your appearance": "تخصيص مظهرك",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "قم بتعيين اسم الخط المثبت على نظامك وسيحاول %(brand)s استخدامه.", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "قم بتعيين اسم الخط المثبت على نظامك وسيحاول %(brand)s استخدامه.",
"Modern": "حديث", "Modern": "حديث",
"Compact": "متراصّ",
"Message layout": "مظهر الرسائل", "Message layout": "مظهر الرسائل",
"Theme": "المظهر", "Theme": "المظهر",
"Add theme": "إضافة مظهر", "Add theme": "إضافة مظهر",
@ -783,20 +704,15 @@
"New version available. <a>Update now.</a>": "ثمة إصدارٌ جديد. <a>حدّث الآن.</a>", "New version available. <a>Update now.</a>": "ثمة إصدارٌ جديد. <a>حدّث الآن.</a>",
"Check for update": "ابحث عن تحديث", "Check for update": "ابحث عن تحديث",
"Error encountered (%(errorDetail)s).": "صودِفَ خطأ: (%(errorDetail)s).", "Error encountered (%(errorDetail)s).": "صودِفَ خطأ: (%(errorDetail)s).",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.",
"Manage integrations": "إدارة التكاملات", "Manage integrations": "إدارة التكاملات",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
"Change": "تغيير", "Change": "تغيير",
"Enter a new identity server": "أدخل خادم هوية جديدًا", "Enter a new identity server": "أدخل خادم هوية جديدًا",
"Do not use an identity server": "لا تستخدم خادم هوية", "Do not use an identity server": "لا تستخدم خادم هوية",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "استخدام خادم الهوية اختياري. إذا اخترت عدم استخدام خادم هوية ، فلن يتمكن المستخدمون الآخرون من اكتشافك ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.", "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "استخدام خادم الهوية اختياري. إذا اخترت عدم استخدام خادم هوية ، فلن يتمكن المستخدمون الآخرون من اكتشافك ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "قطع الاتصال بخادم الهوية الخاص بك يعني أنك لن تكون قابلاً للاكتشاف من قبل المستخدمين الآخرين ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "قطع الاتصال بخادم الهوية الخاص بك يعني أنك لن تكون قابلاً للاكتشاف من قبل المستخدمين الآخرين ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "أنت لا تستخدم حاليًا خادم هوية. لاكتشاف جهات الاتصال الحالية التي تعرفها وتكون قابلاً للاكتشاف ، أضف واحداً أدناه.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "أنت لا تستخدم حاليًا خادم هوية. لاكتشاف جهات الاتصال الحالية التي تعرفها وتكون قابلاً للاكتشاف ، أضف واحداً أدناه.",
"Identity Server": "خادم الهوية",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "إذا كنت لا تريد استخدام <server /> لاكتشاف جهات الاتصال الموجودة التي تعرفها وتكون قابلاً للاكتشاف ، فأدخل خادم هوية آخر أدناه.", "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "إذا كنت لا تريد استخدام <server /> لاكتشاف جهات الاتصال الموجودة التي تعرفها وتكون قابلاً للاكتشاف ، فأدخل خادم هوية آخر أدناه.",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "أنت تستخدم حاليًا <server> </server> لاكتشاف جهات الاتصال الحالية التي تعرفها وتجعل نفسك قابلاً للاكتشاف. يمكنك تغيير خادم الهوية الخاص بك أدناه.", "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "أنت تستخدم حاليًا <server> </server> لاكتشاف جهات الاتصال الحالية التي تعرفها وتجعل نفسك قابلاً للاكتشاف. يمكنك تغيير خادم الهوية الخاص بك أدناه.",
"Identity Server (%(server)s)": "خادمة الهوية (%(server)s)",
"Go back": "ارجع", "Go back": "ارجع",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "نوصي بإزالة عناوين البريد الإلكتروني وأرقام الهواتف من خادم الهوية قبل قطع الاتصال.", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "نوصي بإزالة عناوين البريد الإلكتروني وأرقام الهواتف من خادم الهوية قبل قطع الاتصال.",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "لا زالت <b>بياناتك الشخصية مشاعة</b> على خادم الهوية <idserver />.", "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "لا زالت <b>بياناتك الشخصية مشاعة</b> على خادم الهوية <idserver />.",
@ -814,9 +730,6 @@
"Disconnect from the identity server <current /> and connect to <new /> instead?": "انفصل عن خادم الهوية <current /> واتصل بآخر <new /> بدلاً منه؟", "Disconnect from the identity server <current /> and connect to <new /> instead?": "انفصل عن خادم الهوية <current /> واتصل بآخر <new /> بدلاً منه؟",
"Change identity server": "تغيير خادم الهوية", "Change identity server": "تغيير خادم الهوية",
"Checking server": "فحص خادم", "Checking server": "فحص خادم",
"Could not connect to Identity Server": "تعذر الاتصال بخادم هوية",
"Not a valid Identity Server (status code %(code)s)": "خادم هوية مردود (رقم الحال %(code)s)",
"Identity Server URL must be HTTPS": "يجب أن يكون رابط (URL) خادم الهوية HTTPS",
"not ready": "غير جاهز", "not ready": "غير جاهز",
"ready": "جاهز", "ready": "جاهز",
"Secret storage:": "التخزين السري:", "Secret storage:": "التخزين السري:",
@ -825,7 +738,6 @@
"Backup key cached:": "المفتاح الاحتياطي المحفوظ (في cache):", "Backup key cached:": "المفتاح الاحتياطي المحفوظ (في cache):",
"Backup key stored:": "المفتاح الاختياطي المحفوظ:", "Backup key stored:": "المفتاح الاختياطي المحفوظ:",
"not stored": "لم يُحفظ", "not stored": "لم يُحفظ",
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "قم بعمل نسخة احتياطية من مفاتيح التشفير ببيانات حسابك في حالة فقد الوصول إلى اتصالاتك. ستأمَّن مفاتيحك باستخدام مفتاح استرداد فريد.",
"unexpected type": "نوع غير متوقع", "unexpected type": "نوع غير متوقع",
"well formed": "مشكل جيّداً", "well formed": "مشكل جيّداً",
"Back up your keys before signing out to avoid losing them.": "أضف مفاتيحك للاحتياطي قبل تسجيل الخروج لتتجنب ضياعها.", "Back up your keys before signing out to avoid losing them.": "أضف مفاتيحك للاحتياطي قبل تسجيل الخروج لتتجنب ضياعها.",
@ -867,16 +779,8 @@
"Enable audible notifications for this session": "تمكين الإشعارات الصوتية لهذا الاتصال", "Enable audible notifications for this session": "تمكين الإشعارات الصوتية لهذا الاتصال",
"Show message in desktop notification": "إظهار الرسالة في إشعارات سطح المكتب", "Show message in desktop notification": "إظهار الرسالة في إشعارات سطح المكتب",
"Enable desktop notifications for this session": "تمكين إشعارات سطح المكتب لهذا الاتصال", "Enable desktop notifications for this session": "تمكين إشعارات سطح المكتب لهذا الاتصال",
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "ربما قد ضبطتها في عميل آخر غير %(brand)s. لا يمكنك ضبطها في %(brand)s لكنها تبقى سارية.",
"There are advanced notifications which are not shown here.": "توجد إشعارات متقدمة لا تظهر هنا.",
"Notification targets": "أهداف الإشعار", "Notification targets": "أهداف الإشعار",
"Unable to fetch notification target list": "تعذر جلب قائمة هدف الإشعار",
"Notifications on the following keywords follow rules which cant be displayed here:": "تتبع الإشعارات بالكلمات المفتاحية التالية قواعد لا يمكن عرضها هنا:",
"Add an email address to configure email notifications": "أضف عنوان بريد إلكتروني لضبط إشعاراته",
"Enable email notifications": "تمكين إشعارات البريد الإلكتروني",
"Clear notifications": "محو الإشعارات", "Clear notifications": "محو الإشعارات",
"Enable notifications for this account": "تمكين الإشعارات لهذا الحساب",
"Notify me for anything else": "أشعرني بأي شيء آخر",
"You've successfully verified your device!": "لقد نجحت في التحقق من جهازك!", "You've successfully verified your device!": "لقد نجحت في التحقق من جهازك!",
"In encrypted rooms, verify all users to ensure its secure.": "في الغرف المشفرة ، تحقق من جميع المستخدمين للتأكد من أنها آمنة.", "In encrypted rooms, verify all users to ensure its secure.": "في الغرف المشفرة ، تحقق من جميع المستخدمين للتأكد من أنها آمنة.",
"Verify all users in a room to ensure it's secure.": "تحقق من جميع المستخدمين في الغرفة للتأكد من أنها آمنة.", "Verify all users in a room to ensure it's secure.": "تحقق من جميع المستخدمين في الغرفة للتأكد من أنها آمنة.",
@ -891,7 +795,6 @@
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "الاتصال الذي تحاول التحقق منه لا يدعم مسح رمز الاستجابة السريعة أو التحقق من الرموز التعبيرية ، وهو ما يدعمه %(brand)s. جرب مع عميل مختلف.", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "الاتصال الذي تحاول التحقق منه لا يدعم مسح رمز الاستجابة السريعة أو التحقق من الرموز التعبيرية ، وهو ما يدعمه %(brand)s. جرب مع عميل مختلف.",
"Security": "الأمان", "Security": "الأمان",
"This client does not support end-to-end encryption.": "لا يدعم هذا العميل التشفير من طرف إلى طرف.", "This client does not support end-to-end encryption.": "لا يدعم هذا العميل التشفير من طرف إلى طرف.",
"Role": "الدور",
"Failed to deactivate user": "تعذر إلغاء نشاط المستخدم", "Failed to deactivate user": "تعذر إلغاء نشاط المستخدم",
"Deactivate user": "إلغاء نشاط المستخدم", "Deactivate user": "إلغاء نشاط المستخدم",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "سيؤدي إلغاء نشاط هذا المستخدم إلى تسجيل خروجهم ومنعهم من تسجيل الدخول مرة أخرى. بالإضافة إلى ذلك ، سيغادرون جميع الغرف التي يتواجدون فيها. لا يمكن التراجع عن هذا الإجراء. هل أنت متأكد أنك تريد إلغاء نشاط هذا المستخدم؟", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "سيؤدي إلغاء نشاط هذا المستخدم إلى تسجيل خروجهم ومنعهم من تسجيل الدخول مرة أخرى. بالإضافة إلى ذلك ، سيغادرون جميع الغرف التي يتواجدون فيها. لا يمكن التراجع عن هذا الإجراء. هل أنت متأكد أنك تريد إلغاء نشاط هذا المستخدم؟",
@ -928,7 +831,6 @@
"Demote": "تخفيض", "Demote": "تخفيض",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "لن تتمكن من التراجع عن هذا التغيير لأنك تقوم بتخفيض رتبتك ، إذا كنت آخر مستخدم ذي امتياز في الغرفة ، فسيكون من المستحيل استعادة الامتيازات.", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "لن تتمكن من التراجع عن هذا التغيير لأنك تقوم بتخفيض رتبتك ، إذا كنت آخر مستخدم ذي امتياز في الغرفة ، فسيكون من المستحيل استعادة الامتيازات.",
"Demote yourself?": "خفض مرتبة نفسك؟", "Demote yourself?": "خفض مرتبة نفسك؟",
"Direct message": "رسالة مباشرة",
"Share Link to User": "مشاركة رابط للمستخدم", "Share Link to User": "مشاركة رابط للمستخدم",
"Mention": "إشارة", "Mention": "إشارة",
"Invite": "دعوة", "Invite": "دعوة",
@ -972,7 +874,6 @@
"Start Verification": "ابدأ التحقق", "Start Verification": "ابدأ التحقق",
"Accepting…": "جارٍ القبول …", "Accepting…": "جارٍ القبول …",
"Waiting for %(displayName)s to accept…": "بانتظار %(displayName)s ليقبل …", "Waiting for %(displayName)s to accept…": "بانتظار %(displayName)s ليقبل …",
"Waiting for you to accept on your other session…": "في انتظار قبولك من اتصالك الآخر …",
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "عندما يضع شخص ما عنوان URL في رسالته ، يمكن عرض معاينة عنوان URL لإعطاء مزيد من المعلومات حول هذا الرابط مثل العنوان والوصف وصورة من موقع الويب.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "عندما يضع شخص ما عنوان URL في رسالته ، يمكن عرض معاينة عنوان URL لإعطاء مزيد من المعلومات حول هذا الرابط مثل العنوان والوصف وصورة من موقع الويب.",
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "في الغرف المشفرة ، مثل هذه الغرفة ، يتم تعطيل معاينات URL أصلاً للتأكد من أن خادمك الوسيط (حيث يتم إنشاء المعاينات) لا يمكنه جمع معلومات حول الروابط التي تراها في هذه الغرفة.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "في الغرف المشفرة ، مثل هذه الغرفة ، يتم تعطيل معاينات URL أصلاً للتأكد من أن خادمك الوسيط (حيث يتم إنشاء المعاينات) لا يمكنه جمع معلومات حول الروابط التي تراها في هذه الغرفة.",
"URL previews are disabled by default for participants in this room.": "معاينات URL معطلة بشكل أصلي للمشاركين في هذه الغرفة.", "URL previews are disabled by default for participants in this room.": "معاينات URL معطلة بشكل أصلي للمشاركين في هذه الغرفة.",
@ -989,13 +890,6 @@
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' ليس معرف مجتمع صالحًا", "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ليس معرف مجتمع صالحًا",
"Invalid community ID": "معرف المجتمع غير صالح", "Invalid community ID": "معرف المجتمع غير صالح",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "تعذر تحديث flair لهذه الغرفة. قد لا يسمح الخادم بذلك أو حدث خطأ مؤقت.", "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "تعذر تحديث flair لهذه الغرفة. قد لا يسمح الخادم بذلك أو حدث خطأ مؤقت.",
"Notify for all other messages/rooms": "أشعر لجميع الرسائل والغرف الأخرى",
"Messages containing <span>keywords</span>": "رسائل تتضمن <span>كلمات مفتاحية</span>",
"Failed to update keywords": "تعذر تحديث الكلمات المفتاحية",
"Failed to change settings": "تعذر تغيير الإعدادات",
"Enter keywords separated by a comma:": "أدخل كلماتٍ مفتاحية مع فاصلة بينها:",
"Keywords": "الكلمات المفتاحية",
"Error saving email notification preferences": "تعذر حفظ تفضيلات إشعار البريد الإلكتروني",
"The integration manager is offline or it cannot reach your homeserver.": "مدري التكامل غير متصل بالإنرتنت أو لا يمكنه الوصول إلى خادمك الوسيط.", "The integration manager is offline or it cannot reach your homeserver.": "مدري التكامل غير متصل بالإنرتنت أو لا يمكنه الوصول إلى خادمك الوسيط.",
"Cannot connect to integration manager": "لا يمكن الاتصال بمدير التكامل", "Cannot connect to integration manager": "لا يمكن الاتصال بمدير التكامل",
"Connecting to integration manager...": "جارٍ الاتصال بمدير التكامل ...", "Connecting to integration manager...": "جارٍ الاتصال بمدير التكامل ...",
@ -1003,8 +897,6 @@
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s يفقد بعض المكونات المطلوبة لحفظ آمن محليًّا للرسائل المشفرة. إذا أدرت تجربة هذه الخاصية، فأنشئ %(brand)s على سطح المكتب مع <nativeLink>إضافة مكونات البحث</nativeLink>.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s يفقد بعض المكونات المطلوبة لحفظ آمن محليًّا للرسائل المشفرة. إذا أدرت تجربة هذه الخاصية، فأنشئ %(brand)s على سطح المكتب مع <nativeLink>إضافة مكونات البحث</nativeLink>.",
"Securely cache encrypted messages locally for them to appear in search results.": "تخزين الرسائل المشفرة بشكل آمن (في cache) محليًا حتى تظهر في نتائج البحث.", "Securely cache encrypted messages locally for them to appear in search results.": "تخزين الرسائل المشفرة بشكل آمن (في cache) محليًا حتى تظهر في نتائج البحث.",
"Manage": "إدارة", "Manage": "إدارة",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "تخزين الرسائل المشفرة مؤقتًا بشكل آمن محليًا حتى تظهر في نتائج البحث ، باستخدام %(size)s لتخزين الرسائل من %(count)s غرفة.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "تخزين الرسائل المشفرة مؤقتًا بشكل آمن محليًا حتى تظهر في نتائج البحث ، باستخدام%(size)s لتخزين الرسائل من %(count)s غرف.",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "تحقق بشكل فردي من كل اتصال يستخدمه المستخدم لتمييزه أنه موثوق ، دون الوثوق بالأجهزة الموقعة بالتبادل.", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "تحقق بشكل فردي من كل اتصال يستخدمه المستخدم لتمييزه أنه موثوق ، دون الوثوق بالأجهزة الموقعة بالتبادل.",
"Encryption": "تشفير", "Encryption": "تشفير",
"Failed to set display name": "تعذر تعيين الاسم الظاهر", "Failed to set display name": "تعذر تعيين الاسم الظاهر",
@ -1056,12 +948,10 @@
"Failed to upload profile picture!": "تعذَّر رفع صورة الملف الشخصي!", "Failed to upload profile picture!": "تعذَّر رفع صورة الملف الشخصي!",
"Show more": "أظهر أكثر", "Show more": "أظهر أكثر",
"Show less": "أظهر أقل", "Show less": "أظهر أقل",
"Channel: %(channelName)s": "قناة: %(channelName)s",
"This bridge is managed by <user />.": "هذا الجسر يديره <user />.", "This bridge is managed by <user />.": "هذا الجسر يديره <user />.",
"Upload": "رفع", "Upload": "رفع",
"Accept <policyLink /> to continue:": "قبول <policyLink /> للمتابعة:", "Accept <policyLink /> to continue:": "قبول <policyLink /> للمتابعة:",
"Decline (%(counter)s)": "رفض (%(counter)s)", "Decline (%(counter)s)": "رفض (%(counter)s)",
"From %(deviceName)s (%(deviceId)s)": "من %(deviceName)s (%(deviceId)s)",
"Your server isn't responding to some <a>requests</a>.": "خادمك لا يتجاوب مع بعض <a>الطلبات</a>.", "Your server isn't responding to some <a>requests</a>.": "خادمك لا يتجاوب مع بعض <a>الطلبات</a>.",
"Dog": "كلب", "Dog": "كلب",
"To be secure, do this in person or use a trusted way to communicate.": "لتكون آمنًا ، افعل ذلك شخصيًا أو استخدم طريقة موثوقة للتواصل.", "To be secure, do this in person or use a trusted way to communicate.": "لتكون آمنًا ، افعل ذلك شخصيًا أو استخدم طريقة موثوقة للتواصل.",
@ -1089,12 +979,7 @@
"The other party cancelled the verification.": "ألغى الطرف الآخر التحقق.", "The other party cancelled the verification.": "ألغى الطرف الآخر التحقق.",
"Accept": "قبول", "Accept": "قبول",
"Decline": "رفض", "Decline": "رفض",
"Incoming call": "مكالمة واردة",
"Incoming video call": "مكالمة مرئية واردة",
"Incoming voice call": "مكالمة صوتية واردة",
"Unknown caller": "متصل غير معروف", "Unknown caller": "متصل غير معروف",
"Call Paused": "أوقِفَ الاتصال",
"Active call": "مكالمة نشطة",
"This is your list of users/servers you have blocked - don't leave the room!": "هذه قائمتك للمستخدمين / الخوادم التي حظرت - لا تغادر الغرفة!", "This is your list of users/servers you have blocked - don't leave the room!": "هذه قائمتك للمستخدمين / الخوادم التي حظرت - لا تغادر الغرفة!",
"My Ban List": "قائمة الحظر", "My Ban List": "قائمة الحظر",
"When rooms are upgraded": "عند ترقية الغرف", "When rooms are upgraded": "عند ترقية الغرف",
@ -1115,23 +1000,19 @@
"How fast should messages be downloaded.": "ما مدى سرعة تنزيل الرسائل.", "How fast should messages be downloaded.": "ما مدى سرعة تنزيل الرسائل.",
"Enable message search in encrypted rooms": "تمكين البحث عن الرسائل في الغرف المشفرة", "Enable message search in encrypted rooms": "تمكين البحث عن الرسائل في الغرف المشفرة",
"Show previews/thumbnails for images": "إظهار المعاينات / الصور المصغرة للصور", "Show previews/thumbnails for images": "إظهار المعاينات / الصور المصغرة للصور",
"Send read receipts for messages (requires compatible homeserver to disable)": "إرسال إيصالات بالقراءة للرسائل (يتطلب التعطيل توافق الخادم الوسيط)",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "السماح بخادم مساعدة الاتصال الاحتياطي turn.matrix.org عندما لا يقدم خادمك واحدًا (سيتم مشاركة عنوان IP الخاص بك أثناء المكالمة)", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "السماح بخادم مساعدة الاتصال الاحتياطي turn.matrix.org عندما لا يقدم خادمك واحدًا (سيتم مشاركة عنوان IP الخاص بك أثناء المكالمة)",
"Low bandwidth mode": "وضع النطاق الترددي المنخفض",
"Show hidden events in timeline": "إظهار الأحداث المخفية في الجدول الزمني", "Show hidden events in timeline": "إظهار الأحداث المخفية في الجدول الزمني",
"Show shortcuts to recently viewed rooms above the room list": "إظهار اختصارات للغرف التي تم عرضها مؤخرًا أعلى قائمة الغرف", "Show shortcuts to recently viewed rooms above the room list": "إظهار اختصارات للغرف التي تم عرضها مؤخرًا أعلى قائمة الغرف",
"Show rooms with unread notifications first": "اعرض الغرف ذات الإشعارات غير المقروءة أولاً", "Show rooms with unread notifications first": "اعرض الغرف ذات الإشعارات غير المقروءة أولاً",
"Order rooms by name": "ترتيب الغرف بالاسم", "Order rooms by name": "ترتيب الغرف بالاسم",
"Show developer tools": "عرض أدوات المطور", "Show developer tools": "عرض أدوات المطور",
"Prompt before sending invites to potentially invalid matrix IDs": "أعلمني قبل إرسال دعوات لمعرِّفات قد لا تكون صحيحة", "Prompt before sending invites to potentially invalid matrix IDs": "أعلمني قبل إرسال دعوات لمعرِّفات قد لا تكون صحيحة",
"Room Colour": "لون الغرفة",
"Enable URL previews by default for participants in this room": "تمكين معاينة الروابط أصلاً لأي مشارك في هذه الغرفة", "Enable URL previews by default for participants in this room": "تمكين معاينة الروابط أصلاً لأي مشارك في هذه الغرفة",
"Enable URL previews for this room (only affects you)": "تمكين معاينة الروابط لهذه الغرفة (يؤثر عليك فقط)", "Enable URL previews for this room (only affects you)": "تمكين معاينة الروابط لهذه الغرفة (يؤثر عليك فقط)",
"Enable inline URL previews by default": "تمكين معاينة الروابط أصلاً", "Enable inline URL previews by default": "تمكين معاينة الروابط أصلاً",
"Never send encrypted messages to unverified sessions in this room from this session": "لا ترسل أبدًا رسائل مشفرة إلى اتصالات التي لم يتم التحقق منها في هذه الغرفة من هذا الاتصال", "Never send encrypted messages to unverified sessions in this room from this session": "لا ترسل أبدًا رسائل مشفرة إلى اتصالات التي لم يتم التحقق منها في هذه الغرفة من هذا الاتصال",
"Never send encrypted messages to unverified sessions from this session": "لا ترسل أبدًا رسائل مشفرة إلى اتصالات لم يتم التحقق منها من هذا الاتصال", "Never send encrypted messages to unverified sessions from this session": "لا ترسل أبدًا رسائل مشفرة إلى اتصالات لم يتم التحقق منها من هذا الاتصال",
"Send analytics data": "إرسال بيانات التحليلات", "Send analytics data": "إرسال بيانات التحليلات",
"Allow Peer-to-Peer for 1:1 calls": "تمكين مكالمة القرين للقرين إذا كانت فردية (1:1)",
"System font name": "اسم خط النظام", "System font name": "اسم خط النظام",
"Use a system font": "استخدام خط النظام", "Use a system font": "استخدام خط النظام",
"Match system theme": "مطابقة ألوان النظام", "Match system theme": "مطابقة ألوان النظام",
@ -1142,7 +1023,6 @@
"Enable big emoji in chat": "تفعيل الرموز التعبيرية الكبيرة في المحادثة", "Enable big emoji in chat": "تفعيل الرموز التعبيرية الكبيرة في المحادثة",
"Show avatars in user and room mentions": "إظهار الصورة الرمزية عند ذكر مستخدم أو غرفة", "Show avatars in user and room mentions": "إظهار الصورة الرمزية عند ذكر مستخدم أو غرفة",
"Enable automatic language detection for syntax highlighting": "تمكين التعرف التلقائي للغة لتلوين الجمل", "Enable automatic language detection for syntax highlighting": "تمكين التعرف التلقائي للغة لتلوين الجمل",
"Autoplay GIFs and videos": "التشغيل التلقائي لملفات GIF ومقاطع الفيديو",
"Always show message timestamps": "اعرض دائمًا الطوابع الزمنية للرسالة", "Always show message timestamps": "اعرض دائمًا الطوابع الزمنية للرسالة",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "عرض الطوابع الزمنية بتنسيق 12 ساعة (على سبيل المثال 2:30pm)", "Show timestamps in 12 hour format (e.g. 2:30pm)": "عرض الطوابع الزمنية بتنسيق 12 ساعة (على سبيل المثال 2:30pm)",
"Show read receipts sent by other users": "إظهار إيصالات القراءة المرسلة من قبل مستخدمين آخرين", "Show read receipts sent by other users": "إظهار إيصالات القراءة المرسلة من قبل مستخدمين آخرين",
@ -1155,18 +1035,15 @@
"Use custom size": "استخدام حجم مخصص", "Use custom size": "استخدام حجم مخصص",
"Font size": "حجم الخط", "Font size": "حجم الخط",
"Show info about bridges in room settings": "إظهار المعلومات حول الجسور في إعدادات الغرفة", "Show info about bridges in room settings": "إظهار المعلومات حول الجسور في إعدادات الغرفة",
"Enable advanced debugging for the room list": "تفعيل التصحيح المتقدم لقائمة الغرف",
"Offline encrypted messaging using dehydrated devices": "الرسائل المشفرة في وضع عدم الاتصال باستخدام أجهزة مجففة", "Offline encrypted messaging using dehydrated devices": "الرسائل المشفرة في وضع عدم الاتصال باستخدام أجهزة مجففة",
"Show message previews for reactions in all rooms": "أظهر معاينات الرسائل للتفاعلات في كل الغرف", "Show message previews for reactions in all rooms": "أظهر معاينات الرسائل للتفاعلات في كل الغرف",
"Show message previews for reactions in DMs": "أظهر معاينات الرسائل للتفاعلات في المراسلة المباشرة", "Show message previews for reactions in DMs": "أظهر معاينات الرسائل للتفاعلات في المراسلة المباشرة",
"Support adding custom themes": "دعم إضافة ألوان مخصصة", "Support adding custom themes": "دعم إضافة ألوان مخصصة",
"Try out new ways to ignore people (experimental)": "جرب طرق أخرى لتجاهل الناس (تجريبي)", "Try out new ways to ignore people (experimental)": "جرب طرق أخرى لتجاهل الناس (تجريبي)",
"Multiple integration managers": "تعدد مدراء التكامل",
"Render simple counters in room header": "إظهار عدّادات بسيطة في رأس الغرفة", "Render simple counters in room header": "إظهار عدّادات بسيطة في رأس الغرفة",
"Group & filter rooms by custom tags (refresh to apply changes)": "جمع وتصفية الغرف حسب أوسمة مخصصة (حدّث لترى التغيرات)", "Group & filter rooms by custom tags (refresh to apply changes)": "جمع وتصفية الغرف حسب أوسمة مخصصة (حدّث لترى التغيرات)",
"Custom user status messages": "تخصيص رسالة حالة المستخدم", "Custom user status messages": "تخصيص رسالة حالة المستخدم",
"Message Pinning": "تثبيت الرسالة", "Message Pinning": "تثبيت الرسالة",
"New spinner design": "التصميم الجديد للدوَّار",
"Change notification settings": "تغيير إعدادات الإشعار", "Change notification settings": "تغيير إعدادات الإشعار",
"%(senderName)s is calling": "%(senderName)s يتصل", "%(senderName)s is calling": "%(senderName)s يتصل",
"Waiting for answer": "بانتظار الرد", "Waiting for answer": "بانتظار الرد",
@ -1187,7 +1064,6 @@
"Guest": "ضيف", "Guest": "ضيف",
"New version of %(brand)s is available": "يتوفر إصدار جديد من %(brand)s", "New version of %(brand)s is available": "يتوفر إصدار جديد من %(brand)s",
"Update %(brand)s": "حدّث: %(brand)s", "Update %(brand)s": "حدّث: %(brand)s",
"Verify the new login accessing your account: %(name)s": "تحقق من تسجيل الدخول الجديد لحسابك: %(name)s",
"New login. Was this you?": "تسجيل دخول جديد. هل كان ذاك أنت؟", "New login. Was this you?": "تسجيل دخول جديد. هل كان ذاك أنت؟",
"Other users may not trust it": "قد لا يثق به المستخدمون الآخرون", "Other users may not trust it": "قد لا يثق به المستخدمون الآخرون",
"This message cannot be decrypted": "لا يمكن فك تشفير هذه الرسالة", "This message cannot be decrypted": "لا يمكن فك تشفير هذه الرسالة",
@ -1196,9 +1072,6 @@
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "إذا كانت اتصالاتك الأخرى لا تحتوي على مفتاح هذه الرسالة ، فلن تتمكن من فك تشفيرها.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "إذا كانت اتصالاتك الأخرى لا تحتوي على مفتاح هذه الرسالة ، فلن تتمكن من فك تشفيرها.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "يتم إرسال طلبات مشاركة المفاتيح إلى اتصالاتك الأخرى تلقائيًا. إذا رفضت أو ألغيت طلب مشاركة المفتاح في اتصالاتك الأخرى ، فانقر هنا لطلب مفاتيح هذا الاتصال مرة أخرى.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "يتم إرسال طلبات مشاركة المفاتيح إلى اتصالاتك الأخرى تلقائيًا. إذا رفضت أو ألغيت طلب مشاركة المفتاح في اتصالاتك الأخرى ، فانقر هنا لطلب مفاتيح هذا الاتصال مرة أخرى.",
"Your key share request has been sent - please check your other sessions for key share requests.": "تم إرسال طلبك لمشاركة المفتاح - يرجى التحقق من اتصالاتك الأخرى في طلبات مشاركة المفتاح.", "Your key share request has been sent - please check your other sessions for key share requests.": "تم إرسال طلبك لمشاركة المفتاح - يرجى التحقق من اتصالاتك الأخرى في طلبات مشاركة المفتاح.",
"%(senderName)s uploaded a file": "%(senderName)s رفع ملفًّا",
"%(senderName)s sent a video": "%(senderName)s أرسل مرئيًّا",
"%(senderName)s sent an image": "%(senderName)s أرسل صورة",
"This event could not be displayed": "تعذر عرض هذا الحدث", "This event could not be displayed": "تعذر عرض هذا الحدث",
"Mod": "مشرف", "Mod": "مشرف",
"Edit message": "تعديل الرسالة", "Edit message": "تعديل الرسالة",
@ -1209,7 +1082,6 @@
"You have not verified this user.": "أنت لم تتحقق من هذا المستخدم.", "You have not verified this user.": "أنت لم تتحقق من هذا المستخدم.",
"This user has not verified all of their sessions.": "هذا المستخدم لم يتحقق من جميع اتصالاته.", "This user has not verified all of their sessions.": "هذا المستخدم لم يتحقق من جميع اتصالاته.",
"Drop file here to upload": "قم بإسقاط الملف هنا ليُرفَع", "Drop file here to upload": "قم بإسقاط الملف هنا ليُرفَع",
"Drop File Here": "أسقط الملف هنا",
"Phone Number": "رقم الهاتف", "Phone Number": "رقم الهاتف",
"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. الرجاء إدخال رمز التحقق الذي فيها.",
"Remove %(phone)s?": "حذف %(phone)s؟", "Remove %(phone)s?": "حذف %(phone)s؟",
@ -1238,7 +1110,6 @@
"Your email address hasn't been verified yet": "لم يتم التحقق من عنوان بريدك الإلكتروني حتى الآن", "Your email address hasn't been verified yet": "لم يتم التحقق من عنوان بريدك الإلكتروني حتى الآن",
"Unable to share email address": "تعذرت مشاركة البريد الإلتكروني", "Unable to share email address": "تعذرت مشاركة البريد الإلتكروني",
"Unable to revoke sharing for email address": "تعذر إبطال مشاركة عنوان البريد الإلكتروني", "Unable to revoke sharing for email address": "تعذر إبطال مشاركة عنوان البريد الإلكتروني",
"Who can access this room?": "من يمكنه الوصول إلى هذه الغرفة؟",
"Encrypted": "التشفير", "Encrypted": "التشفير",
"Once enabled, encryption cannot be disabled.": "لا يمكن تعطيل التشفير بعد تمكينه.", "Once enabled, encryption cannot be disabled.": "لا يمكن تعطيل التشفير بعد تمكينه.",
"Security & Privacy": "الأمان والخصوصية", "Security & Privacy": "الأمان والخصوصية",
@ -1248,12 +1119,8 @@
"Members only (since the point in time of selecting this option)": "الأعضاء فقط (منذ اللحظة التي حدد فيها هذا الخيار)", "Members only (since the point in time of selecting this option)": "الأعضاء فقط (منذ اللحظة التي حدد فيها هذا الخيار)",
"Anyone": "أي أحد", "Anyone": "أي أحد",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "ستنطبق التغييرات على من يمكنه قراءة السجل على الرسائل المستقبلية في هذه الغرفة فقط. رؤية التاريخ الحالي لن تتغير.", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "ستنطبق التغييرات على من يمكنه قراءة السجل على الرسائل المستقبلية في هذه الغرفة فقط. رؤية التاريخ الحالي لن تتغير.",
"Anyone who knows the room's link, including guests": "أي شخص يعرف رابط الغرفة بما في ذلك الضيوف",
"Anyone who knows the room's link, apart from guests": "أي شخص يعرف رابط الغرفة باستثناء الضيوف",
"Only people who have been invited": "المدعوون فقط", "Only people who have been invited": "المدعوون فقط",
"To link to this room, please add an address.": "للربط لهذه الغرفة ، يرجى إضافة عنوان.", "To link to this room, please add an address.": "للربط لهذه الغرفة ، يرجى إضافة عنوان.",
"Click here to fix": "انقر هنا للإصلاح",
"Guests cannot join this room even if explicitly invited.": "لا يمكن للضيوف الانضمام إلى هذه الغرفة حتى إذا تمت دعوتهم بعينهم.",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "لا يمكن العدول عن التشفير بعد تمكينه للغرفة. التشفير يحجب حتى الخادم من رؤية رسائل الغرفة، فقط أعضاؤها هم من يرونها. قد يمنع تمكين التشفير العديد من الروبوتات والجسور من العمل بشكل صحيح. <a> اعرف المزيد حول التشفير. </a>", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "لا يمكن العدول عن التشفير بعد تمكينه للغرفة. التشفير يحجب حتى الخادم من رؤية رسائل الغرفة، فقط أعضاؤها هم من يرونها. قد يمنع تمكين التشفير العديد من الروبوتات والجسور من العمل بشكل صحيح. <a> اعرف المزيد حول التشفير. </a>",
"Enable encryption?": "تمكين التشفير؟", "Enable encryption?": "تمكين التشفير؟",
"Select the roles required to change various parts of the room": "حدد الأدوار المطلوبة لتغيير أجزاء مختلفة من الغرفة", "Select the roles required to change various parts of the room": "حدد الأدوار المطلوبة لتغيير أجزاء مختلفة من الغرفة",
@ -1372,11 +1239,8 @@
"Don't miss a reply": "لا تفوت أي رد", "Don't miss a reply": "لا تفوت أي رد",
"Later": "لاحقاً", "Later": "لاحقاً",
"Review": "مراجعة", "Review": "مراجعة",
"Verify all your sessions to ensure your account & messages are safe": "تحقق من جميع اتصالاتك للتأكد من أمان حسابك ورسائلك",
"Review where youre logged in": "راجع أماكن تسجيل دخولك",
"No": "لا", "No": "لا",
"I want to help": "أريد أن أساعد", "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "أرسل <UsageDataLink>بيانات استخدام مجهولة</UsageDataLink> والتي تساعدنا في تحسين %(brand)s. سيستخدم هذا <PolicyLink>ملف تعريف الارتباط</PolicyLink>.",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "أرسل <UsageDataLink> بيانات استخدام مجهولة </ UseDataLink> والتي تساعدنا في تحسين %(brand)s. سيستخدم هذا <PolicyLink> ملف تعريف الارتباط </ PolicyLink>.",
"Help us improve %(brand)s": "ساعدنا في تحسين %(brand)s", "Help us improve %(brand)s": "ساعدنا في تحسين %(brand)s",
"Unknown App": "تطبيق غير معروف", "Unknown App": "تطبيق غير معروف",
"Short keyboard patterns are easy to guess": "من السهل تخمين أنماط قصيرة من لوحة المفاتيح", "Short keyboard patterns are easy to guess": "من السهل تخمين أنماط قصيرة من لوحة المفاتيح",
@ -1414,7 +1278,6 @@
"United States": "الولايات المتحدة", "United States": "الولايات المتحدة",
"End conference": "إنهاء المؤتمر", "End conference": "إنهاء المؤتمر",
"Answered Elsewhere": "أُجيب في مكان آخر", "Answered Elsewhere": "أُجيب في مكان آخر",
"The other party declined the call.": "رفض الطرف الآخر المكالمة.",
"Default Device": "الجهاز الاعتيادي", "Default Device": "الجهاز الاعتيادي",
"Albania": "ألبانيا", "Albania": "ألبانيا",
"Afghanistan": "أفغانستان", "Afghanistan": "أفغانستان",
@ -1422,7 +1285,6 @@
"This will end the conference for everyone. Continue?": "هذا سينهي المؤتمر للجميع. استمر؟", "This will end the conference for everyone. Continue?": "هذا سينهي المؤتمر للجميع. استمر؟",
"The call was answered on another device.": "تم الرد على المكالمة على جهاز آخر.", "The call was answered on another device.": "تم الرد على المكالمة على جهاز آخر.",
"The call could not be established": "تعذر إجراء المكالمة", "The call could not be established": "تعذر إجراء المكالمة",
"Call Declined": "رُفض الاتصال",
"See videos posted to your active room": "أظهر الفيديوهات المرسلة إلى هذه غرفتك النشطة", "See videos posted to your active room": "أظهر الفيديوهات المرسلة إلى هذه غرفتك النشطة",
"See videos posted to this room": "أظهر الفيديوهات المرسلة إلى هذه الغرفة", "See videos posted to this room": "أظهر الفيديوهات المرسلة إلى هذه الغرفة",
"Send videos as you in your active room": "أرسل الفيديوهات بهويتك في غرفتك النشطة", "Send videos as you in your active room": "أرسل الفيديوهات بهويتك في غرفتك النشطة",
@ -1474,10 +1336,6 @@
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر المستخدمين المطابقة %(glob)s بسبب %(reason)s", "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر المستخدمين المطابقة %(glob)s بسبب %(reason)s",
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الخوادم المطابقة %(glob)s بسبب %(reason)s", "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الخوادم المطابقة %(glob)s بسبب %(reason)s",
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الغرفة المطابقة %(glob)s بسبب %(reason)s", "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الغرفة المطابقة %(glob)s بسبب %(reason)s",
"%(senderName)s declined the call.": "%(senderName)s رفض المكالمة.",
"(an error occurred)": "(حدث خطأ)",
"(their device couldn't start the camera / microphone)": "(تعذر على جهازهم بدء تشغيل الكاميرا / الميكروفون)",
"(connection failed)": "(فشل الاتصال)",
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 جميع الخوادم ممنوعة من المشاركة! لم يعد من الممكن استخدام هذه الغرفة.", "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 جميع الخوادم ممنوعة من المشاركة! لم يعد من الممكن استخدام هذه الغرفة.",
"Takes the call in the current room off hold": "يوقف المكالمة في الغرفة الحالية", "Takes the call in the current room off hold": "يوقف المكالمة في الغرفة الحالية",
"Places the call in the current room on hold": "يضع المكالمة في الغرفة الحالية قيد الانتظار", "Places the call in the current room on hold": "يضع المكالمة في الغرفة الحالية قيد الانتظار",
@ -1485,9 +1343,7 @@
"No other application is using the webcam": "لا يوجد تطبيق آخر يستخدم كاميرا الويب", "No other application is using the webcam": "لا يوجد تطبيق آخر يستخدم كاميرا الويب",
"Permission is granted to use the webcam": "منح الإذن باستخدام كاميرا الويب", "Permission is granted to use the webcam": "منح الإذن باستخدام كاميرا الويب",
"A microphone and webcam are plugged in and set up correctly": "الميكروفون وكاميرا ويب موصولان ومعدان بشكل صحيح", "A microphone and webcam are plugged in and set up correctly": "الميكروفون وكاميرا ويب موصولان ومعدان بشكل صحيح",
"Call failed because no webcam or microphone could not be accessed. Check that:": "فشلت المكالمة نظرًا لتعذر الوصول إلى كاميرا الويب أو الميكروفون. تحقق مما يلي:",
"Unable to access webcam / microphone": "تعذر الوصول إلى كاميرا الويب / الميكروفون", "Unable to access webcam / microphone": "تعذر الوصول إلى كاميرا الويب / الميكروفون",
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لأنه لا يمكن الوصول إلى ميكروفون. تحقق من توصيل الميكروفون وإعداده بشكل صحيح.",
"Unable to access microphone": "تعذر الوصول إلى الميكروفون", "Unable to access microphone": "تعذر الوصول إلى الميكروفون",
"Cuba": "كوبا", "Cuba": "كوبا",
"Croatia": "كرواتيا", "Croatia": "كرواتيا",
@ -1550,17 +1406,150 @@
"Already in call": "في مكالمة بالفعل", "Already in call": "في مكالمة بالفعل",
"You've reached the maximum number of simultaneous calls.": "لقد وصلت للحد الاقصى من المكالمات المتزامنة.", "You've reached the maximum number of simultaneous calls.": "لقد وصلت للحد الاقصى من المكالمات المتزامنة.",
"Too Many Calls": "مكالمات كثيرة جدا", "Too Many Calls": "مكالمات كثيرة جدا",
"Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك قم بالتأكد.", "Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا، من فضلك قم بالتأكد:",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.", "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.",
"Explore rooms": "استكشِف الغرف", "Explore rooms": "استكشِف الغرف",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات <helpIcon /> مع %(widgetDomain)s ومدير التكامل الخاص بك.", "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات <helpIcon /> مع %(widgetDomain)s ومدير التكامل الخاص بك.",
"Identity server is": "خادم الهوية هو", "Identity server is": "خادم الهوية هو",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.", "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط، ويمكنهم تعديل عناصر واجهة المستخدم، وإرسال دعوات الغرف، وتعيين مستويات القوة نيابة عنك.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.", "Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.", "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"Identity server": "خادوم الهوية", "Identity server": "خادوم الهوية",
"Identity server (%(server)s)": "خادوم الهوية (%(server)s)", "Identity server (%(server)s)": "خادوم الهوية (%(server)s)",
"Could not connect to identity server": "تعذر الاتصال بخادوم الهوية", "Could not connect to identity server": "تعذر الاتصال بخادوم الهوية",
"Not a valid identity server (status code %(code)s)": "ليس خادوم هوية صالح (رمز الحالة %(code)s)", "Not a valid identity server (status code %(code)s)": "ليس خادوم هوية صالح (رمز الحالة %(code)s)",
"Identity server URL must be HTTPS": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS" "Identity server URL must be HTTPS": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS",
"%(targetName)s rejected the invitation": "رفض %(targetName)s الدعوة",
"%(targetName)s joined the room": "انضم %(targetName)s إلى الغرفة",
"%(senderName)s made no change": "لم يقم %(senderName)s بأي تغيير",
"%(senderName)s set a profile picture": "قام %(senderName)s بتعيين صورة رمزية",
"%(senderName)s changed their profile picture": "%(senderName)s قام بتغيير صورته الرمزية",
"Paraguay": "باراغواي",
"Netherlands": "هولندا",
"Dismiss read marker and jump to bottom": "تجاهل علامة القراءة وانتقل إلى الأسفل",
"Scroll up/down in the timeline": "قم بالتمرير لأعلى/لأسفل في الجدول الزمني",
"Toggle video on/off": "تبديل تشغيل/إيقاف الفيديو",
"Toggle microphone mute": "تبديل كتم صوت الميكروفون",
"Cancel replying to a message": "إلغاء الرد على رسالة",
"Jump to start/end of the composer": "انتقل إلى بداية/نهاية المؤلف",
"Navigate recent messages to edit": "تصفح الرسائل الأخيرة لتحريرها",
"New line": "سطر جديد",
"[number]": "[رقم]",
"Greece": "اليونان",
"%(senderName)s removed their profile picture": "%(senderName)s أزال صورة ملفه الشخصي",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s أزال اسمه (%(oldDisplayName)s)",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s قام بتعيين اسمه إلى %(displayName)s",
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s غير اسمه إلى %(displayName)s",
"%(senderName)s banned %(targetName)s": "%(senderName)s حظر %(targetName)s",
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s حظر %(targetName)s: %(reason)s",
"%(senderName)s invited %(targetName)s": "%(senderName)s دعى %(targetName)s",
"%(targetName)s accepted an invitation": "%(targetName)s قبل دعوة",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s قبل الدعوة ل %(displayName)s",
"Converts the DM to a room": "تحويل المحادثة المباشرة إلى غرفة",
"Converts the room to a DM": "تحويل الغرفة إلى محادثة مباشرة",
"Some invites couldn't be sent": "تعذر إرسال بعض الدعوات",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "أرسلنا الآخرين، ولكن لم تتم دعوة الأشخاص أدناه إلى <RoomName/>",
"Zimbabwe": "زمبابوي",
"Yemen": "اليمن",
"Vietnam": "فيتنام",
"Venezuela": "فنزويلا",
"Uzbekistan": "أوزباكستان",
"United Arab Emirates": "الامارات العربية المتحدة",
"Ukraine": "اوكرانيا",
"Uganda": "اوغندا",
"Turkmenistan": "تركمانستان",
"Turkey": "تركيا",
"Tunisia": "تونس",
"Thailand": "تايلند",
"Tanzania": "تنزانيا",
"Tajikistan": "طاجاكستان",
"Taiwan": "تايوان",
"Syria": "سوريا",
"Sweden": "السويد",
"Sudan": "السودان",
"Sri Lanka": "سيريلانكا",
"Spain": "اسبانيا",
"South Sudan": "السودان الجنوبية",
"South Korea": "كوريا الجنوبية",
"South Africa": "جنوب افريقيا",
"Somalia": "الصومال",
"Slovakia": "سلوفاكيا",
"Singapore": "سنغافورة",
"Serbia": "صربيا",
"Senegal": "السنغال",
"Saudi Arabia": "المملكة العربية السعودية",
"Rwanda": "رواندا",
"Russia": "روسيا",
"Romania": "رومانيا",
"Qatar": "قطر",
"Portugal": "البرتغال",
"Poland": "بولاندا",
"Philippines": "الفلبين",
"Panama": "باناما",
"Palestine": "فلسطين",
"Pakistan": "باكستان",
"Oman": "عمان",
"Norway": "النرويج",
"North Korea": "كوريا الشمالية",
"Nigeria": "نيجيريا",
"Niger": "النيجر",
"Nicaragua": "نيكاراقوا",
"New Zealand": "نيوزلاندا",
"Nepal": "النيبال",
"Myanmar": "ماينمار",
"Mozambique": "موزمبيق",
"Morocco": "المغرب",
"Mongolia": "منغوليا",
"Mexico": "المكسيك",
"Mauritius": "موريشيوس",
"Mauritania": "موريتانيا",
"Malta": "مالطا",
"Mali": "مالي",
"Maldives": "جزر المالديف",
"Malaysia": "ماليزيا",
"Madagascar": "مدغشقر",
"Luxembourg": "لوكسمبرغ",
"Libya": "ليبيا",
"Liberia": "ليبريا",
"Lebanon": "لبنان",
"Latvia": "لاتفيا",
"Kuwait": "الكويت",
"Kenya": "كينيا",
"Kazakhstan": "كازاخستان",
"Jordan": "الأردن",
"Japan": "اليابان",
"Jamaica": "جامايكا",
"Italy": "ايطاليا",
"Israel": "فلسطين (اسرائيل المحتلة)",
"Ireland": "ايرلاندا",
"Iraq": "العراق",
"Iran": "ايران",
"Indonesia": "اندونيسيا",
"India": "الهند",
"Iceland": "ايسلاندا",
"Hungary": "هنقاريا",
"Hong Kong": "هونج كونج",
"Ghana": "غانا",
"Germany": "ألمانيا",
"Georgia": "جورجيا",
"France": "فرنسا",
"Finland": "فنلندا",
"Ethiopia": "اثيوبيا",
"Estonia": "استونيا",
"Eritrea": "إريتيريا",
"Egypt": "مصر",
"Ecuador": "الإكوادور",
"Denmark": "الدنمارك",
"Czech Republic": "جمهورية التشيك",
"Cyprus": "قبرص",
"Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "خادمك المنزلي رفض محاولة تسجيلك الدخول. قد يكون هذا بسبب الأشياء التي تستغرق وقتًا طويلاً جدًا. الرجاء المحاولة مرة اخرى. إذا استمر هذا الأمر، يرجى الاتصال بمسؤول الخادم المنزلي.",
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "تعذر الوصول إلى الخادم الرئيسي الخاص بك ولم يتمكن من تسجيل دخولك. يرجى المحاولة مرة أخرى. إذا استمر هذا ، يرجى الاتصال بمسؤول الخادم المنزلي الخاص بك.",
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "لقد طلبنا من المتصفح أن يتذكر الخادم الرئيسي الذي تستخدمه للسماح لك بتسجيل الدخول، ولكن للأسف نسيه متصفحك. اذهب إلى صفحة تسجيل الدخول وحاول مرة أخرى.",
"Failed to transfer call": "فشل تحويل المكالمة",
"Transfer Failed": "فشل التحويل",
"Unable to transfer call": "غير قادر على تحويل المكالمة",
"There was an error looking up the phone number": "حدث خطأ أثناء البحث عن رقم الهاتف",
"Unable to look up phone number": "غير قادر على ايجاد رقم الهاتف",
"The user you called is busy.": "المستخدم الذي اتصلت به مشغول.",
"User Busy": "المستخدم مشغول"
} }

View file

@ -1,7 +1,6 @@
{ {
"Collecting app version information": "Proqramın versiyası haqqında məlumatın yığılması", "Collecting app version information": "Proqramın versiyası haqqında məlumatın yığılması",
"Collecting logs": "Jurnalların bir yığım", "Collecting logs": "Jurnalların bir yığım",
"Uploading report": "Hesabatın göndərilməsi",
"Waiting for response from server": "Serverdən cavabın gözlənməsi", "Waiting for response from server": "Serverdən cavabın gözlənməsi",
"Messages containing my display name": "Mənim adımı özündə saxlayan mesajlar", "Messages containing my display name": "Mənim adımı özündə saxlayan mesajlar",
"Messages in one-to-one chats": "Fərdi çatlarda mesajlar", "Messages in one-to-one chats": "Fərdi çatlarda mesajlar",
@ -9,20 +8,8 @@
"When I'm invited to a room": "Nə vaxt ki, məni otağa dəvət edirlər", "When I'm invited to a room": "Nə vaxt ki, məni otağa dəvət edirlər",
"Call invitation": "Dəvət zəngi", "Call invitation": "Dəvət zəngi",
"Messages sent by bot": "Botla göndərilmiş mesajlar", "Messages sent by bot": "Botla göndərilmiş mesajlar",
"Error saving email notification preferences": "Email üzrə xəbərdarlıqların qurmalarının saxlanılması səhv",
"An error occurred whilst saving your email notification preferences.": "Email üzrə bildirişin qurmalarının saxlanılması səhv yarandı.",
"Keywords": "Açar sözlər",
"Enter keywords separated by a comma:": "Vergül bölünmüş açar sözləri daxil edin:",
"OK": "OK", "OK": "OK",
"Failed to change settings": "Qurmaları dəyişdirməyi bacarmadı",
"Operation failed": "Əməliyyatın nasazlığı", "Operation failed": "Əməliyyatın nasazlığı",
"Can't update user notification settings": "Bildirişin istifadəçi qurmalarını yeniləməyə müvəffəq olmur",
"Failed to update keywords": "Açar sözləri yeniləməyi bacarmadı",
"Messages containing <span>keywords</span>": "Müəyyən <span>açar sözləri</span> özündə saxlayan mesajlar",
"Notify for all other messages/rooms": "Bütün başqa mesajdan/otaqlardan xəbər vermək",
"Notify me for anything else": "Bütün qalan hadisələrdə xəbər vermək",
"Enable notifications for this account": "Bu hesab üçün xəbərdarlıqları qoşmaq",
"All notifications are currently disabled for all targets.": "Bütün qurğular üçün bütün bildirişlər kəsilmişdir.",
"Failed to verify email address: make sure you clicked the link in the email": "Email-i yoxlamağı bacarmadı: əmin olun ki, siz məktubda istinaddakı ünvana keçdiniz", "Failed to verify email address: make sure you clicked the link in the email": "Email-i yoxlamağı bacarmadı: əmin olun ki, siz məktubda istinaddakı ünvana keçdiniz",
"The platform you're on": "İstifadə edilən platforma", "The platform you're on": "İstifadə edilən platforma",
"The version of %(brand)s": "%(brand)s versiyası", "The version of %(brand)s": "%(brand)s versiyası",
@ -35,10 +22,6 @@
"Your device resolution": "Sizin cihazınızın qətnaməsi", "Your device resolution": "Sizin cihazınızın qətnaməsi",
"The information being sent to us to help make %(brand)s better includes:": "%(brand)s'i daha yaxşı etmək üçün bizə göndərilən məlumatlar daxildir:", "The information being sent to us to help make %(brand)s better includes:": "%(brand)s'i daha yaxşı etmək üçün bizə göndərilən məlumatlar daxildir:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Əgər bu səhifədə şəxsi xarakterin məlumatları rast gəlinirsə, məsələn otağın, istifadəçinin adının və ya qrupun adı, onlar serverə göndərilmədən əvvəl silinirlər.", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Əgər bu səhifədə şəxsi xarakterin məlumatları rast gəlinirsə, məsələn otağın, istifadəçinin adının və ya qrupun adı, onlar serverə göndərilmədən əvvəl silinirlər.",
"Call Timeout": "Cavab yoxdur",
"Unable to capture screen": "Ekranın şəkilini etməyə müvəffəq olmur",
"Existing Call": "Cari çağırış",
"You are already in a call.": "Danışıq gedir.",
"VoIP is unsupported": "Zənglər dəstəklənmir", "VoIP is unsupported": "Zənglər dəstəklənmir",
"You cannot place VoIP calls in this browser.": "Zənglər bu brauzerdə dəstəklənmir.", "You cannot place VoIP calls in this browser.": "Zənglər bu brauzerdə dəstəklənmir.",
"You cannot place a call with yourself.": "Siz özünə zəng vura bilmirsiniz.", "You cannot place a call with yourself.": "Siz özünə zəng vura bilmirsiniz.",
@ -78,8 +61,6 @@
"Missing room_id in request": "Sorğuda room_id yoxdur", "Missing room_id in request": "Sorğuda room_id yoxdur",
"Missing user_id in request": "Sorğuda user_id yoxdur", "Missing user_id in request": "Sorğuda user_id yoxdur",
"Usage": "İstifadə", "Usage": "İstifadə",
"/ddg is not a command": "/ddg — bu komanda deyil",
"To use it, just wait for autocomplete results to load and tab through them.": "Bu funksiyadan istifadə etmək üçün, avto-əlavənin pəncərəsində nəticələrin yükləməsini gözləyin, sonra burulma üçün Tab-dan istifadə edin.",
"Changes your display nickname": "Sizin təxəllüsünüz dəyişdirir", "Changes your display nickname": "Sizin təxəllüsünüz dəyişdirir",
"Invites user with given id to current room": "Verilmiş ID-lə istifadəçini cari otağa dəvət edir", "Invites user with given id to current room": "Verilmiş ID-lə istifadəçini cari otağa dəvət edir",
"Leave room": "Otağı tərk etmək", "Leave room": "Otağı tərk etmək",
@ -94,26 +75,8 @@
"Deops user with given id": "Verilmiş ID-lə istifadəçidən operatorun səlahiyyətlərini çıxardır", "Deops user with given id": "Verilmiş ID-lə istifadəçidən operatorun səlahiyyətlərini çıxardır",
"Displays action": "Hərəkətlərin nümayişi", "Displays action": "Hərəkətlərin nümayişi",
"Reason": "Səbəb", "Reason": "Səbəb",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s %(displayName)s-dən dəvəti qəbul etdi.",
"%(targetName)s accepted an invitation.": "%(targetName)s dəvəti qəbul etdi.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s %(targetName)s-nı dəvət edir.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s %(targetName)s-i blokladı.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s öz görünüş adını sildi (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s avatarını sildi.",
"%(senderName)s changed their profile picture.": "%(senderName)s öz avatar-ı dəyişdirdi.",
"VoIP conference started.": "Konfrans-zəng başlandı.",
"%(targetName)s joined the room.": "%(targetName)s otağa girdi.",
"VoIP conference finished.": "Konfrans-zəng qurtarılmışdır.",
"%(targetName)s rejected the invitation.": "%(targetName)s dəvəti rədd etdi.",
"%(targetName)s left the room.": "%(targetName)s otaqdan çıxdı.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s %(targetName)s blokdan çıxardı.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s %(targetName)s-nı qovdu.",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s öz dəvətini sildi %(targetName)s.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s otağın mövzusunu \"%(topic)s\" dəyişdirdi.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s otağın mövzusunu \"%(topic)s\" dəyişdirdi.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s otağın adını %(roomName)s dəyişdirdi.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s otağın adını %(roomName)s dəyişdirdi.",
"(not supported by this browser)": "(bu brauzerlə dəstəklənmir)",
"%(senderName)s answered the call.": "%(senderName)s zəngə cavab verdi.",
"%(senderName)s ended the call.": "%(senderName)s zəng qurtardı.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s dəvət edilmiş iştirakçılar üçün danışıqların tarixini açdı.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s dəvət edilmiş iştirakçılar üçün danışıqların tarixini açdı.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s girmiş iştirakçılar üçün danışıqların tarixini açdı.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s girmiş iştirakçılar üçün danışıqların tarixini açdı.",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s iştirakçılar üçün danışıqların tarixini açdı.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s iştirakçılar üçün danışıqların tarixini açdı.",
@ -123,7 +86,6 @@
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s hüquqların səviyyələrini dəyişdirdi %(powerLevelDiffText)s.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s hüquqların səviyyələrini dəyişdirdi %(powerLevelDiffText)s.",
"Failed to join room": "Otağa girməyi bacarmadı", "Failed to join room": "Otağa girməyi bacarmadı",
"Always show message timestamps": "Həmişə mesajların göndərilməsi vaxtını göstərmək", "Always show message timestamps": "Həmişə mesajların göndərilməsi vaxtını göstərmək",
"Autoplay GIFs and videos": "GIF animasiyalarını və videolarını avtomatik olaraq oynayır",
"Accept": "Qəbul etmək", "Accept": "Qəbul etmək",
"Error": "Səhv", "Error": "Səhv",
"Incorrect verification code": "Təsdiq etmənin səhv kodu", "Incorrect verification code": "Təsdiq etmənin səhv kodu",
@ -177,11 +139,7 @@
"No users have specific privileges in this room": "Heç bir istifadəçi bu otaqda xüsusi hüquqlara malik deyil", "No users have specific privileges in this room": "Heç bir istifadəçi bu otaqda xüsusi hüquqlara malik deyil",
"Banned users": "Bloklanmış istifadəçilər", "Banned users": "Bloklanmış istifadəçilər",
"Favourite": "Seçilmiş", "Favourite": "Seçilmiş",
"Click here to fix": "Düzəltmək üçün, buraya basın",
"Who can access this room?": "Kim bu otağa girə bilər?",
"Only people who have been invited": "Yalnız dəvət edilmiş iştirakçılar", "Only people who have been invited": "Yalnız dəvət edilmiş iştirakçılar",
"Anyone who knows the room's link, apart from guests": "Hamı, kimdə bu otağa istinad var, qonaqlardan başqa",
"Anyone who knows the room's link, including guests": "Hamı, kimdə bu otağa istinad var, qonaqlar daxil olmaqla",
"Who can read history?": "Kim tarixi oxuya bilər?", "Who can read history?": "Kim tarixi oxuya bilər?",
"Permissions": "Girişin hüquqları", "Permissions": "Girişin hüquqları",
"Advanced": "Təfərrüatlar", "Advanced": "Təfərrüatlar",
@ -194,15 +152,12 @@
"Sign in with": "Seçmək", "Sign in with": "Seçmək",
"Register": "Qeydiyyatdan keçmək", "Register": "Qeydiyyatdan keçmək",
"Remove": "Silmək", "Remove": "Silmək",
"You are not receiving desktop notifications": "Siz sistem xəbərdarlıqlarını almırsınız",
"What's New": "Nə dəyişdi", "What's New": "Nə dəyişdi",
"Update": "Yeniləmək", "Update": "Yeniləmək",
"Create new room": "Otağı yaratmaq", "Create new room": "Otağı yaratmaq",
"No results": "Nəticə yoxdur", "No results": "Nəticə yoxdur",
"Home": "Başlanğıc", "Home": "Başlanğıc",
"Manage Integrations": "İnteqrasiyaları idarə etmə",
"%(items)s and %(lastItem)s": "%(items)s və %(lastItem)s", "%(items)s and %(lastItem)s": "%(items)s və %(lastItem)s",
"Room directory": "Otaqların kataloqu",
"Start chat": "Çata başlamaq", "Start chat": "Çata başlamaq",
"Create Room": "Otağı yaratmaq", "Create Room": "Otağı yaratmaq",
"Deactivate Account": "Hesabı bağlamaq", "Deactivate Account": "Hesabı bağlamaq",
@ -213,32 +168,19 @@
"Please check your email and click on the link it contains. Once this is done, click continue.": "Öz elektron poçtunu yoxlayın və olan istinadı basın. Bundan sonra düyməni Davam etməyə basın.", "Please check your email and click on the link it contains. Once this is done, click continue.": "Öz elektron poçtunu yoxlayın və olan istinadı basın. Bundan sonra düyməni Davam etməyə basın.",
"Unable to add email address": "Email-i əlavə etməyə müvəffəq olmur", "Unable to add email address": "Email-i əlavə etməyə müvəffəq olmur",
"Unable to verify email address.": "Email-i yoxlamağı bacarmadı.", "Unable to verify email address.": "Email-i yoxlamağı bacarmadı.",
"Username not available": "İstifadəçi adı mövcud deyil",
"An error occurred: %(error_string)s": "Səhv baş verdi: %(error_string)s",
"Username available": "İstifadəçi adı mövcuddur",
"Failed to change password. Is your password correct?": "Şifrəni əvəz etməyi bacarmadı. Siz cari şifrə düzgün daxil etdiniz?", "Failed to change password. Is your password correct?": "Şifrəni əvəz etməyi bacarmadı. Siz cari şifrə düzgün daxil etdiniz?",
"Reject invitation": "Dəvəti rədd etmək", "Reject invitation": "Dəvəti rədd etmək",
"Are you sure you want to reject the invitation?": "Siz əminsiniz ki, siz dəvəti rədd etmək istəyirsiniz?", "Are you sure you want to reject the invitation?": "Siz əminsiniz ki, siz dəvəti rədd etmək istəyirsiniz?",
"Name": "Ad", "Name": "Ad",
"There are no visible files in this room": "Bu otaqda görülən fayl yoxdur",
"Featured Users:": "Seçilmiş istifadəçilər:", "Featured Users:": "Seçilmiş istifadəçilər:",
"Failed to reject invitation": "Dəvəti rədd etməyi bacarmadı", "Failed to reject invitation": "Dəvəti rədd etməyi bacarmadı",
"Failed to leave room": "Otaqdan çıxmağı bacarmadı",
"For security, this session has been signed out. Please sign in again.": "Təhlükəsizliyin təmin olunması üçün sizin sessiyanız başa çatmışdır idi. Zəhmət olmasa, yenidən girin.", "For security, this session has been signed out. Please sign in again.": "Təhlükəsizliyin təmin olunması üçün sizin sessiyanız başa çatmışdır idi. Zəhmət olmasa, yenidən girin.",
"Logout": ıxmaq", "Logout": ıxmaq",
"You have no visible notifications": "Görülən xəbərdarlıq yoxdur",
"Files": "Fayllar",
"Notifications": "Xəbərdarlıqlar", "Notifications": "Xəbərdarlıqlar",
"Connectivity to the server has been lost.": "Serverlə əlaqə itirilmişdir.", "Connectivity to the server has been lost.": "Serverlə əlaqə itirilmişdir.",
"Sent messages will be stored until your connection has returned.": "Hələ ki serverlə əlaqə bərpa olmayacaq, göndərilmiş mesajlar saxlanacaq.", "Sent messages will be stored until your connection has returned.": "Hələ ki serverlə əlaqə bərpa olmayacaq, göndərilmiş mesajlar saxlanacaq.",
"Active call": "Aktiv çağırış",
"No more results": "Daha çox nəticə yoxdur", "No more results": "Daha çox nəticə yoxdur",
"Failed to reject invite": "Dəvəti rədd etməyi bacarmadı", "Failed to reject invite": "Dəvəti rədd etməyi bacarmadı",
"Fill screen": "Ekranı doldurmaq",
"Click to unmute video": "Klikləyin, videonu qoşmaq üçün",
"Click to mute video": "Klikləyin, videonu söndürmək üçün",
"Click to unmute audio": "Klikləyin, səsi qoşmaq üçün",
"Click to mute audio": "Klikləyin, səsi söndürmək üçün",
"Failed to load timeline position": "Xronologiyadan nişanı yükləməyi bacarmadı", "Failed to load timeline position": "Xronologiyadan nişanı yükləməyi bacarmadı",
"Unable to remove contact information": "Əlaqə məlumatlarının silməyi bacarmadı", "Unable to remove contact information": "Əlaqə məlumatlarının silməyi bacarmadı",
"<not supported>": "<dəstəklənmir>", "<not supported>": "<dəstəklənmir>",
@ -250,19 +192,13 @@
"Email": "E-poçt", "Email": "E-poçt",
"Profile": "Profil", "Profile": "Profil",
"Account": "Hesab", "Account": "Hesab",
"Access Token:": "Girişin token-i:",
"click to reveal": "açılış üçün basın",
"Homeserver is": "Ev serveri bu", "Homeserver is": "Ev serveri bu",
"Identity Server is": "Eyniləşdirmənin serveri bu",
"olm version:": "Olm versiyası:",
"Failed to send email": "Email göndərilməsinin səhvi", "Failed to send email": "Email göndərilməsinin səhvi",
"A new password must be entered.": "Yeni parolu daxil edin.", "A new password must be entered.": "Yeni parolu daxil edin.",
"New passwords must match each other.": "Yeni şifrələr uyğun olmalıdır.", "New passwords must match each other.": "Yeni şifrələr uyğun olmalıdır.",
"I have verified my email address": "Mən öz email-i təsdiq etdim", "I have verified my email address": "Mən öz email-i təsdiq etdim",
"Return to login screen": "Girişin ekranına qayıtmaq", "Return to login screen": "Girişin ekranına qayıtmaq",
"Send Reset Email": "Şifrənizi sıfırlamaq üçün istinadla məktubu göndərmək", "Send Reset Email": "Şifrənizi sıfırlamaq üçün istinadla məktubu göndərmək",
"Set a display name:": "Görünüş adını daxil edin:",
"Upload an avatar:": "Avatar yüklə:",
"This server does not support authentication with a phone number.": "Bu server telefon nömrəsinin köməyi ilə müəyyənləşdirilməni dəstəkləmir.", "This server does not support authentication with a phone number.": "Bu server telefon nömrəsinin köməyi ilə müəyyənləşdirilməni dəstəkləmir.",
"Commands": "Komandalar", "Commands": "Komandalar",
"Emoji": "Smaylar", "Emoji": "Smaylar",
@ -274,10 +210,6 @@
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "'Çörək parçaları' funksiyadan istifadə edmiirsiniz (otaqlar siyahısından yuxarıdakı avatarlar)", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "'Çörək parçaları' funksiyadan istifadə edmiirsiniz (otaqlar siyahısından yuxarıdakı avatarlar)",
"Analytics": "Analitik", "Analytics": "Analitik",
"Call Failed": "Uğursuz zəng", "Call Failed": "Uğursuz zəng",
"The remote side failed to pick up": "Qarşı tərəf ala bilmədi",
"Call in Progress": "Zəng edir",
"A call is currently being placed!": "Hazırda zəng edilir!",
"A call is already in progress!": "Zəng artıq edilir!",
"Permission Required": "İzn tələb olunur", "Permission Required": "İzn tələb olunur",
"You do not have permission to start a conference call in this room": "Bu otaqda konfrans başlamaq üçün icazə yoxdur", "You do not have permission to start a conference call in this room": "Bu otaqda konfrans başlamaq üçün icazə yoxdur",
"Replying With Files": "Dosyalarla cavab", "Replying With Files": "Dosyalarla cavab",
@ -312,7 +244,6 @@
"You are not in this room.": "Sən bu otaqda deyilsən.", "You are not in this room.": "Sən bu otaqda deyilsən.",
"You do not have permission to do that in this room.": "Bu otaqda bunu etməyə icazəniz yoxdur.", "You do not have permission to do that in this room.": "Bu otaqda bunu etməyə icazəniz yoxdur.",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "¯ \\ _ (ツ) _ / ¯ işarəsini mesaja elavə edir.", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "¯ \\ _ (ツ) _ / ¯ işarəsini mesaja elavə edir.",
"Searches DuckDuckGo for results": "Nəticələr üçün DuckDuckGo-da axtarır",
"Upgrades a room to a new version": "Bir otağı yeni bir versiyaya yüksəldir", "Upgrades a room to a new version": "Bir otağı yeni bir versiyaya yüksəldir",
"Changes your display nickname in the current room only": "Yalnız cari otaqda ekran ləqəbinizi dəyişdirir", "Changes your display nickname in the current room only": "Yalnız cari otaqda ekran ləqəbinizi dəyişdirir",
"Changes your avatar in this current room only": "Avatarınızı yalnız bu cari otaqda dəyişir", "Changes your avatar in this current room only": "Avatarınızı yalnız bu cari otaqda dəyişir",
@ -347,7 +278,6 @@
"Only continue if you trust the owner of the server.": "Yalnız server sahibinə etibar etsəniz davam edin.", "Only continue if you trust the owner of the server.": "Yalnız server sahibinə etibar etsəniz davam edin.",
"Trust": "Etibar", "Trust": "Etibar",
"Custom (%(level)s)": "Xüsusi (%(level)s)", "Custom (%(level)s)": "Xüsusi (%(level)s)",
"Failed to invite the following users to the %(roomName)s room:": "Aşağıdakı istifadəçiləri %(roomName)s otağına dəvət etmək alınmadı:",
"Room %(roomId)s not visible": "Otaq %(roomId)s görünmür", "Room %(roomId)s not visible": "Otaq %(roomId)s görünmür",
"Messages": "Mesajlar", "Messages": "Mesajlar",
"Actions": "Tədbirlər", "Actions": "Tədbirlər",
@ -363,11 +293,6 @@
"Please supply a https:// or http:// widget URL": "Zəhmət olmasa https:// və ya http:// widget URL təmin edin", "Please supply a https:// or http:// widget URL": "Zəhmət olmasa https:// və ya http:// widget URL təmin edin",
"Forces the current outbound group session in an encrypted room to be discarded": "Şifrəli bir otaqda mövcud qrup sessiyasını ləğv etməyə məcbur edir", "Forces the current outbound group session in an encrypted room to be discarded": "Şifrəli bir otaqda mövcud qrup sessiyasını ləğv etməyə məcbur edir",
"Displays list of commands with usages and descriptions": "İstifadə qaydaları və təsvirləri ilə komanda siyahısını göstərir", "Displays list of commands with usages and descriptions": "İstifadə qaydaları və təsvirləri ilə komanda siyahısını göstərir",
"%(senderName)s requested a VoIP conference.": "%(senderName)s VoIP konfrans istədi.",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s göstərilən adlarını %(displayName)s olaraq dəyişdirdi.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s öz adlarını %(displayName)s olaraq təyin etdilər.",
"%(senderName)s set a profile picture.": "%(senderName)s profil şəkli təyin etdi.",
"%(senderName)s made no change.": "%(senderName)s dəyişiklik etməyib.",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s otaq otağını sildi.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s otaq otağını sildi.",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s bu otağı təkmilləşdirdi.", "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s bu otağı təkmilləşdirdi.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s linki olanlara otağııq etdi.", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s linki olanlara otağııq etdi.",
@ -379,7 +304,6 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "Bu otaqda %(qruplar)s üçün %(senderDisplayName)s aktiv oldu.", "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "Bu otaqda %(qruplar)s üçün %(senderDisplayName)s aktiv oldu.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "Bu otaqda %(groups)s üçün %(senderDisplayName)s aktiv oldu.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "Bu otaqda %(groups)s üçün %(senderDisplayName)s aktiv oldu.",
"powered by Matrix": "Matrix tərəfindən təchiz edilmişdir", "powered by Matrix": "Matrix tərəfindən təchiz edilmişdir",
"Custom Server Options": "Fərdi Server Seçimləri",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.",
"Create Account": "Hesab Aç", "Create Account": "Hesab Aç",
"Explore rooms": "Otaqları kəşf edin", "Explore rooms": "Otaqları kəşf edin",

View file

@ -1,61 +1,38 @@
{ {
"Couldn't find a matching Matrix room": "Не атрымалася знайсці адпаведны пакой Matrix", "Couldn't find a matching Matrix room": "Не атрымалася знайсці адпаведны пакой Matrix",
"All messages (noisy)": "Усе паведамленні (гучна)",
"Reject": "Адхіліць", "Reject": "Адхіліць",
"Failed to forget room %(errCode)s": "Не атрымалася забыць пакой %(errCode)s", "Failed to forget room %(errCode)s": "Не атрымалася забыць пакой %(errCode)s",
"Failed to update keywords": "Не атрымалася абнавіць ключавыя словы",
"All messages": "Усе паведамленні", "All messages": "Усе паведамленні",
"All notifications are currently disabled for all targets.": "Усе апавяшчэнні ў цяперашні час адключаныя для ўсіх мэтаў.",
"Fetching third party location failed": "Не ўдалося атрымаць месцазнаходжанне трэцяга боку", "Fetching third party location failed": "Не ўдалося атрымаць месцазнаходжанне трэцяга боку",
"Guests can join": "Госці могуць далучыцца", "Guests can join": "Госці могуць далучыцца",
"Enable them now": "Уключыць іх зараз",
"Notification targets": "Мэты апавяшчэння", "Notification targets": "Мэты апавяшчэння",
"Failed to set direct chat tag": "Не ўдалося ўсталяваць тэг прамога чата", "Failed to set direct chat tag": "Не ўдалося ўсталяваць тэг прамога чата",
"Failed to set Direct Message status of room": "Не ўдалося ўсталяваць статут прамога паведамлення пакою",
"Favourite": "Улюбёнае", "Favourite": "Улюбёнае",
"Quote": "Цытата", "Quote": "Цытата",
"Dismiss": "Aдхіліць", "Dismiss": "Aдхіліць",
"Remove from Directory": "Выдалiць з каталога", "Remove from Directory": "Выдалiць з каталога",
"Cancel Sending": "Адмяніць адпраўку",
"Failed to add tag %(tagName)s to room": "Не атрымалася дадаць %(tagName)s ў пакоі", "Failed to add tag %(tagName)s to room": "Не атрымалася дадаць %(tagName)s ў пакоі",
"Close": "Зачыніць", "Close": "Зачыніць",
"Notifications": "Апавяшчэнні", "Notifications": "Апавяшчэнні",
"Low Priority": "Нізкі прыярытэт", "Low Priority": "Нізкі прыярытэт",
"%(brand)s does not know how to join a room on this network": "%(brand)s не ведае, як увайсці ў пакой у гэтай сетке", "%(brand)s does not know how to join a room on this network": "%(brand)s не ведае, як увайсці ў пакой у гэтай сетке",
"Members": "Удзельнікі", "Members": "Удзельнікі",
"Can't update user notification settings": "Немагчыма абнавіць налады апавяшчэнняў карыстальніка",
"Failed to change settings": "Не атрымалася змяніць налады",
"Noisy": "Шумна", "Noisy": "Шумна",
"Resend": "Паўторна", "Resend": "Паўторна",
"On": "Уключыць", "On": "Уключыць",
"remove %(name)s from the directory.": "выдаліць %(name)s з каталога.", "remove %(name)s from the directory.": "выдаліць %(name)s з каталога.",
"Off": "Выключыць", "Off": "Выключыць",
"Invite to this room": "Запрасіць у гэты пакой", "Invite to this room": "Запрасіць у гэты пакой",
"Notifications on the following keywords follow rules which cant be displayed here:": "Апавяшчэнні па наступных ключавых словах прытрымліваюцца правілаў, якія не могуць быць адлюстраваны тут:",
"Mentions only": "Толькі згадкі",
"Remove": "Выдалiць", "Remove": "Выдалiць",
"Failed to remove tag %(tagName)s from room": "Не ўдалося выдаліць %(tagName)s з пакоя", "Failed to remove tag %(tagName)s from room": "Не ўдалося выдаліць %(tagName)s з пакоя",
"Leave": "Пакінуць", "Leave": "Пакінуць",
"Enable notifications for this account": "Ўключыць апавяшчэнні для гэтага ўліковага запісу",
"Error": "Памылка", "Error": "Памылка",
"No rooms to show": "Няма пакояў для паказу", "No rooms to show": "Няма пакояў для паказу",
"Download this file": "Спампаваць гэты файл",
"Operation failed": "Не атрымалася выканаць аперацыю", "Operation failed": "Не атрымалася выканаць аперацыю",
"Forget": "Забыць",
"Mute": "Без гуку", "Mute": "Без гуку",
"Error saving email notification preferences": "Памылка захавання налад апавяшчэнняў па электроннай пошце",
"Enter keywords separated by a comma:": "Калі ласка, увядзіце ключавыя словы, падзеленыя коскамі:",
"powered by Matrix": "працуе на Matrix", "powered by Matrix": "працуе на Matrix",
"Custom Server Options": "Карыстальніцкія параметры сервера",
"Remove %(name)s from the directory?": "Выдаліць %(name)s з каталога?", "Remove %(name)s from the directory?": "Выдаліць %(name)s з каталога?",
"Notify me for anything else": "Паведаміць мне што-небудзь яшчэ",
"Source URL": "URL-адрас крыніцы", "Source URL": "URL-адрас крыніцы",
"Enable email notifications": "Ўключыць паведамлення па электроннай пошце",
"Files": "Файлы",
"Keywords": "Ключавыя словы",
"Direct Chat": "Прамы чат",
"An error occurred whilst saving your email notification preferences.": "Адбылася памылка падчас захавання налады апавяшчэнняў па электроннай пошце.",
"Room not found": "Пакой не знойдзены", "Room not found": "Пакой не знойдзены",
"Notify for all other messages/rooms": "Апавяшчаць для ўсіх іншых паведамленняў/пакояў",
"The server may be unavailable or overloaded": "Сервер можа быць недаступны ці перагружаны" "The server may be unavailable or overloaded": "Сервер можа быць недаступны ці перагружаны"
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
{ {
"Add a widget": "Afegeix un giny",
"Account": "Compte", "Account": "Compte",
"No Microphones detected": "No s'ha detectat cap micròfon", "No Microphones detected": "No s'ha detectat cap micròfon",
"No Webcams detected": "No s'ha detectat cap càmera web", "No Webcams detected": "No s'ha detectat cap càmera web",
@ -14,12 +13,10 @@
"Failed to forget room %(errCode)s": "No s'ha pogut oblidar la sala %(errCode)s", "Failed to forget room %(errCode)s": "No s'ha pogut oblidar la sala %(errCode)s",
"Favourite": "Favorit", "Favourite": "Favorit",
"Mute": "Silencia", "Mute": "Silencia",
"Room directory": "Directori de sales",
"Settings": "Configuració", "Settings": "Configuració",
"Start chat": "Inicia un xat", "Start chat": "Inicia un xat",
"Failed to change password. Is your password correct?": "S'ha produït un error en canviar la contrasenya. És correcta la teva contrasenya?", "Failed to change password. Is your password correct?": "S'ha produït un error en canviar la contrasenya. És correcta la teva contrasenya?",
"Continue": "Continua", "Continue": "Continua",
"Custom Server Options": "Opcions de servidor personalitzat",
"Dismiss": "Omet", "Dismiss": "Omet",
"Notifications": "Notificacions", "Notifications": "Notificacions",
"Remove": "Elimina", "Remove": "Elimina",
@ -29,7 +26,6 @@
"Search": "Cerca", "Search": "Cerca",
"powered by Matrix": "amb tecnologia de Matrix", "powered by Matrix": "amb tecnologia de Matrix",
"Edit": "Edita", "Edit": "Edita",
"Unpin Message": "Anul·la la fixació de missatge",
"Register": "Registre", "Register": "Registre",
"Rooms": "Sales", "Rooms": "Sales",
"Add rooms to this community": "Afegeix sales a aquesta comunitat", "Add rooms to this community": "Afegeix sales a aquesta comunitat",
@ -39,10 +35,6 @@
"This phone number is already in use": "Aquest número de telèfon ja està en ús", "This phone number is already in use": "Aquest número de telèfon ja està en ús",
"Failed to verify email address: make sure you clicked the link in the email": "No s'ha pogut verificar l'adreça de correu electrònic: assegura't de fer clic a l'enllaç del correu electrònic", "Failed to verify email address: make sure you clicked the link in the email": "No s'ha pogut verificar l'adreça de correu electrònic: assegura't de fer clic a l'enllaç del correu electrònic",
"Call Failed": "No s'ha pogut realitzar la trucada", "Call Failed": "No s'ha pogut realitzar la trucada",
"The remote side failed to pick up": "El part remota no ha contestat",
"Unable to capture screen": "No s'ha pogut capturar la pantalla",
"Existing Call": "Trucada existent",
"You are already in a call.": "Ja ets en una trucada.",
"VoIP is unsupported": "VoIP no és compatible", "VoIP is unsupported": "VoIP no és compatible",
"You cannot place VoIP calls in this browser.": "No pots fer trucades VoIP en aquest navegador.", "You cannot place VoIP calls in this browser.": "No pots fer trucades VoIP en aquest navegador.",
"You cannot place a call with yourself.": "No pots trucar-te a tu mateix.", "You cannot place a call with yourself.": "No pots trucar-te a tu mateix.",
@ -95,7 +87,6 @@
"Moderator": "Moderador", "Moderator": "Moderador",
"Admin": "Administrador", "Admin": "Administrador",
"Failed to invite": "No s'ha pogut convidar", "Failed to invite": "No s'ha pogut convidar",
"Failed to invite the following users to the %(roomName)s room:": "No s'ha pogut convidar a la sala %(roomName)s els següents usuaris:",
"You need to be logged in.": "Has d'haver iniciat sessió.", "You need to be logged in.": "Has d'haver iniciat sessió.",
"You need to be able to invite users to do that.": "Per fer això, necessites poder convidar a usuaris.", "You need to be able to invite users to do that.": "Per fer això, necessites poder convidar a usuaris.",
"Unable to create widget.": "No s'ha pogut crear el giny.", "Unable to create widget.": "No s'ha pogut crear el giny.",
@ -108,44 +99,17 @@
"Room %(roomId)s not visible": "Sala %(roomId)s no visible", "Room %(roomId)s not visible": "Sala %(roomId)s no visible",
"Missing user_id in request": "Falta l'user_id a la sol·licitud", "Missing user_id in request": "Falta l'user_id a la sol·licitud",
"Usage": "Ús", "Usage": "Ús",
"/ddg is not a command": "/ddg no és una ordre",
"To use it, just wait for autocomplete results to load and tab through them.": "Per utilitzar-ho, simplement espera que es completin els resultats automàticament i clica'n el desitjat.",
"Ignored user": "Usuari ignorat", "Ignored user": "Usuari ignorat",
"You are now ignoring %(userId)s": "Estàs ignorant l'usuari %(userId)s", "You are now ignoring %(userId)s": "Estàs ignorant l'usuari %(userId)s",
"Unignored user": "Usuari no ignorat", "Unignored user": "Usuari no ignorat",
"You are no longer ignoring %(userId)s": "Ja no estàs ignorant l'usuari %(userId)s", "You are no longer ignoring %(userId)s": "Ja no estàs ignorant l'usuari %(userId)s",
"Verified key": "Claus verificades", "Verified key": "Claus verificades",
"Call Timeout": "Temps d'espera de les trucades",
"Reason": "Raó", "Reason": "Raó",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s ha acceptat la invitació de %(displayName)s.",
"%(targetName)s accepted an invitation.": "%(targetName)s ha acceptat una invitació.",
"%(senderName)s requested a VoIP conference.": "%(senderName)s ha sol·licitat una conferència VoIP.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s ha convidat a %(targetName)s.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s ha expulsat a %(targetName)s.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s han establert el seu nom visible a %(displayName)s.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ha retirat el seu nom visible %(oldDisplayName)s.",
"%(senderName)s removed their profile picture.": "%(senderName)s ha retirat la seva foto de perfil.",
"%(senderName)s changed their profile picture.": "%(senderName)s ha canviat la seva foto de perfil.",
"%(senderName)s set a profile picture.": "%(senderName)s ha establert una foto de perfil.",
"VoIP conference started.": "S'ha iniciat la conferència VoIP.",
"%(targetName)s joined the room.": "%(targetName)s ha entrat a la sala.",
"VoIP conference finished.": "S'ha finalitzat la conferència VoIP.",
"%(targetName)s rejected the invitation.": "%(targetName)s ha rebutjat la invitació.",
"%(targetName)s left the room.": "%(targetName)s ha sortit de la sala.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s ha readmès a %(targetName)s.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s ha fet fora a %(targetName)s.",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s ha retirat la invitació per a %(targetName)s.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ha canviat el tema a \"%(topic)s\".", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ha canviat el tema a \"%(topic)s\".",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ha eliminat el nom de la sala.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ha eliminat el nom de la sala.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ha canviat el nom de la sala a %(roomName)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ha canviat el nom de la sala a %(roomName)s.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s ha enviat una imatge.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s ha enviat una imatge.",
"Someone": "Algú", "Someone": "Algú",
"(not supported by this browser)": "(no és compatible amb aquest navegador)",
"%(senderName)s answered the call.": "%(senderName)s ha contestat la trucada.",
"(could not connect media)": "(no s'ha pogut connectar el medi)",
"(no answer)": "(sense resposta)",
"(unknown failure: %(reason)s)": "(error desconegut: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s ha penjat.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha convidat a %(targetDisplayName)s a entrar a la sala.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha convidat a %(targetDisplayName)s a entrar a la sala.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres, a partir de que hi són convidats.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres, a partir de que hi són convidats.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres des de que s'hi uneixen.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres des de que s'hi uneixen.",
@ -168,18 +132,11 @@
"Failed to join room": "No s'ha pogut entrar a la sala", "Failed to join room": "No s'ha pogut entrar a la sala",
"Message Pinning": "Fixació de missatges", "Message Pinning": "Fixació de missatges",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostra les marques de temps en format de 12 hores (p.e. 2:30pm)", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostra les marques de temps en format de 12 hores (p.e. 2:30pm)",
"Autoplay GIFs and videos": "Reprodueix de forma automàtica els GIF i vídeos",
"Enable automatic language detection for syntax highlighting": "Activa la detecció automàtica d'idiomes per al ressaltat de sintaxi", "Enable automatic language detection for syntax highlighting": "Activa la detecció automàtica d'idiomes per al ressaltat de sintaxi",
"Automatically replace plain text Emoji": "Substitueix automàticament Emoji de text pla", "Automatically replace plain text Emoji": "Substitueix automàticament Emoji de text pla",
"Enable inline URL previews by default": "Activa per defecte la vista prèvia d'URL en línia", "Enable inline URL previews by default": "Activa per defecte la vista prèvia d'URL en línia",
"Enable URL previews for this room (only affects you)": "Activa la vista prèvia d'URL d'aquesta sala (no afecta altres usuaris)", "Enable URL previews for this room (only affects you)": "Activa la vista prèvia d'URL d'aquesta sala (no afecta altres usuaris)",
"Enable URL previews by default for participants in this room": "Activa per defecte la vista prèvia d'URL per als participants d'aquesta sala", "Enable URL previews by default for participants in this room": "Activa per defecte la vista prèvia d'URL per als participants d'aquesta sala",
"Room Colour": "Color de la sala",
"Active call (%(roomName)s)": "Trucada activa (%(roomName)s)",
"unknown caller": "trucada d'un desconegut",
"Incoming voice call from %(name)s": "Trucada de veu entrant de %(name)s",
"Incoming video call from %(name)s": "Trucada de vídeo entrant de %(name)s",
"Incoming call from %(name)s": "Trucada entrant de %(name)s",
"Decline": "Declina", "Decline": "Declina",
"Accept": "Accepta", "Accept": "Accepta",
"Incorrect verification code": "El codi de verificació és incorrecte", "Incorrect verification code": "El codi de verificació és incorrecte",
@ -201,18 +158,8 @@
"Authentication": "Autenticació", "Authentication": "Autenticació",
"Last seen": "Vist per última vegada", "Last seen": "Vist per última vegada",
"Failed to set display name": "No s'ha pogut establir el nom visible", "Failed to set display name": "No s'ha pogut establir el nom visible",
"Cannot add any more widgets": "No s'ha pogut afegir cap més giny",
"The maximum permitted number of widgets have already been added to this room.": "Ja s'han afegit el màxim de ginys permesos en aquesta sala.",
"Drop File Here": "Deixeu anar un fitxer aquí",
"Drop file here to upload": "Deixa anar el fitxer aquí per pujar-lo", "Drop file here to upload": "Deixa anar el fitxer aquí per pujar-lo",
" (unsupported)": " (incompatible)",
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Uneix-te com <voiceText>voice</voiceText> o <videoText>video</videoText>.",
"Ongoing conference call%(supportedText)s.": "Trucada de conferència en curs %(supportedText)s.",
"%(senderName)s sent an image": "%(senderName)s ha enviat una imatge",
"%(senderName)s sent a video": "%(senderName)s ha enviat un vídeo",
"%(senderName)s uploaded a file": "%(senderName)s ha pujat un fitxer",
"Options": "Opcions", "Options": "Opcions",
"Please select the destination room for this message": "Si us plau, seleccioneu la sala destinatària per a aquest missatge",
"Disinvite": "Descarta la invitació", "Disinvite": "Descarta la invitació",
"Kick": "Fes fora", "Kick": "Fes fora",
"Disinvite this user?": "Descartar la invitació per a aquest usuari?", "Disinvite this user?": "Descartar la invitació per a aquest usuari?",
@ -253,10 +200,7 @@
"Mirror local video feed": "Remet el flux de vídeo local", "Mirror local video feed": "Remet el flux de vídeo local",
"Server unavailable, overloaded, or something else went wrong.": "El servidor no està disponible, està sobrecarregat o alguna altra cosa no ha funcionat correctament.", "Server unavailable, overloaded, or something else went wrong.": "El servidor no està disponible, està sobrecarregat o alguna altra cosa no ha funcionat correctament.",
"Command error": "Error en l'ordre", "Command error": "Error en l'ordre",
"Jump to message": "Salta al missatge",
"No pinned messages.": "No hi ha cap missatge fixat.",
"Loading...": "S'està carregant...", "Loading...": "S'està carregant...",
"Pinned Messages": "Missatges fixats",
"%(duration)ss": "%(duration)ss", "%(duration)ss": "%(duration)ss",
"%(duration)sm": "%(duration)sm", "%(duration)sm": "%(duration)sm",
"%(duration)sh": "%(duration)sh", "%(duration)sh": "%(duration)sh",
@ -279,7 +223,6 @@
"Join Room": "Entra a la sala", "Join Room": "Entra a la sala",
"Upload avatar": "Puja l'avatar", "Upload avatar": "Puja l'avatar",
"Forget room": "Oblida la sala", "Forget room": "Oblida la sala",
"Community Invites": "Invitacions de les comunitats",
"Invites": "Invitacions", "Invites": "Invitacions",
"Favourites": "Preferits", "Favourites": "Preferits",
"Low priority": "Baixa prioritat", "Low priority": "Baixa prioritat",
@ -294,11 +237,7 @@
"Banned users": "Usuaris expulsats", "Banned users": "Usuaris expulsats",
"This room is not accessible by remote Matrix servers": "Aquesta sala no és accessible per a servidors de Matrix remots", "This room is not accessible by remote Matrix servers": "Aquesta sala no és accessible per a servidors de Matrix remots",
"Leave room": "Surt de la sala", "Leave room": "Surt de la sala",
"Guests cannot join this room even if explicitly invited.": "Els usuaris d'altres xarxes no poden entrar a la sala d'aquest esdeveniment encara que hi hagin sigut convidats de forma explícita.",
"Click here to fix": "Feu clic aquí per corregir-ho",
"Who can access this room?": "Qui pot entrar a aquesta sala?",
"Only people who have been invited": "Només les persones que hi hagin sigut convidades", "Only people who have been invited": "Només les persones que hi hagin sigut convidades",
"Anyone who knows the room's link, apart from guests": "Qualsevol que conegui l'enllaç de la sala, excepte usuaris d'altres xarxes",
"Publish this room to the public in %(domain)s's room directory?": "Vols publicar aquesta sala al directori de sales públiques de %(domain)s?", "Publish this room to the public in %(domain)s's room directory?": "Vols publicar aquesta sala al directori de sales públiques de %(domain)s?",
"Who can read history?": "Qui pot llegir l'historial?", "Who can read history?": "Qui pot llegir l'historial?",
"Anyone": "Qualsevol", "Anyone": "Qualsevol",
@ -306,9 +245,7 @@
"Members only (since they were invited)": "Només els membres (a partir del punt en què hi són convidats)", "Members only (since they were invited)": "Només els membres (a partir del punt en què hi són convidats)",
"Members only (since they joined)": "Només els membres (a partir del punt en què entrin a la sala)", "Members only (since they joined)": "Només els membres (a partir del punt en què entrin a la sala)",
"Permissions": "Permisos", "Permissions": "Permisos",
"Add a topic": "Afegeix un tema",
"Jump to first unread message.": "Salta al primer missatge no llegit.", "Jump to first unread message.": "Salta al primer missatge no llegit.",
"Anyone who knows the room's link, including guests": "Qualsevol que conegui l'enllaç de la sala, inclosos els usuaris d'altres xarxes",
"not specified": "sense especificar", "not specified": "sense especificar",
"This room has no local addresses": "Aquesta sala no té adreces locals", "This room has no local addresses": "Aquesta sala no té adreces locals",
"Invalid community ID": "L'ID de la comunitat no és vàlid", "Invalid community ID": "L'ID de la comunitat no és vàlid",
@ -319,7 +256,6 @@
"URL previews are enabled by default for participants in this room.": "Les previsualitzacions dels URL estan habilitades per defecte per als membres d'aquesta sala.", "URL previews are enabled by default for participants in this room.": "Les previsualitzacions dels URL estan habilitades per defecte per als membres d'aquesta sala.",
"URL previews are disabled by default for participants in this room.": "Les previsualitzacions dels URL estan inhabilitades per defecte per als membres d'aquesta sala.", "URL previews are disabled by default for participants in this room.": "Les previsualitzacions dels URL estan inhabilitades per defecte per als membres d'aquesta sala.",
"URL Previews": "Previsualitzacions dels URL", "URL Previews": "Previsualitzacions dels URL",
"Error decrypting audio": "Error desxifrant àudio",
"Error decrypting attachment": "Error desxifrant fitxer adjunt", "Error decrypting attachment": "Error desxifrant fitxer adjunt",
"Decrypt %(text)s": "Desxifra %(text)s", "Decrypt %(text)s": "Desxifra %(text)s",
"Download %(text)s": "Baixa %(text)s", "Download %(text)s": "Baixa %(text)s",
@ -332,8 +268,6 @@
"Copied!": "Copiat!", "Copied!": "Copiat!",
"Failed to copy": "No s'ha pogut copiar", "Failed to copy": "No s'ha pogut copiar",
"Add an Integration": "Afegeix una integració", "Add an Integration": "Afegeix una integració",
"An email has been sent to %(emailAddress)s": "S'ha enviat un correu electrònic a %(emailAddress)s",
"Please check your email to continue registration.": "Reviseu el vostre correu electrònic per a poder continuar amb el registre.",
"Token incorrect": "Token incorrecte", "Token incorrect": "Token incorrecte",
"A text message has been sent to %(msisdn)s": "S'ha enviat un missatge de text a %(msisdn)s", "A text message has been sent to %(msisdn)s": "S'ha enviat un missatge de text a %(msisdn)s",
"Please enter the code it contains:": "Introdueix el codi que conté:", "Please enter the code it contains:": "Introdueix el codi que conté:",
@ -341,7 +275,6 @@
"Sign in with": "Inicieu sessió amb", "Sign in with": "Inicieu sessió amb",
"Email address": "Correu electrònic", "Email address": "Correu electrònic",
"Sign in": "Inicia sessió", "Sign in": "Inicia sessió",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Si no especifiqueu una adreça de correu electrònic, no podreu restablir la vostra contrasenya. N'esteu segur?",
"Remove from community": "Elimina de la comunitat", "Remove from community": "Elimina de la comunitat",
"Disinvite this user from community?": "Voleu retirar la invitació de aquest usuari a la comunitat?", "Disinvite this user from community?": "Voleu retirar la invitació de aquest usuari a la comunitat?",
"Remove this user from community?": "Voleu eliminar de la comunitat a aquest usuari?", "Remove this user from community?": "Voleu eliminar de la comunitat a aquest usuari?",
@ -362,15 +295,12 @@
"Something went wrong when trying to get your communities.": "Alguna cosa ha anat malament mentre s'intentaven obtenir les comunitats.", "Something went wrong when trying to get your communities.": "Alguna cosa ha anat malament mentre s'intentaven obtenir les comunitats.",
"You're not currently a member of any communities.": "Actualment no sou membre de cap comunitat.", "You're not currently a member of any communities.": "Actualment no sou membre de cap comunitat.",
"Unknown Address": "Adreça desconeguda", "Unknown Address": "Adreça desconeguda",
"Allow": "Permetre",
"Delete Widget": "Suprimeix el giny", "Delete Widget": "Suprimeix el giny",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "La supressió d'un giny l'elimina per a tots els usuaris d'aquesta sala. Esteu segur que voleu eliminar aquest giny?", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "La supressió d'un giny l'elimina per a tots els usuaris d'aquesta sala. Esteu segur que voleu eliminar aquest giny?",
"Delete widget": "Suprimeix el giny", "Delete widget": "Suprimeix el giny",
"Minimize apps": "Minimitza les aplicacions",
"No results": "Sense resultats", "No results": "Sense resultats",
"Communities": "Comunitats", "Communities": "Comunitats",
"Home": "Inici", "Home": "Inici",
"Manage Integrations": "Gestiona les integracions",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit",
@ -423,8 +353,6 @@
"expand": "expandeix", "expand": "expandeix",
"Custom level": "Nivell personalitzat", "Custom level": "Nivell personalitzat",
"And %(count)s more...|other": "I %(count)s més...", "And %(count)s more...|other": "I %(count)s més...",
"ex. @bob:example.com": "per exemple @carles:exemple.cat",
"Add User": "Afegeix un usuari",
"Matrix ID": "ID de Matrix", "Matrix ID": "ID de Matrix",
"Matrix Room ID": "ID de la sala de Matrix", "Matrix Room ID": "ID de la sala de Matrix",
"email address": "correu electrònic", "email address": "correu electrònic",
@ -463,22 +391,10 @@
"Unable to verify email address.": "No s'ha pogut verificar el correu electrònic.", "Unable to verify email address.": "No s'ha pogut verificar el correu electrònic.",
"This will allow you to reset your password and receive notifications.": "Això us permetrà restablir la vostra contrasenya i rebre notificacions.", "This will allow you to reset your password and receive notifications.": "Això us permetrà restablir la vostra contrasenya i rebre notificacions.",
"Skip": "Omet", "Skip": "Omet",
"Username not available": "Aquest nom d'usuari no està disponible",
"Username invalid: %(errMessage)s": "El nom d'usuari és invàlid: %(errMessage)s",
"An error occurred: %(error_string)s": "S'ha produït un error: %(error_string)s",
"Username available": "Aquest nom d'usuari està disponible",
"To get started, please pick a username!": "Per començar, seleccioneu un nom d'usuari!",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Aquest serà el nom del seu compte al <span></span> servidor amfitrió, o bé trieu-ne un altre <a>different server</a>.",
"If you already have a Matrix account you can <a>log in</a> instead.": "Si ja teniu un compte a Matrix, podeu <a>log in</a>.",
"If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Si anteriorment heu utilitzat un versió de %(brand)s més recent, la vostra sessió podría ser incompatible amb aquesta versió. Tanqueu aquesta finestra i torneu a la versió més recent.", "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Si anteriorment heu utilitzat un versió de %(brand)s més recent, la vostra sessió podría ser incompatible amb aquesta versió. Tanqueu aquesta finestra i torneu a la versió més recent.",
"Private Chat": "Xat privat",
"Public Chat": "Xat públic",
"Custom": "Personalitzat",
"Name": "Nom", "Name": "Nom",
"You must <a>register</a> to use this functionality": "Per poder utilitzar aquesta funcionalitat has de <a>registrar-te</a>", "You must <a>register</a> to use this functionality": "Per poder utilitzar aquesta funcionalitat has de <a>registrar-te</a>",
"You must join the room to see its files": "Per poder veure els fitxers de la sala t'hi has d'unir", "You must join the room to see its files": "Per poder veure els fitxers de la sala t'hi has d'unir",
"There are no visible files in this room": "No hi ha fitxers visibles en aquesta sala",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>Aquest és l'HTML per a la pàgina de la vostra comunitat</h1>\n<p>\n Utilitzeu la descripció llarga per a presentar la comunitat a nous membres,\n o per afegir-hi <a href=\"foo\">enlaços</a> d'interès. \n</p>\n<p>\n També podeu utilitzar etiquetes 'img'.\n</p>\n",
"Add rooms to the community summary": "Afegiu sales al resum de la comunitat", "Add rooms to the community summary": "Afegiu sales al resum de la comunitat",
"Which rooms would you like to add to this summary?": "Quines sales voleu afegir a aquest resum?", "Which rooms would you like to add to this summary?": "Quines sales voleu afegir a aquest resum?",
"Add to summary": "Afegeix-ho al resum", "Add to summary": "Afegeix-ho al resum",
@ -516,7 +432,6 @@
"Are you sure you want to reject the invitation?": "Esteu segur que voleu rebutjar la invitació?", "Are you sure you want to reject the invitation?": "Esteu segur que voleu rebutjar la invitació?",
"Failed to reject invitation": "No s'ha pogut rebutjar la invitació", "Failed to reject invitation": "No s'ha pogut rebutjar la invitació",
"Are you sure you want to leave the room '%(roomName)s'?": "Esteu segur que voleu sortir de la sala '%(roomName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Esteu segur que voleu sortir de la sala '%(roomName)s'?",
"Failed to leave room": "No s'ha pogut sortir de la sala",
"For security, this session has been signed out. Please sign in again.": "Per seguretat, aquesta sessió s'ha tancat. Torna a iniciar la sessió.", "For security, this session has been signed out. Please sign in again.": "Per seguretat, aquesta sessió s'ha tancat. Torna a iniciar la sessió.",
"Old cryptography data detected": "S'han detectat dades de criptografia antigues", "Old cryptography data detected": "S'han detectat dades de criptografia antigues",
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "S'han detectat dades d'una versió antiga del %(brand)s. Això haurà provocat que el xifratge d'extrem a extrem no funcioni correctament a la versió anterior. Els missatges xifrats d'extrem a extrem que s'han intercanviat recentment mentre s'utilitzava la versió anterior no es poden desxifrar en aquesta versió. També pot provocar que els missatges intercanviats amb aquesta versió fallin. Si teniu problemes, sortiu de la sessió i torneu a entrar-hi. Per poder llegir l'historial dels missatges xifrats, exporteu i torneu a importar les vostres claus.", "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "S'han detectat dades d'una versió antiga del %(brand)s. Això haurà provocat que el xifratge d'extrem a extrem no funcioni correctament a la versió anterior. Els missatges xifrats d'extrem a extrem que s'han intercanviat recentment mentre s'utilitzava la versió anterior no es poden desxifrar en aquesta versió. També pot provocar que els missatges intercanviats amb aquesta versió fallin. Si teniu problemes, sortiu de la sessió i torneu a entrar-hi. Per poder llegir l'historial dels missatges xifrats, exporteu i torneu a importar les vostres claus.",
@ -525,13 +440,9 @@
"Error whilst fetching joined communities": "Error en l'obtenció de comunitats unides", "Error whilst fetching joined communities": "Error en l'obtenció de comunitats unides",
"Create a new community": "Crea una comunitat nova", "Create a new community": "Crea una comunitat nova",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crea una comunitat per agrupar usuaris i sales! Creeu una pàgina d'inici personalitzada per definir el vostre espai a l'univers Matrix.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crea una comunitat per agrupar usuaris i sales! Creeu una pàgina d'inici personalitzada per definir el vostre espai a l'univers Matrix.",
"You have no visible notifications": "No teniu cap notificació visible",
"%(count)s of your messages have not been sent.|other": "Alguns dels vostres missatges no s'han enviat.",
"%(count)s of your messages have not been sent.|one": "El vostre missatge no s'ha enviat.",
"Warning": "Avís", "Warning": "Avís",
"Connectivity to the server has been lost.": "S'ha perdut la connectivitat amb el servidor.", "Connectivity to the server has been lost.": "S'ha perdut la connectivitat amb el servidor.",
"Sent messages will be stored until your connection has returned.": "Els missatges enviats s'emmagatzemaran fins que la vostra connexió hagi tornat.", "Sent messages will be stored until your connection has returned.": "Els missatges enviats s'emmagatzemaran fins que la vostra connexió hagi tornat.",
"Active call": "Trucada activa",
"You seem to be uploading files, are you sure you want to quit?": "Sembla que s'està pujant fitxers, esteu segur que voleu sortir?", "You seem to be uploading files, are you sure you want to quit?": "Sembla que s'està pujant fitxers, esteu segur que voleu sortir?",
"You seem to be in a call, are you sure you want to quit?": "Sembla que està en una trucada, estàs segur que vols sortir?", "You seem to be in a call, are you sure you want to quit?": "Sembla que està en una trucada, estàs segur que vols sortir?",
"Search failed": "No s'ha pogut cercar", "Search failed": "No s'ha pogut cercar",
@ -539,18 +450,10 @@
"No more results": "No hi ha més resultats", "No more results": "No hi ha més resultats",
"Room": "Sala", "Room": "Sala",
"Failed to reject invite": "No s'ha pogut rebutjar la invitació", "Failed to reject invite": "No s'ha pogut rebutjar la invitació",
"Fill screen": "Emplena la pantalla",
"Click to unmute video": "Feu clic per activar el so de vídeo",
"Click to mute video": "Feu clic per desactivar el so de vídeo",
"Click to unmute audio": "Feu clic per activar el so",
"Click to mute audio": "Feu clic per desactivar el so",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "S'ha intentat carregar un punt específic dins la línia de temps d'aquesta sala, però no teniu permís per veure el missatge en qüestió.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "S'ha intentat carregar un punt específic dins la línia de temps d'aquesta sala, però no teniu permís per veure el missatge en qüestió.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "S'ha intentat carregar un punt específic de la línia de temps d'aquesta sala, però no s'ha pogut trobar.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "S'ha intentat carregar un punt específic de la línia de temps d'aquesta sala, però no s'ha pogut trobar.",
"Failed to load timeline position": "No s'ha pogut carregar aquesta posició de la línia de temps", "Failed to load timeline position": "No s'ha pogut carregar aquesta posició de la línia de temps",
"Signed Out": "Sessió tancada", "Signed Out": "Sessió tancada",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Reenviar tot</resendText> o <cancelText>cancel·lar tot</cancelText> ara. També pots seleccionar missatges individualment per reenviar o cancel·lar.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Reenviar missarge</resendText> o <cancelText>cancel·lar missatge</cancelText> ara.",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "No hi ha ningú més aquí! T'agradaria <inviteText>convidar algú</inviteText> o <nowarnText>no avisar més que la sala està buida</nowarnText>?",
"Uploading %(filename)s and %(count)s others|other": "Pujant %(filename)s i %(count)s més", "Uploading %(filename)s and %(count)s others|other": "Pujant %(filename)s i %(count)s més",
"Uploading %(filename)s and %(count)s others|zero": "Pujant %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Pujant %(filename)s",
"Sign out": "Tanca la sessió", "Sign out": "Tanca la sessió",
@ -558,12 +461,9 @@
"Cryptography": "Criptografia", "Cryptography": "Criptografia",
"Labs": "Laboratoris", "Labs": "Laboratoris",
"%(brand)s version:": "Versió de %(brand)s:", "%(brand)s version:": "Versió de %(brand)s:",
"olm version:": "Versió d'olm:",
"Incorrect username and/or password.": "Usuari i/o contrasenya incorrectes.", "Incorrect username and/or password.": "Usuari i/o contrasenya incorrectes.",
"The phone number entered looks invalid": "El número de telèfon introduït sembla erroni",
"Session ID": "ID de la sessió", "Session ID": "ID de la sessió",
"Export room keys": "Exporta les claus de la sala", "Export room keys": "Exporta les claus de la sala",
"Upload an avatar:": "Pujar un avatar:",
"Confirm passphrase": "Introduïu una contrasenya", "Confirm passphrase": "Introduïu una contrasenya",
"Export": "Exporta", "Export": "Exporta",
"Import room keys": "Importa les claus de la sala", "Import room keys": "Importa les claus de la sala",
@ -574,8 +474,6 @@
"Send Reset Email": "Envia email de reinici", "Send Reset Email": "Envia email de reinici",
"Your homeserver's URL": "L'URL del teu servidor propi", "Your homeserver's URL": "L'URL del teu servidor propi",
"Analytics": "Analítiques", "Analytics": "Analítiques",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ha canviat el seu nom visible a %(displayName)s.",
"Identity Server is": "El servidor d'identitat és",
"Submit debug logs": "Enviar logs de depuració", "Submit debug logs": "Enviar logs de depuració",
"The platform you're on": "La plataforma a la que et trobes", "The platform you're on": "La plataforma a la que et trobes",
"Your language of choice": "El teu idioma desitjat", "Your language of choice": "El teu idioma desitjat",
@ -584,42 +482,26 @@
"The information being sent to us to help make %(brand)s better includes:": "La informació que s'envia a %(brand)s per ajudar-nos a millorar inclou:", "The information being sent to us to help make %(brand)s better includes:": "La informació que s'envia a %(brand)s per ajudar-nos a millorar inclou:",
"Fetching third party location failed": "Ha fallat l'obtenció de la ubicació de tercers", "Fetching third party location failed": "Ha fallat l'obtenció de la ubicació de tercers",
"Send Account Data": "Envia les dades del compte", "Send Account Data": "Envia les dades del compte",
"Advanced notification settings": "Configuració avançada de notificacions",
"Uploading report": "S'està enviant l'informe",
"Sunday": "Diumenge", "Sunday": "Diumenge",
"Failed to add tag %(tagName)s to room": "No s'ha pogut afegir l'etiqueta %(tagName)s a la sala", "Failed to add tag %(tagName)s to room": "No s'ha pogut afegir l'etiqueta %(tagName)s a la sala",
"Notification targets": "Objectius de les notificacions", "Notification targets": "Objectius de les notificacions",
"Failed to set direct chat tag": "No s'ha pogut establir l'etiqueta del xat directe", "Failed to set direct chat tag": "No s'ha pogut establir l'etiqueta del xat directe",
"Today": "Avui", "Today": "Avui",
"Files": "Fitxers",
"You are not receiving desktop notifications": "No esteu rebent notificacions d'escriptori",
"Friday": "Divendres", "Friday": "Divendres",
"Update": "Actualització", "Update": "Actualitzar",
"What's New": "Novetats", "What's New": "Novetats",
"On": "Engegat", "On": "Engegat",
"Changelog": "Registre de canvis", "Changelog": "Registre de canvis",
"Waiting for response from server": "S'està esperant una resposta del servidor", "Waiting for response from server": "S'està esperant una resposta del servidor",
"Uploaded on %(date)s by %(user)s": "Pujat el %(date)s per l'usuari %(user)s",
"Send Custom Event": "Envia els esdeveniments personalitzats", "Send Custom Event": "Envia els esdeveniments personalitzats",
"All notifications are currently disabled for all targets.": "Actualment totes les notificacions estan inhabilitades per a tots els objectius.",
"Failed to send logs: ": "No s'han pogut enviar els logs: ", "Failed to send logs: ": "No s'han pogut enviar els logs: ",
"Forget": "Oblida",
"You cannot delete this image. (%(code)s)": "No podeu eliminar aquesta imatge. (%(code)s)",
"Cancel Sending": "Cancel·la l'enviament",
"This Room": "Aquesta sala", "This Room": "Aquesta sala",
"Resend": "Reenvia", "Resend": "Reenvia",
"Room not found": "No s'ha trobat la sala", "Room not found": "No s'ha trobat la sala",
"Messages containing my display name": "Missatges que contenen el meu nom visible", "Messages containing my display name": "Missatges que contenen el meu nom visible",
"Messages in one-to-one chats": "Missatges en xats un a un", "Messages in one-to-one chats": "Missatges en xats un a un",
"Unavailable": "No disponible", "Unavailable": "No disponible",
"Error saving email notification preferences": "Error desant preferències de notificacions de correu electrònic",
"View Decrypted Source": "Mostra el codi desxifrat",
"Failed to update keywords": "No s'han pogut actualitzar les paraules clau",
"remove %(name)s from the directory.": "elimina %(name)s del directori.", "remove %(name)s from the directory.": "elimina %(name)s del directori.",
"Notifications on the following keywords follow rules which cant be displayed here:": "Les notificacions sobre les següents paraules clau segueixen regles que no es poden mostrar aquí:",
"Please set a password!": "Si us plau, establiu una contrasenya",
"You have successfully set a password!": "Heu establert correctament la contrasenya",
"An error occurred whilst saving your email notification preferences.": "S'ha produït un error mentre es desaven les teves preferències de notificació de correu electrònic.",
"Explore Room State": "Esbrina els estats de les sales", "Explore Room State": "Esbrina els estats de les sales",
"Source URL": "URL origen", "Source URL": "URL origen",
"Messages sent by bot": "Missatges enviats pel bot", "Messages sent by bot": "Missatges enviats pel bot",
@ -628,33 +510,22 @@
"No update available.": "No hi ha cap actualització disponible.", "No update available.": "No hi ha cap actualització disponible.",
"Noisy": "Sorollós", "Noisy": "Sorollós",
"Collecting app version information": "S'està recollint la informació de la versió de l'aplicació", "Collecting app version information": "S'està recollint la informació de la versió de l'aplicació",
"Enable notifications for this account": "Habilita les notificacions per aquest compte",
"Invite to this community": "Convida a aquesta comunitat", "Invite to this community": "Convida a aquesta comunitat",
"Search…": "Cerca…", "Search…": "Cerca…",
"Messages containing <span>keywords</span>": "Missatges que contenen <span>keywords</span>",
"When I'm invited to a room": "Quan sóc convidat a una sala", "When I'm invited to a room": "Quan sóc convidat a una sala",
"Tuesday": "Dimarts", "Tuesday": "Dimarts",
"Enter keywords separated by a comma:": "Introduïu les paraules clau separades per una coma:",
"Forward Message": "Reenvia el missatge",
"Remove %(name)s from the directory?": "Voleu retirar %(name)s del directori?", "Remove %(name)s from the directory?": "Voleu retirar %(name)s del directori?",
"%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s utilitza moltes funcions avançades del navegador, algunes de les quals no estan disponibles o són experimentals al vostre navegador actual.",
"Developer Tools": "Eines de desenvolupador", "Developer Tools": "Eines de desenvolupador",
"Preparing to send logs": "Preparant l'enviament de logs", "Preparing to send logs": "Preparant l'enviament de logs",
"Explore Account Data": "Explora les dades del compte", "Explore Account Data": "Explora les dades del compte",
"Remove from Directory": "Elimina del directori", "Remove from Directory": "Elimina del directori",
"Saturday": "Dissabte", "Saturday": "Dissabte",
"Remember, you can always set an email address in user settings if you change your mind.": "Recordeu-ho, si canvieu d'idea, sempre podreu establir una adreça de correu electrònic a las vostra configuració d'usuari.",
"Direct Chat": "Xat directe",
"The server may be unavailable or overloaded": "El servidor pot no estar disponible o sobrecarregat", "The server may be unavailable or overloaded": "El servidor pot no estar disponible o sobrecarregat",
"Reject": "Rebutja", "Reject": "Rebutja",
"Failed to set Direct Message status of room": "No s'ha pogut establir l'estat del missatge directe de la sala",
"Monday": "Dilluns", "Monday": "Dilluns",
"All messages (noisy)": "Tots els missatges (sorollós)",
"Enable them now": "Habilita-ho ara",
"Toolbox": "Caixa d'eines", "Toolbox": "Caixa d'eines",
"Collecting logs": "S'estan recopilant els registres", "Collecting logs": "S'estan recopilant els registres",
"You must specify an event type!": "Has d'especificar un tipus d'esdeveniment!", "You must specify an event type!": "Has d'especificar un tipus d'esdeveniment!",
"(HTTP status %(httpStatus)s)": "(Estat de l´HTTP %(httpStatus)s)",
"All Rooms": "Totes les sales", "All Rooms": "Totes les sales",
"State Key": "Clau d'estat", "State Key": "Clau d'estat",
"Wednesday": "Dimecres", "Wednesday": "Dimecres",
@ -662,50 +533,32 @@
"All messages": "Tots els missatges", "All messages": "Tots els missatges",
"Call invitation": "Invitació de trucada", "Call invitation": "Invitació de trucada",
"Downloading update...": "Descarregant l'actualització...", "Downloading update...": "Descarregant l'actualització...",
"You have successfully set a password and an email address!": "Heu establert correctament la vostra contrasenya i l'adreça de correu electrònic",
"Failed to send custom event.": "No s'ha pogut enviar l'esdeveniment personalitzat.", "Failed to send custom event.": "No s'ha pogut enviar l'esdeveniment personalitzat.",
"What's new?": "Què hi ha de nou?", "What's new?": "Què hi ha de nou?",
"Notify me for anything else": "Notifica'm per a qualsevol altra cosa",
"View Source": "Mostra el codi", "View Source": "Mostra el codi",
"Keywords": "Paraules clau",
"Can't update user notification settings": "No es pot actualitzar la configuració de notificacions d'usuari",
"Notify for all other messages/rooms": "Notifica per a tots els altres missatges o sales",
"Unable to look up room ID from server": "No s'ha pogut cercar l'ID de la sala en el servidor", "Unable to look up room ID from server": "No s'ha pogut cercar l'ID de la sala en el servidor",
"Couldn't find a matching Matrix room": "No s'ha pogut trobar una sala de Matrix que coincideixi", "Couldn't find a matching Matrix room": "No s'ha pogut trobar una sala de Matrix que coincideixi",
"Invite to this room": "Convida a aquesta sala", "Invite to this room": "Convida a aquesta sala",
"You cannot delete this message. (%(code)s)": "No podeu eliminar aquest missatge. (%(code)s)", "You cannot delete this message. (%(code)s)": "No podeu eliminar aquest missatge. (%(code)s)",
"Thursday": "Dijous", "Thursday": "Dijous",
"I understand the risks and wish to continue": "Entenc el riscos i desitjo continuar",
"Logs sent": "Logs enviats", "Logs sent": "Logs enviats",
"Back": "Enrere", "Back": "Enrere",
"Reply": "Respon", "Reply": "Respon",
"Show message in desktop notification": "Mostra els missatges amb notificacions d'escriptori", "Show message in desktop notification": "Mostra els missatges amb notificacions d'escriptori",
"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.": "Els logs de depuració contenen dades d'ús de l'aplicació que inclouen el teu nom d'usuari, les IDs o pseudònims de les sales o grups que has visitat i els noms d'usuari d'altres usuaris. No contenen missatges.",
"Unhide Preview": "Mostra la previsualització",
"Unable to join network": "No s'ha pogut unir-se a la xarxa", "Unable to join network": "No s'ha pogut unir-se a la xarxa",
"Sorry, your browser is <b>not</b> able to run %(brand)s.": "Disculpeu, el seu navegador <b>not</b> pot executar %(brand)s.",
"Quote": "Cita", "Quote": "Cita",
"Messages in group chats": "Missatges en xats de grup", "Messages in group chats": "Missatges en xats de grup",
"Yesterday": "Ahir", "Yesterday": "Ahir",
"Error encountered (%(errorDetail)s).": "S'ha trobat un error (%(errorDetail)s).", "Error encountered (%(errorDetail)s).": "S'ha trobat un error (%(errorDetail)s).",
"Low Priority": "Baixa prioritat", "Low Priority": "Baixa prioritat",
"Unable to fetch notification target list": "No s'ha pogut obtenir la llista d'objectius de les notificacions",
"Set Password": "Establiu una contrasenya",
"Off": "Apagat", "Off": "Apagat",
"%(brand)s does not know how to join a room on this network": "El %(brand)s no sap com unir-se a una sala en aquesta xarxa", "%(brand)s does not know how to join a room on this network": "El %(brand)s no sap com unir-se a una sala en aquesta xarxa",
"Mentions only": "Només mencions",
"Failed to remove tag %(tagName)s from room": "No s'ha pogut esborrar l'etiqueta %(tagName)s de la sala", "Failed to remove tag %(tagName)s from room": "No s'ha pogut esborrar l'etiqueta %(tagName)s de la sala",
"You can now return to your account after signing out, and sign in on other devices.": "Ara podreu tornar a entrar al vostre compte des de altres dispositius.",
"Enable email notifications": "Habilita les notificacions per correu electrònic",
"Event Type": "Tipus d'esdeveniment", "Event Type": "Tipus d'esdeveniment",
"Download this file": "Descarrega aquest fitxer",
"Pin Message": "Enganxa el missatge",
"Failed to change settings": "No s'ha pogut canviar la configuració",
"View Community": "Mira la communitat", "View Community": "Mira la communitat",
"Event sent!": "Esdeveniment enviat!", "Event sent!": "Esdeveniment enviat!",
"Event Content": "Contingut de l'esdeveniment", "Event Content": "Contingut de l'esdeveniment",
"Thank you!": "Gràcies!", "Thank you!": "Gràcies!",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Amb el vostre navegador actual, l'aparença de l'aplicació pot ser completament incorrecta i algunes o totes les funcions poden no funcionar correctament. Si voleu provar-ho de totes maneres, podeu continuar, però esteu sols pel que fa als problemes que pugueu trobar!",
"Checking for an update...": "Comprovant si hi ha actualitzacions...", "Checking for an update...": "Comprovant si hi ha actualitzacions...",
"e.g. %(exampleValue)s": "p.e. %(exampleValue)s", "e.g. %(exampleValue)s": "p.e. %(exampleValue)s",
"Every page you use in the app": "Cada pàgina que utilitzes a l'aplicació", "Every page you use in the app": "Cada pàgina que utilitzes a l'aplicació",
@ -713,15 +566,11 @@
"Your device resolution": "La resolució del teu dispositiu", "Your device resolution": "La resolució del teu dispositiu",
"Show Stickers": "Mostra els adhesius", "Show Stickers": "Mostra els adhesius",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quan aquesta pàgina contingui informació d'identificació, com per exemple una sala, usuari o ID de grup, aquestes dades s'eliminen abans d'enviar-se al servidor.", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quan aquesta pàgina contingui informació d'identificació, com per exemple una sala, usuari o ID de grup, aquestes dades s'eliminen abans d'enviar-se al servidor.",
"Call in Progress": "Trucada en curs",
"A call is currently being placed!": "En aquest moment s'està realitzant una trucada!",
"A call is already in progress!": "Ja hi ha una trucada en curs!",
"Permission Required": "Es necessita permís", "Permission Required": "Es necessita permís",
"You do not have permission to start a conference call in this room": "No tens permís per iniciar una conferència telefònica en aquesta sala", "You do not have permission to start a conference call in this room": "No tens permís per iniciar una conferència telefònica en aquesta sala",
"Unable to load! Check your network connectivity and try again.": "No s'ha pogut carregar! Comprova la connectivitat de xarxa i torna-ho a intentar.", "Unable to load! Check your network connectivity and try again.": "No s'ha pogut carregar! Comprova la connectivitat de xarxa i torna-ho a intentar.",
"Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:", "Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:",
"Missing roomId.": "Falta l'ID de sala.", "Missing roomId.": "Falta l'ID de sala.",
"Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats",
"Changes your display nickname": "Canvia l'àlies a mostrar", "Changes your display nickname": "Canvia l'àlies a mostrar",
"Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat", "Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat",
"Kicks user with given id": "Expulsa l'usuari amb l'ID indicat", "Kicks user with given id": "Expulsa l'usuari amb l'ID indicat",
@ -799,8 +648,6 @@
"Show avatar changes": "Mostra els canvis d'avatar", "Show avatar changes": "Mostra els canvis d'avatar",
"Show display name changes": "Mostra els canvis de nom", "Show display name changes": "Mostra els canvis de nom",
"Show read receipts sent by other users": "Mostra les confirmacions de lectura enviades pels altres usuaris", "Show read receipts sent by other users": "Mostra les confirmacions de lectura enviades pels altres usuaris",
"Always show encryption icons": "Mostra sempre les icones que indiquen encriptació",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Mostra un recordatori per activar la Recuperació de missatges segurs en sales encriptades",
"Show avatars in user and room mentions": "Mostra avatars en mencions d'usuaris i sales", "Show avatars in user and room mentions": "Mostra avatars en mencions d'usuaris i sales",
"Enable big emoji in chat": "Activa Emojis grans en xats", "Enable big emoji in chat": "Activa Emojis grans en xats",
"Send analytics data": "Envia dades d'anàlisi", "Send analytics data": "Envia dades d'anàlisi",
@ -810,14 +657,12 @@
"Language and region": "Idioma i regió", "Language and region": "Idioma i regió",
"Theme": "Tema", "Theme": "Tema",
"Phone Number": "Número de telèfon", "Phone Number": "Número de telèfon",
"Help": "Ajuda",
"Send typing notifications": "Envia notificacions d'escriptura", "Send typing notifications": "Envia notificacions d'escriptura",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Vols suprimir l'adreça de la sala %(alias)s i eliminar %(name)s del directori?", "Delete the room address %(alias)s and remove %(name)s from the directory?": "Vols suprimir l'adreça de la sala %(alias)s i eliminar %(name)s del directori?",
"We encountered an error trying to restore your previous session.": "Hem trobat un error en intentar recuperar la teva sessió prèvia.", "We encountered an error trying to restore your previous session.": "Hem trobat un error en intentar recuperar la teva sessió prèvia.",
"There was an error updating your community. The server is unable to process your request.": "S'ha produït un error en actualitzar la comunitat. El servidor no ha pogut processar la petició.", "There was an error updating your community. The server is unable to process your request.": "S'ha produït un error en actualitzar la comunitat. El servidor no ha pogut processar la petició.",
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "S'ha produït un error en crear la comunitat. Potser el nom ja existeix o el servidor no ha pogut processar la petició.", "There was an error creating your community. The name may be taken or the server is unable to process your request.": "S'ha produït un error en crear la comunitat. Potser el nom ja existeix o el servidor no ha pogut processar la petició.",
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "S'ha retornat un error (%(errcode)s) mentre s'intentava validar la invitació. Pots provar a informar d'això a un administrador de sala.", "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "S'ha retornat un error (%(errcode)s) mentre s'intentava validar la invitació. Pots provar a informar d'això a un administrador de sala.",
"Error: Problem communicating with the given homeserver.": "Error: problema comunicant-se amb el servidor local proporcionat.",
"Upload Error": "Error de pujada", "Upload Error": "Error de pujada",
"A connection error occurred while trying to contact the server.": "S'ha produït un error de connexió mentre s'intentava connectar al servidor.", "A connection error occurred while trying to contact the server.": "S'ha produït un error de connexió mentre s'intentava connectar al servidor.",
"%(brand)s encountered an error during upload of:": "%(brand)s ha trobat un error durant la pujada de:", "%(brand)s encountered an error during upload of:": "%(brand)s ha trobat un error durant la pujada de:",
@ -841,8 +686,6 @@
"Error leaving room": "Error sortint de la sala", "Error leaving room": "Error sortint de la sala",
"Unexpected error resolving identity server configuration": "Error inesperat resolent la configuració del servidor d'identitat", "Unexpected error resolving identity server configuration": "Error inesperat resolent la configuració del servidor d'identitat",
"Unexpected error resolving homeserver configuration": "Error inesperat resolent la configuració del servidor local", "Unexpected error resolving homeserver configuration": "Error inesperat resolent la configuració del servidor local",
"(an error occurred)": "(s'ha produït un error)",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Els gestors d'integracions reben dades de configuració i poden modificar ginys, enviar invitacions a sales i establir nivells d'autoritat en nom teu.",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar els requisits del nivell d'autoritat de la sala. Assegura't que tens suficients permisos i torna-ho a provar.", "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar els requisits del nivell d'autoritat de la sala. Assegura't que tens suficients permisos i torna-ho a provar.",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar el nivell d'autoritat de l'usuari. Assegura't que tens suficients permisos i torna-ho a provar.", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar el nivell d'autoritat de l'usuari. Assegura't que tens suficients permisos i torna-ho a provar.",
"Power level": "Nivell d'autoritat", "Power level": "Nivell d'autoritat",
@ -918,9 +761,7 @@
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, utilitza un servidor d'identitat a Configuració.", "Use an identity server in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, utilitza un servidor d'identitat a Configuració.",
"Share this email in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, comparteix aquest correu electrònic a Configuració.", "Share this email in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, comparteix aquest correu electrònic a Configuració.",
"Enable 'Manage Integrations' in Settings to do this.": "Per fer això, activa 'Gestió d'integracions' a Configuració.", "Enable 'Manage Integrations' in Settings to do this.": "Per fer això, activa 'Gestió d'integracions' a Configuració.",
"We recommend you change your password and recovery key in Settings immediately": "Et recomanem que canviïs immediatament la teva contrasenya i clau de recuperació a Configuració",
"Go to Settings": "Ves a Configuració", "Go to Settings": "Ves a Configuració",
"No identity server is configured: add one in server settings to reset your password.": "No hi ha cap servidor d'identitat configurat: afegeix-ne un a la configuració del servidor per poder restablir la teva contrasenya.",
"User settings": "Configuració d'usuari", "User settings": "Configuració d'usuari",
"All settings": "Totes les configuracions", "All settings": "Totes les configuracions",
"This will end the conference for everyone. Continue?": "Això finalitzarà la conferència per a tothom. Vols continuar?", "This will end the conference for everyone. Continue?": "Això finalitzarà la conferència per a tothom. Vols continuar?",
@ -931,8 +772,6 @@
"Call failed due to misconfigured server": "La trucada ha fallat a causa d'una configuració errònia al servidor", "Call failed due to misconfigured server": "La trucada ha fallat a causa d'una configuració errònia al servidor",
"The call was answered on another device.": "La trucada s'ha respost des d'un altre dispositiu.", "The call was answered on another device.": "La trucada s'ha respost des d'un altre dispositiu.",
"The call could not be established": "No s'ha pogut establir la trucada", "The call could not be established": "No s'ha pogut establir la trucada",
"The other party declined the call.": "L'altra part ha rebutjat la trucada.",
"Call Declined": "Trucada rebutjada",
"Add Phone Number": "Afegeix número de telèfon", "Add Phone Number": "Afegeix número de telèfon",
"Confirm adding email": "Confirma l'addició del correu electrònic", "Confirm adding email": "Confirma l'addició del correu electrònic",
"To continue, use Single Sign On to prove your identity.": "Per continuar, utilitza la inscripció única SSO (per demostrar la teva identitat).", "To continue, use Single Sign On to prove your identity.": "Per continuar, utilitza la inscripció única SSO (per demostrar la teva identitat).",

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{ {
"Filter room members": "Filter medlemmer", "Filter room members": "Filter medlemmer",
"You have no visible notifications": "Du har ingen synlige meddelelser",
"Invites": "Invitationer", "Invites": "Invitationer",
"Favourites": "Favoritter", "Favourites": "Favoritter",
"Rooms": "Rum", "Rooms": "Rum",
@ -17,7 +16,6 @@
"Invites user with given id to current room": "Inviterer bruger med givet id til nuværende rum", "Invites user with given id to current room": "Inviterer bruger med givet id til nuværende rum",
"Kicks user with given id": "Smider bruger med givet id ud", "Kicks user with given id": "Smider bruger med givet id ud",
"Changes your display nickname": "Ændrer dit viste navn", "Changes your display nickname": "Ændrer dit viste navn",
"Searches DuckDuckGo for results": "Søger på DuckDuckGo efter resultater",
"Commands": "Kommandoer", "Commands": "Kommandoer",
"Emoji": "Emoji", "Emoji": "Emoji",
"Sign in": "Log ind", "Sign in": "Log ind",
@ -25,11 +23,8 @@
"Account": "Konto", "Account": "Konto",
"Admin": "Administrator", "Admin": "Administrator",
"Advanced": "Avanceret", "Advanced": "Avanceret",
"Anyone who knows the room's link, apart from guests": "Alle der kender link til rummet, bortset fra gæster",
"Anyone who knows the room's link, including guests": "Alle der kender link til rummet, inklusiv gæster",
"Are you sure you want to reject the invitation?": "Er du sikker på du vil afvise invitationen?", "Are you sure you want to reject the invitation?": "Er du sikker på du vil afvise invitationen?",
"Banned users": "Bortviste brugere", "Banned users": "Bortviste brugere",
"Click here to fix": "Klik her for at rette",
"Continue": "Fortsæt", "Continue": "Fortsæt",
"Create Room": "Opret rum", "Create Room": "Opret rum",
"Cryptography": "Kryptografi", "Cryptography": "Kryptografi",
@ -38,7 +33,6 @@
"Error": "Fejl", "Error": "Fejl",
"Export E2E room keys": "Eksporter E2E rum nøgler", "Export E2E room keys": "Eksporter E2E rum nøgler",
"Failed to change password. Is your password correct?": "Kunne ikke ændre password. Er dit password korrekt?", "Failed to change password. Is your password correct?": "Kunne ikke ændre password. Er dit password korrekt?",
"Failed to leave room": "Kunne ikke forlade rum",
"Failed to reject invitation": "Kunne ikke afvise invitationen", "Failed to reject invitation": "Kunne ikke afvise invitationen",
"Failed to send email": "Kunne ikke sende e-mail", "Failed to send email": "Kunne ikke sende e-mail",
"Failed to unban": "Var ikke i stand til at ophæve forbuddet", "Failed to unban": "Var ikke i stand til at ophæve forbuddet",
@ -47,19 +41,13 @@
"Remove": "Fjern", "Remove": "Fjern",
"Settings": "Indstillinger", "Settings": "Indstillinger",
"unknown error code": "Ukendt fejlkode", "unknown error code": "Ukendt fejlkode",
"%(targetName)s accepted an invitation.": "%(targetName)s accepterede en invitation.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepterede invitationen til %(displayName)s.",
"%(senderName)s answered the call.": "%(senderName)s besvarede opkaldet.",
"Add a widget": "Tilføj en widget",
"OK": "OK", "OK": "OK",
"Search": "Søg", "Search": "Søg",
"Custom Server Options": "Brugerdefinerede serverindstillinger",
"Dismiss": "Afslut", "Dismiss": "Afslut",
"powered by Matrix": "Drevet af Matrix", "powered by Matrix": "Drevet af Matrix",
"Close": "Luk", "Close": "Luk",
"Cancel": "Afbryd", "Cancel": "Afbryd",
"Edit": "Rediger", "Edit": "Rediger",
"Unpin Message": "Frigør Besked",
"Failed to forget room %(errCode)s": "Kunne ikke glemme rummet %(errCode)s", "Failed to forget room %(errCode)s": "Kunne ikke glemme rummet %(errCode)s",
"Mute": "Sæt på lydløs", "Mute": "Sæt på lydløs",
"Leave": "Forlad", "Leave": "Forlad",
@ -72,11 +60,6 @@
"This email address is already in use": "Denne email adresse er allerede i brug", "This email address is already in use": "Denne email adresse er allerede i brug",
"This phone number is already in use": "Dette telefonnummer er allerede i brug", "This phone number is already in use": "Dette telefonnummer er allerede i brug",
"Failed to verify email address: make sure you clicked the link in the email": "Kunne ikke bekræfte emailaddressen: vær sikker på at klikke på linket i e-mailen", "Failed to verify email address: make sure you clicked the link in the email": "Kunne ikke bekræfte emailaddressen: vær sikker på at klikke på linket i e-mailen",
"Call Timeout": "Opkalds Timeout",
"The remote side failed to pick up": "Den anden side tog den ikke",
"Unable to capture screen": "Kunne ikke optage skærm",
"Existing Call": "Eksisterende Opkald",
"You are already in a call.": "Du er allerede i et opkald.",
"VoIP is unsupported": "VoIP er ikke understøttet", "VoIP is unsupported": "VoIP er ikke understøttet",
"You cannot place VoIP calls in this browser.": "Du kan ikke lave VoIP-opkald i denne browser.", "You cannot place VoIP calls in this browser.": "Du kan ikke lave VoIP-opkald i denne browser.",
"You cannot place a call with yourself.": "Du kan ikke ringe til dig selv.", "You cannot place a call with yourself.": "Du kan ikke ringe til dig selv.",
@ -126,7 +109,6 @@
"Moderator": "Moderator", "Moderator": "Moderator",
"Operation failed": "Operation mislykkedes", "Operation failed": "Operation mislykkedes",
"Failed to invite": "Kunne ikke invitere", "Failed to invite": "Kunne ikke invitere",
"Failed to invite the following users to the %(roomName)s room:": "Kunne ikke invitere de følgende brugere til %(roomName)s rummet:",
"You need to be logged in.": "Du skal være logget ind.", "You need to be logged in.": "Du skal være logget ind.",
"You need to be able to invite users to do that.": "Du skal kunne invitere brugere for at gøre dette.", "You need to be able to invite users to do that.": "Du skal kunne invitere brugere for at gøre dette.",
"Unable to create widget.": "Kunne ikke lave widget.", "Unable to create widget.": "Kunne ikke lave widget.",
@ -139,81 +121,42 @@
"Room %(roomId)s not visible": "rum %(roomId)s ikke synligt", "Room %(roomId)s not visible": "rum %(roomId)s ikke synligt",
"Missing user_id in request": "Manglende user_id i forespørgsel", "Missing user_id in request": "Manglende user_id i forespørgsel",
"Usage": "Brug", "Usage": "Brug",
"/ddg is not a command": "/ddg er ikke en kommando",
"To use it, just wait for autocomplete results to load and tab through them.": "For at bruge det skal du bare vente på at autocomplete resultaterne indlæses, og så bruge Tab for at bladre igennem dem.",
"Ignored user": "Ignoreret bruger", "Ignored user": "Ignoreret bruger",
"You are now ignoring %(userId)s": "Du ignorerer nu %(userId)s", "You are now ignoring %(userId)s": "Du ignorerer nu %(userId)s",
"Unignored user": "Holdt op med at ignorere bruger", "Unignored user": "Holdt op med at ignorere bruger",
"You are no longer ignoring %(userId)s": "Du ignorerer ikke længere %(userId)s", "You are no longer ignoring %(userId)s": "Du ignorerer ikke længere %(userId)s",
"Verified key": "Verificeret nøgle", "Verified key": "Verificeret nøgle",
"Reason": "Årsag", "Reason": "Årsag",
"%(senderName)s requested a VoIP conference.": "%(senderName)s forespurgte en VoIP konference.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s inviterede %(targetName)s.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s bannede %(targetName)s.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s satte deres viste navn til %(displayName)s.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s fjernede deres viste navn (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s fjernede deres profilbillede.",
"%(senderName)s changed their profile picture.": "%(senderName)s ændrede deres profilbillede.",
"%(senderName)s set a profile picture.": "%(senderName)s indstillede deres profilbillede.",
"VoIP conference started.": "VoIP konference startet.",
"%(targetName)s joined the room.": "%(targetName)s forbandt til rummet.",
"VoIP conference finished.": "VoIP konference afsluttet.",
"%(targetName)s rejected the invitation.": "%(targetName)s afviste invitationen.",
"%(targetName)s left the room.": "%(targetName)s forlod rummet.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbannede %(targetName)s.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s kickede %(targetName)s.",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s trak %(targetName)ss invitation tilbage.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ændrede emnet til \"%(topic)s\".", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ændrede emnet til \"%(topic)s\".",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s fjernede rumnavnet.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s fjernede rumnavnet.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ændrede rumnavnet til %(roomName)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ændrede rumnavnet til %(roomName)s.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sendte et billed.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sendte et billed.",
"Someone": "Nogen", "Someone": "Nogen",
"(not supported by this browser)": "(Ikke understøttet af denne browser)",
"(could not connect media)": "(kunne ikke forbinde til mediet)",
"(no answer)": "(intet svar)",
"(unknown failure: %(reason)s)": "(ukendt fejl: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s afsluttede opkaldet.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s inviterede %(targetDisplayName)s til rummet.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s inviterede %(targetDisplayName)s til rummet.",
"Submit debug logs": "Indsend debug-logfiler", "Submit debug logs": "Indsend debug-logfiler",
"Online": "Online", "Online": "Online",
"Fetching third party location failed": "Hentning af tredjeparts placering mislykkedes", "Fetching third party location failed": "Hentning af tredjeparts placering mislykkedes",
"Send Account Data": "Send Konto Data", "Send Account Data": "Send Konto Data",
"All notifications are currently disabled for all targets.": "Alle meddelelser er for øjeblikket deaktiveret for alle mål.",
"Uploading report": "Uploader rapport",
"Sunday": "Søndag", "Sunday": "Søndag",
"Messages sent by bot": "Beskeder sendt af en bot", "Messages sent by bot": "Beskeder sendt af en bot",
"Notification targets": "Meddelelsesmål", "Notification targets": "Meddelelsesmål",
"Failed to set direct chat tag": "Kunne ikke markere rummet som direkte chat", "Failed to set direct chat tag": "Kunne ikke markere rummet som direkte chat",
"Today": "I dag", "Today": "I dag",
"Files": "Filer",
"You are not receiving desktop notifications": "Du modtager ikke skrivebordsmeddelelser",
"Friday": "Fredag", "Friday": "Fredag",
"Update": "Opdater", "Update": "Opdater",
"What's New": "Hvad er nyt", "What's New": "Hvad er nyt",
"On": "Tændt", "On": "Tændt",
"Changelog": "Ændringslog", "Changelog": "Ændringslog",
"Waiting for response from server": "Venter på svar fra server", "Waiting for response from server": "Venter på svar fra server",
"Uploaded on %(date)s by %(user)s": "Uploadet den %(date)s af %(user)s",
"Send Custom Event": "Send Brugerdefineret Begivenhed", "Send Custom Event": "Send Brugerdefineret Begivenhed",
"Off": "Slukket", "Off": "Slukket",
"Advanced notification settings": "Avancerede notifikationsindstillinger",
"Forget": "Glem",
"You cannot delete this image. (%(code)s)": "Du kan ikke slette dette billede. (%(code)s)",
"Cancel Sending": "Stop Forsendelse",
"Warning": "Advarsel", "Warning": "Advarsel",
"This Room": "Dette rum", "This Room": "Dette rum",
"Room not found": "Rummet ikke fundet", "Room not found": "Rummet ikke fundet",
"Messages containing my display name": "Beskeder der indeholder mit viste navn", "Messages containing my display name": "Beskeder der indeholder mit viste navn",
"Messages in one-to-one chats": "Beskeder i en-til-en chats", "Messages in one-to-one chats": "Beskeder i en-til-en chats",
"Unavailable": "Utilgængelig", "Unavailable": "Utilgængelig",
"Error saving email notification preferences": "Fejl ved at gemme e-mail-underretningsindstillinger",
"View Decrypted Source": "Se Dekrypteret Kilde",
"Failed to update keywords": "Kunne ikke opdatere søgeord",
"remove %(name)s from the directory.": "fjern %(name)s fra kataloget.", "remove %(name)s from the directory.": "fjern %(name)s fra kataloget.",
"Notifications on the following keywords follow rules which cant be displayed here:": "Meddelelser om følgende søgeord følger regler, der ikke kan vises her:",
"Please set a password!": "Indstil venligst et password!",
"You have successfully set a password!": "Du har succesfuldt indstillet et password!",
"An error occurred whilst saving your email notification preferences.": "Der opstod en fejl under opbevaring af dine e-mail-underretningsindstillinger.",
"Explore Room State": "Udforsk Rum Tilstand", "Explore Room State": "Udforsk Rum Tilstand",
"Source URL": "Kilde URL", "Source URL": "Kilde URL",
"Failed to add tag %(tagName)s to room": "Kunne ikke tilføje tag(s): %(tagName)s til rummet", "Failed to add tag %(tagName)s to room": "Kunne ikke tilføje tag(s): %(tagName)s til rummet",
@ -222,32 +165,21 @@
"No update available.": "Ingen opdatering tilgængelig.", "No update available.": "Ingen opdatering tilgængelig.",
"Noisy": "Støjende", "Noisy": "Støjende",
"Collecting app version information": "Indsamler app versionsoplysninger", "Collecting app version information": "Indsamler app versionsoplysninger",
"Keywords": "Søgeord",
"Enable notifications for this account": "Aktivér underretninger for dette brugernavn",
"Invite to this community": "Inviter til dette fællesskab", "Invite to this community": "Inviter til dette fællesskab",
"Search…": "Søg…", "Search…": "Søg…",
"Messages containing <span>keywords</span>": "Beskeder der indeholder <span>keywords</span>",
"When I'm invited to a room": "Når jeg bliver inviteret til et rum", "When I'm invited to a room": "Når jeg bliver inviteret til et rum",
"Tuesday": "Tirsdag", "Tuesday": "Tirsdag",
"Enter keywords separated by a comma:": "Indtast søgeord adskilt af et komma:",
"Forward Message": "Videresend Besked",
"Remove %(name)s from the directory?": "Fjern %(name)s fra kataloget?", "Remove %(name)s from the directory?": "Fjern %(name)s fra kataloget?",
"%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s bruger mange avancerede browser funktioner, hvoraf nogle af dem ikke er tilgængelige eller er eksperimentelle i din browser.",
"Event sent!": "Begivenhed sendt!", "Event sent!": "Begivenhed sendt!",
"Explore Account Data": "Udforsk Konto Data", "Explore Account Data": "Udforsk Konto Data",
"Saturday": "Lørdag", "Saturday": "Lørdag",
"Remember, you can always set an email address in user settings if you change your mind.": "Husk, du kan altid indstille en emailadresse i dine bruger indstillinger hvis du ombestemmer dig.",
"Direct Chat": "Personlig Chat",
"The server may be unavailable or overloaded": "Serveren kan være utilgængelig eller overbelastet", "The server may be unavailable or overloaded": "Serveren kan være utilgængelig eller overbelastet",
"Reject": "Afvis", "Reject": "Afvis",
"Failed to set Direct Message status of room": "Kunne ikke indstille Direkte Beskedstatus for rummet",
"Monday": "Mandag", "Monday": "Mandag",
"Remove from Directory": "Fjern fra Katalog", "Remove from Directory": "Fjern fra Katalog",
"Enable them now": "Aktivér dem nu",
"Toolbox": "Værktøjer", "Toolbox": "Værktøjer",
"Collecting logs": "Indsamler logfiler", "Collecting logs": "Indsamler logfiler",
"You must specify an event type!": "Du skal angive en begivenhedstype!", "You must specify an event type!": "Du skal angive en begivenhedstype!",
"(HTTP status %(httpStatus)s)": "(HTTP tilstand %(httpStatus)s)",
"Invite to this room": "Inviter til dette rum", "Invite to this room": "Inviter til dette rum",
"State Key": "Tilstandsnøgle", "State Key": "Tilstandsnøgle",
"Send": "Send", "Send": "Send",
@ -255,51 +187,33 @@
"All messages": "Alle beskeder", "All messages": "Alle beskeder",
"Call invitation": "Opkalds invitation", "Call invitation": "Opkalds invitation",
"Downloading update...": "Downloader opdatering...", "Downloading update...": "Downloader opdatering...",
"You have successfully set a password and an email address!": "Du har succesfuldt indstillet et password og en emailadresse!",
"Failed to send custom event.": "Kunne ikke sende brugerdefinerede begivenhed.", "Failed to send custom event.": "Kunne ikke sende brugerdefinerede begivenhed.",
"What's new?": "Hvad er nyt?", "What's new?": "Hvad er nyt?",
"Notify me for anything else": "Underret mig om noget andet",
"View Source": "Se Kilde", "View Source": "Se Kilde",
"Can't update user notification settings": "Kan ikke opdatere brugermeddelelsesindstillinger",
"Notify for all other messages/rooms": "Underret om alle andre meddelelser / rum",
"Unable to look up room ID from server": "Kunne ikke slå rum-id op på server", "Unable to look up room ID from server": "Kunne ikke slå rum-id op på server",
"Couldn't find a matching Matrix room": "Kunne ikke finde et matchende Matrix-rum", "Couldn't find a matching Matrix room": "Kunne ikke finde et matchende Matrix-rum",
"All Rooms": "Alle rum", "All Rooms": "Alle rum",
"You cannot delete this message. (%(code)s)": "Du kan ikke slette denne besked. (%(code)s)", "You cannot delete this message. (%(code)s)": "Du kan ikke slette denne besked. (%(code)s)",
"Thursday": "Torsdag", "Thursday": "Torsdag",
"I understand the risks and wish to continue": "Jeg forstår risikoen og ønsker at fortsætte",
"Back": "Tilbage", "Back": "Tilbage",
"Show message in desktop notification": "Vis besked i skrivebordsnotifikation", "Show message in desktop notification": "Vis besked i skrivebordsnotifikation",
"Unhide Preview": "Vis Forhåndsvisning",
"Unable to join network": "Kan ikke forbinde til netværket", "Unable to join network": "Kan ikke forbinde til netværket",
"Sorry, your browser is <b>not</b> able to run %(brand)s.": "Beklager, din browser kan <b>ikke</b> køre %(brand)s.",
"Quote": "Citat", "Quote": "Citat",
"Messages in group chats": "Beskeder i gruppechats", "Messages in group chats": "Beskeder i gruppechats",
"Yesterday": "I går", "Yesterday": "I går",
"Error encountered (%(errorDetail)s).": "En fejl er opstået (%(errorDetail)s).", "Error encountered (%(errorDetail)s).": "En fejl er opstået (%(errorDetail)s).",
"Event Type": "Begivenhedstype", "Event Type": "Begivenhedstype",
"Low Priority": "Lav prioritet", "Low Priority": "Lav prioritet",
"Unable to fetch notification target list": "Kan ikke hente meddelelsesmålliste",
"Set Password": "Indstil Password",
"Resend": "Send igen", "Resend": "Send igen",
"%(brand)s does not know how to join a room on this network": "%(brand)s ved ikke, hvordan man kan deltage i et rum på dette netværk", "%(brand)s does not know how to join a room on this network": "%(brand)s ved ikke, hvordan man kan deltage i et rum på dette netværk",
"Mentions only": "Kun nævninger",
"Failed to remove tag %(tagName)s from room": "Kunne ikke fjerne tag(s): %(tagName)s fra rummet", "Failed to remove tag %(tagName)s from room": "Kunne ikke fjerne tag(s): %(tagName)s fra rummet",
"Wednesday": "Onsdag", "Wednesday": "Onsdag",
"You can now return to your account after signing out, and sign in on other devices.": "Du kan nu vende tilbage til din konto efter at have logget ud og logge ind på andre enheder.",
"Enable email notifications": "Aktivér e-mail-underretninger",
"Download this file": "Download denne fil",
"Pin Message": "Fasthold Besked",
"Failed to change settings": "Kunne ikke ændre indstillinger",
"Developer Tools": "Udviklingsværktøjer", "Developer Tools": "Udviklingsværktøjer",
"Event Content": "Begivenhedsindhold", "Event Content": "Begivenhedsindhold",
"Thank you!": "Tak!", "Thank you!": "Tak!",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Med din nuværnde broser kan udseendet og fornemmelsen af programmet være helt forkert og nogle funktioner virker måske ikke. Hvis du alligevel vil prøve så kan du fortsætte, men det er på egen risiko!",
"Checking for an update...": "Checker om der er en opdatering...", "Checking for an update...": "Checker om der er en opdatering...",
"Logs sent": "Logfiler sendt", "Logs sent": "Logfiler sendt",
"Reply": "Besvar", "Reply": "Besvar",
"All messages (noisy)": "Alle meddelelser (højlydt)",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-logfiler indeholder brugerdata såsom brugernavn, ID'er eller aliaser for de rum eller grupper, du har besøgt, og andres brugernavne. De indeholder ikke meddelelser.",
"Failed to send logs: ": "Kunne ikke sende logfiler: ", "Failed to send logs: ": "Kunne ikke sende logfiler: ",
"View Community": "Vis community", "View Community": "Vis community",
"Preparing to send logs": "Forbereder afsendelse af logfiler", "Preparing to send logs": "Forbereder afsendelse af logfiler",
@ -323,9 +237,6 @@
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bed administratoren af din homeserver (<code>%(homeserverDomain)s</code>) om at konfigurere en TURN server for at opkald virker pålideligt.", "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bed administratoren af din homeserver (<code>%(homeserverDomain)s</code>) om at konfigurere en TURN server for at opkald virker pålideligt.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativt kan du prøve at bruge den offentlige server <code>turn.matrix.org</code>, men det er ikke lige så pålideligt, og din IP-adresse deles med den server. Du kan også administrere dette under Indstillinger.", "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativt kan du prøve at bruge den offentlige server <code>turn.matrix.org</code>, men det er ikke lige så pålideligt, og din IP-adresse deles med den server. Du kan også administrere dette under Indstillinger.",
"Try using turn.matrix.org": "Prøv at bruge turn.matrix.org", "Try using turn.matrix.org": "Prøv at bruge turn.matrix.org",
"Call in Progress": "Igangværende opkald",
"A call is currently being placed!": "Et opkald er allerede ved at blive oprettet!",
"A call is already in progress!": "Et opkald er allerede i gang!",
"Permission Required": "Tilladelse påkrævet", "Permission Required": "Tilladelse påkrævet",
"You do not have permission to start a conference call in this room": "Du har ikke rettighed til at starte et gruppekald i dette rum", "You do not have permission to start a conference call in this room": "Du har ikke rettighed til at starte et gruppekald i dette rum",
"Replying With Files": "Svare med filer", "Replying With Files": "Svare med filer",
@ -372,8 +283,6 @@
"Sends the given message coloured as a rainbow": "Sender beskeden med regnbuefarver", "Sends the given message coloured as a rainbow": "Sender beskeden med regnbuefarver",
"Sends the given emote coloured as a rainbow": "Sender emoji'en med regnbuefarver", "Sends the given emote coloured as a rainbow": "Sender emoji'en med regnbuefarver",
"Displays list of commands with usages and descriptions": "Viser en liste over kommandoer med beskrivelser", "Displays list of commands with usages and descriptions": "Viser en liste over kommandoer med beskrivelser",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ændrede sit visningsnavn til %(displayName)s.",
"%(senderName)s made no change.": "%(senderName)s foretog ingen ændring.",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s opgraderede dette rum.", "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s opgraderede dette rum.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s gjorde rummet offentligt for alle som kender linket.", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s gjorde rummet offentligt for alle som kender linket.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s begrænsede adgang til rummet til kun inviterede.", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s begrænsede adgang til rummet til kun inviterede.",
@ -468,7 +377,6 @@
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s ændret af %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s ændret af %(senderName)s",
"Group & filter rooms by custom tags (refresh to apply changes)": "Gruppér og filtrér rum efter egne tags (opdater for at anvende ændringerne)", "Group & filter rooms by custom tags (refresh to apply changes)": "Gruppér og filtrér rum efter egne tags (opdater for at anvende ændringerne)",
"Render simple counters in room header": "Vis simple tællere i rumhovedet", "Render simple counters in room header": "Vis simple tællere i rumhovedet",
"Multiple integration managers": "Flere integrationsmanagere",
"Enable Emoji suggestions while typing": "Aktiver emoji forslag under indtastning", "Enable Emoji suggestions while typing": "Aktiver emoji forslag under indtastning",
"Show a placeholder for removed messages": "Vis en pladsholder for fjernede beskeder", "Show a placeholder for removed messages": "Vis en pladsholder for fjernede beskeder",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Hvorvidt du benytter %(brand)s på en enhed, hvor touch er den primære input-grænseflade", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Hvorvidt du benytter %(brand)s på en enhed, hvor touch er den primære input-grænseflade",
@ -483,15 +391,11 @@
"Confirm adding phone number": "Bekræft tilføjelse af telefonnummer", "Confirm adding phone number": "Bekræft tilføjelse af telefonnummer",
"Click the button below to confirm adding this phone number.": "Klik på knappen herunder for at bekræfte tilføjelsen af dette telefonnummer.", "Click the button below to confirm adding this phone number.": "Klik på knappen herunder for at bekræfte tilføjelsen af dette telefonnummer.",
"Whether you're using %(brand)s as an installed Progressive Web App": "Om du anvender %(brand)s som en installeret Progressiv Web App", "Whether you're using %(brand)s as an installed Progressive Web App": "Om du anvender %(brand)s som en installeret Progressiv Web App",
"If you cancel now, you won't complete verifying the other user.": "Hvis du annullerer du, vil du ikke have færdiggjort verifikationen af den anden bruger.",
"If you cancel now, you won't complete verifying your other session.": "Hvis du annullerer nu, vil du ikke have færdiggjort verifikationen af din anden session.",
"If you cancel now, you won't complete your operation.": "Hvis du annullerer nu, vil du ikke færdiggøre din operation.",
"Cancel entering passphrase?": "Annuller indtastning af kodeord?", "Cancel entering passphrase?": "Annuller indtastning af kodeord?",
"Enter passphrase": "Indtast kodeord", "Enter passphrase": "Indtast kodeord",
"Setting up keys": "Sætter nøgler op", "Setting up keys": "Sætter nøgler op",
"Verify this session": "Verificér denne session", "Verify this session": "Verificér denne session",
"Encryption upgrade available": "Opgradering af kryptering tilgængelig", "Encryption upgrade available": "Opgradering af kryptering tilgængelig",
"Set up encryption": "Opsæt kryptering",
"Identity server has no terms of service": "Identity serveren har ingen terms of service", "Identity server has no terms of service": "Identity serveren har ingen terms of service",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Denne handling kræver adgang til default identitets serveren <server /> for at validere en email adresse eller et telefonnummer, men serveren har ingen terms of service.", "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Denne handling kræver adgang til default identitets serveren <server /> for at validere en email adresse eller et telefonnummer, men serveren har ingen terms of service.",
"Only continue if you trust the owner of the server.": "Fortsæt kun hvis du stoler på ejeren af denne server.", "Only continue if you trust the owner of the server.": "Fortsæt kun hvis du stoler på ejeren af denne server.",
@ -538,7 +442,6 @@
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s opdaterede en ban-regel der matcher %(glob)s på grund af %(reason)s", "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s opdaterede en ban-regel der matcher %(glob)s på grund af %(reason)s",
"Explore rooms": "Udforsk rum", "Explore rooms": "Udforsk rum",
"Verification code": "Verifikationskode", "Verification code": "Verifikationskode",
"Who can access this room?": "Hvem kan tilgå dette rum?",
"Encrypted": "Krypteret", "Encrypted": "Krypteret",
"Once enabled, encryption cannot be disabled.": "Efter aktivering er det ikke muligt at slå kryptering fra.", "Once enabled, encryption cannot be disabled.": "Efter aktivering er det ikke muligt at slå kryptering fra.",
"Security & Privacy": "Sikkerhed & Privatliv", "Security & Privacy": "Sikkerhed & Privatliv",
@ -584,11 +487,8 @@
"Change Password": "Skift adgangskode", "Change Password": "Skift adgangskode",
"Current password": "Nuværende adgangskode", "Current password": "Nuværende adgangskode",
"Theme added!": "Tema tilføjet!", "Theme added!": "Tema tilføjet!",
"The other party declined the call.": "Den anden part afviste opkaldet.",
"Comment": "Kommentar", "Comment": "Kommentar",
"or": "eller", "or": "eller",
"%(brand)s Android": "%(brand)s Android",
"%(brand)s iOS": "%(brand)s iOS",
"Privacy": "Privatliv", "Privacy": "Privatliv",
"Please enter a name for the room": "Indtast et navn for rummet", "Please enter a name for the room": "Indtast et navn for rummet",
"No results": "Ingen resultater", "No results": "Ingen resultater",
@ -617,7 +517,6 @@
"Unable to access webcam / microphone": "Kan ikke tilgå webcam / mikrofon", "Unable to access webcam / microphone": "Kan ikke tilgå webcam / mikrofon",
"Unable to access microphone": "Kan ikke tilgå mikrofonen", "Unable to access microphone": "Kan ikke tilgå mikrofonen",
"The call could not be established": "Opkaldet kunne ikke etableres", "The call could not be established": "Opkaldet kunne ikke etableres",
"Call Declined": "Opkald afvist",
"Folder": "Mappe", "Folder": "Mappe",
"We couldn't log you in": "Vi kunne ikke logge dig ind", "We couldn't log you in": "Vi kunne ikke logge dig ind",
"Try again": "Prøv igen", "Try again": "Prøv igen",

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