Merge branch 'develop' into feature-surround-with
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@@ -36,6 +36,7 @@ import {Analytics} from "../Analytics";
|
||||
import CountlyAnalytics from "../CountlyAnalytics";
|
||||
import UserActivity from "../UserActivity";
|
||||
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -59,6 +60,7 @@ declare global {
|
||||
mxNotifier: typeof Notifier;
|
||||
mxRightPanelStore: RightPanelStore;
|
||||
mxWidgetStore: WidgetStore;
|
||||
mxWidgetLayoutStore: WidgetLayoutStore;
|
||||
mxCallHandler: CallHandler;
|
||||
mxAnalytics: Analytics;
|
||||
mxCountlyAnalytics: typeof CountlyAnalytics;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
|
||||
|
||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||
export const SSO_IDP_ID_KEY = "mx_sso_idp_id";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
@@ -56,7 +57,7 @@ export default abstract class BasePlatform {
|
||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||
}
|
||||
|
||||
abstract async getConfig(): Promise<{}>;
|
||||
abstract getConfig(): Promise<{}>;
|
||||
|
||||
abstract getDefaultDeviceDisplayName(): string;
|
||||
|
||||
@@ -258,6 +259,9 @@ export default abstract class BasePlatform {
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
if (idpId) {
|
||||
localStorage.setItem(SSO_IDP_ID_KEY, idpId);
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
|
||||
}
|
||||
|
||||
@@ -82,6 +82,12 @@ import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
|
||||
import { Action } from './dispatcher/actions';
|
||||
import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||
|
||||
const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
@@ -119,6 +125,8 @@ export default class CallHandler {
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
private dispatcherRef: string = null;
|
||||
private supportsPstnProtocol = null;
|
||||
private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
|
||||
|
||||
static sharedInstance() {
|
||||
if (!window.mxCallHandler) {
|
||||
@@ -128,6 +136,15 @@ export default class CallHandler {
|
||||
return window.mxCallHandler;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
|
||||
* if a voip_mxid_translate_pattern is set in the config)
|
||||
*/
|
||||
public static roomIdForCall(call: MatrixCall) {
|
||||
if (!call) return null;
|
||||
return roomForVirtualRoom(call.roomId) || call.roomId;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
@@ -145,6 +162,8 @@ export default class CallHandler {
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
|
||||
}
|
||||
|
||||
this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS);
|
||||
}
|
||||
|
||||
stop() {
|
||||
@@ -158,6 +177,33 @@ export default class CallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForPstnSupport(maxTries) {
|
||||
try {
|
||||
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
||||
if (protocols['im.vector.protocol.pstn'] !== undefined) {
|
||||
this.supportsPstnProtocol = protocols['im.vector.protocol.pstn'];
|
||||
} else if (protocols['m.protocol.pstn'] !== undefined) {
|
||||
this.supportsPstnProtocol = protocols['m.protocol.pstn'];
|
||||
} else {
|
||||
this.supportsPstnProtocol = null;
|
||||
}
|
||||
dis.dispatch({action: Action.PstnSupportUpdated});
|
||||
} catch (e) {
|
||||
if (maxTries === 1) {
|
||||
console.log("Failed to check for pstn protocol support and no retries remain: assuming no support", e);
|
||||
} else {
|
||||
console.log("Failed to check for pstn protocol support: will retry", e);
|
||||
this.pstnSupportCheckTimer = setTimeout(() => {
|
||||
this.checkForPstnSupport(maxTries - 1);
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSupportsPstnProtocol() {
|
||||
return this.supportsPstnProtocol;
|
||||
}
|
||||
|
||||
private onCallIncoming = (call) => {
|
||||
// we dispatch this synchronously to make sure that the event
|
||||
// handlers on the call are set up immediately (so that if
|
||||
@@ -250,11 +296,15 @@ export default class CallHandler {
|
||||
// We don't allow placing more than one call per room, but that doesn't mean there
|
||||
// can't be more than one, eg. in a glare situation. This checks that the given call
|
||||
// is the call we consider 'the' call for its room.
|
||||
const callForThisRoom = this.getCallForRoom(call.roomId);
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
const callForThisRoom = this.getCallForRoom(mappedRoomId);
|
||||
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
call.on(CallEvent.Error, (err: CallError) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
@@ -284,7 +334,7 @@ export default class CallHandler {
|
||||
|
||||
Analytics.trackEvent('voip', 'callHangup');
|
||||
|
||||
this.removeCallForRoom(call.roomId);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
});
|
||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
@@ -308,8 +358,9 @@ export default class CallHandler {
|
||||
this.play(AudioID.Ringback);
|
||||
break;
|
||||
case CallState.Ended:
|
||||
{
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
|
||||
this.removeCallForRoom(call.roomId);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && (
|
||||
call.hangupParty === CallParty.Remote ||
|
||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||
@@ -341,9 +392,14 @@ export default class CallHandler {
|
||||
title: _t("Answered Elsewhere"),
|
||||
description: _t("The call was answered on another device."),
|
||||
});
|
||||
} else {
|
||||
} else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) {
|
||||
// don't play the end-call sound for calls that never got off the ground
|
||||
this.play(AudioID.CallEnd);
|
||||
}
|
||||
|
||||
this.logCallStats(call, mappedRoomId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
||||
@@ -357,25 +413,70 @@ export default class CallHandler {
|
||||
this.pause(AudioID.Ringback);
|
||||
}
|
||||
|
||||
this.calls.set(newCall.roomId, newCall);
|
||||
this.calls.set(mappedRoomId, newCall);
|
||||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
}
|
||||
|
||||
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
|
||||
const stats = await call.getCurrentCallStats();
|
||||
logger.debug(
|
||||
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
|
||||
`user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
|
||||
`our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
|
||||
`hangup reason: ${call.hangupReason}`,
|
||||
);
|
||||
if (!stats) {
|
||||
logger.debug(
|
||||
"Call statistics are undefined. The call has " +
|
||||
"probably failed before a peerConn was established",
|
||||
);
|
||||
return;
|
||||
}
|
||||
logger.debug("Local candidates:");
|
||||
for (const cand of stats.filter(item => item.type === 'local-candidate')) {
|
||||
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
|
||||
logger.debug(
|
||||
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
|
||||
`protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
|
||||
);
|
||||
}
|
||||
logger.debug("Remote candidates:");
|
||||
for (const cand of stats.filter(item => item.type === 'remote-candidate')) {
|
||||
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
|
||||
logger.debug(
|
||||
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
|
||||
`protocol: ${cand.protocol}`,
|
||||
);
|
||||
}
|
||||
logger.debug("Candidate pairs:");
|
||||
for (const pair of stats.filter(item => item.type === 'candidate-pair')) {
|
||||
logger.debug(
|
||||
`${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` +
|
||||
`nominated: ${pair.nominated}, ` +
|
||||
`requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` +
|
||||
`responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
|
||||
`bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setCallAudioElement(call: MatrixCall) {
|
||||
const audioElement = getRemoteAudioElement();
|
||||
if (audioElement) call.setRemoteAudioElement(audioElement);
|
||||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
console.log(
|
||||
`Call state in ${call.roomId} changed to ${status}`,
|
||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||
);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'call_state',
|
||||
room_id: call.roomId,
|
||||
room_id: mappedRoomId,
|
||||
state: status,
|
||||
});
|
||||
}
|
||||
@@ -442,14 +543,20 @@ export default class CallHandler {
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
private placeCall(
|
||||
private async placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
) {
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||
|
||||
const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
|
||||
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
|
||||
|
||||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
||||
@@ -473,9 +580,17 @@ export default class CallHandler {
|
||||
});
|
||||
return;
|
||||
}
|
||||
call.placeScreenSharingCall(remoteElement, localElement);
|
||||
|
||||
call.placeScreenSharingCall(
|
||||
remoteElement,
|
||||
localElement,
|
||||
async () : Promise<DesktopCapturerSource> => {
|
||||
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
const [source] = await finished;
|
||||
return source;
|
||||
});
|
||||
} else {
|
||||
console.error("Unknown conf call type: %s", type);
|
||||
console.error("Unknown conf call type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +598,12 @@ export default class CallHandler {
|
||||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
{
|
||||
// We might be using managed hybrid widgets
|
||||
if (isManagedHybridWidgetEnabled()) {
|
||||
addManagedHybridWidget(payload.room_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
@@ -503,7 +624,7 @@ export default class CallHandler {
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
console.error(`Room ${payload.room_id} does not exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -514,7 +635,7 @@ export default class CallHandler {
|
||||
});
|
||||
return;
|
||||
} else if (members.length === 2) {
|
||||
console.info("Place %s call in %s", payload.type, payload.room_id);
|
||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||
|
||||
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
|
||||
} else { // > 2
|
||||
@@ -529,17 +650,17 @@ export default class CallHandler {
|
||||
}
|
||||
break;
|
||||
case 'place_conference_call':
|
||||
console.info("Place conference call in %s", payload.room_id);
|
||||
console.info("Place conference call in " + payload.room_id);
|
||||
Analytics.trackEvent('voip', 'placeConferenceCall');
|
||||
CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
|
||||
this.startCallApp(payload.room_id, payload.type);
|
||||
break;
|
||||
case 'end_conference':
|
||||
console.info("Terminating conference call in %s", payload.room_id);
|
||||
console.info("Terminating conference call in " + payload.room_id);
|
||||
this.terminateCallApp(payload.room_id);
|
||||
break;
|
||||
case 'hangup_conference':
|
||||
console.info("Leaving conference call in %s", payload.room_id);
|
||||
console.info("Leaving conference call in "+ payload.room_id);
|
||||
this.hangupCallApp(payload.room_id);
|
||||
break;
|
||||
case 'incoming_call':
|
||||
@@ -551,13 +672,14 @@ export default class CallHandler {
|
||||
|
||||
const call = payload.call as MatrixCall;
|
||||
|
||||
if (this.getCallForRoom(call.roomId)) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
if (this.getCallForRoom(mappedRoomId)) {
|
||||
// ignore multiple incoming calls to the same room
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(call.roomId, call)
|
||||
this.calls.set(mappedRoomId, call)
|
||||
this.setCallListeners(call);
|
||||
}
|
||||
break;
|
||||
@@ -616,6 +738,18 @@ export default class CallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we are currently in any call where we haven't put the remote party on hold
|
||||
*/
|
||||
hasAnyUnheldCall() {
|
||||
for (const call of this.calls.values()) {
|
||||
if (call.state === CallState.Ended) continue;
|
||||
if (!call.isRemoteOnHold()) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async startCallApp(roomId: string, type: string) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
|
||||
@@ -497,7 +497,7 @@ export default class ContentMessages {
|
||||
content.info.mimetype = file.type;
|
||||
}
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
if (file.type.indexOf('image/') === 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||
|
||||
@@ -840,7 +840,7 @@ export default class CountlyAnalytics {
|
||||
let endTime = CountlyAnalytics.getTimestamp();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.getRoom(roomId)) {
|
||||
await new Promise(resolve => {
|
||||
await new Promise<void>(resolve => {
|
||||
const handler = (room) => {
|
||||
if (room.roomId === roomId) {
|
||||
cli.off("Room", handler);
|
||||
@@ -880,7 +880,7 @@ export default class CountlyAnalytics {
|
||||
let endTime = CountlyAnalytics.getTimestamp();
|
||||
|
||||
if (!room.findEventById(eventId)) {
|
||||
await new Promise(resolve => {
|
||||
await new Promise<void>(resolve => {
|
||||
const handler = (ev) => {
|
||||
if (ev.getId() === eventId) {
|
||||
room.off("Room.localEchoUpdated", handler);
|
||||
|
||||
@@ -163,7 +163,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
||||
attribs.target = '_blank'; // by default
|
||||
|
||||
const transformed = tryTransformPermalinkToLocalHref(attribs.href);
|
||||
if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) {
|
||||
if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.ELEMENT_URL_PATTERN)) {
|
||||
attribs.href = transformed;
|
||||
delete attribs.target;
|
||||
}
|
||||
@@ -422,6 +422,8 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||
const phtml = cheerio.load(safeBody,
|
||||
{ _useHtmlParser2: true, decodeEntities: false })
|
||||
// @ts-ignore - The types for `replaceWith` wrongly expect
|
||||
// Cheerio instance to be returned.
|
||||
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
||||
return katex.renderToString(
|
||||
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
|
||||
@@ -438,13 +440,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||
delete sanitizeParams.textFilter;
|
||||
}
|
||||
|
||||
const contentBody = isDisplayedWithHtml ? safeBody : strippedBody;
|
||||
if (opts.returnString) {
|
||||
return isDisplayedWithHtml ? safeBody : strippedBody;
|
||||
return contentBody;
|
||||
}
|
||||
|
||||
let emojiBody = false;
|
||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : '';
|
||||
|
||||
// Ignore spaces in body text. Emojis with spaces in between should
|
||||
// still be counted as purely emoji messages.
|
||||
|
||||
@@ -165,6 +165,7 @@ export default class IdentityAuthClient {
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
} else {
|
||||
throw new AbortedIdentityActionError(
|
||||
|
||||
@@ -46,11 +46,13 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY} from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import {_t} from "./languageHandler";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
@@ -162,7 +164,8 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||
* query-parameters extracted from the real query-string of the starting
|
||||
* URI.
|
||||
*
|
||||
* @param {String} defaultDeviceDisplayName
|
||||
* @param {string} defaultDeviceDisplayName
|
||||
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
|
||||
*
|
||||
* @returns {Promise} promise which resolves to true if we completed the token
|
||||
* login, else false
|
||||
@@ -170,6 +173,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||
export function attemptTokenLogin(
|
||||
queryParams: Record<string, string>,
|
||||
defaultDeviceDisplayName?: string,
|
||||
fragmentAfterLogin?: string,
|
||||
): Promise<boolean> {
|
||||
if (!queryParams.loginToken) {
|
||||
return Promise.resolve(false);
|
||||
@@ -179,6 +183,12 @@ export function attemptTokenLogin(
|
||||
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
|
||||
if (!homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
Modal.createTrackedDialog("SSO", "Unknown HS", ErrorDialog, {
|
||||
title: _t("We couldn't log you in"),
|
||||
description: _t("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."),
|
||||
button: _t("Try again"),
|
||||
});
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
@@ -198,8 +208,28 @@ export function attemptTokenLogin(
|
||||
return true;
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error("Failed to log in with login token: " + err + " " +
|
||||
err.data);
|
||||
Modal.createTrackedDialog("SSO", "Token Rejected", ErrorDialog, {
|
||||
title: _t("We couldn't log you in"),
|
||||
description: err.name === "ConnectionError"
|
||||
? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " +
|
||||
"If this continues, please contact your homeserver administrator.")
|
||||
: _t("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."),
|
||||
button: _t("Try again"),
|
||||
onFinished: tryAgain => {
|
||||
if (tryAgain) {
|
||||
const cli = Matrix.createClient({
|
||||
baseUrl: homeserver,
|
||||
idBaseUrl: identityServer,
|
||||
});
|
||||
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
|
||||
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
|
||||
}
|
||||
},
|
||||
});
|
||||
console.error("Failed to log in with login token:");
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -366,7 +396,7 @@ async function abortLogin() {
|
||||
// The plan is to gradually move the localStorage access done here into
|
||||
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
||||
// localStorage (e.g. isGuest etc.)
|
||||
async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
|
||||
export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
|
||||
const ignoreGuest = opts?.ignoreGuest;
|
||||
|
||||
if (!localStorage) {
|
||||
|
||||
10
src/Login.ts
10
src/Login.ts
@@ -33,10 +33,20 @@ interface IPasswordFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
Gitlab = "org.matrix.gitlab",
|
||||
Github = "org.matrix.github",
|
||||
Apple = "org.matrix.apple",
|
||||
Google = "org.matrix.google",
|
||||
Facebook = "org.matrix.facebook",
|
||||
Twitter = "org.matrix.twitter",
|
||||
}
|
||||
|
||||
export interface IIdentityProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
brand?: IdentityProviderBrand | string;
|
||||
}
|
||||
|
||||
export interface ISSOFlow {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import commonmark from 'commonmark';
|
||||
import * as commonmark from 'commonmark';
|
||||
import {escape} from "lodash";
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||
|
||||
@@ -202,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
}
|
||||
|
||||
function findOverrideMuteRule(roomId) {
|
||||
if (!MatrixClientPeg.get().pushRules ||
|
||||
!MatrixClientPeg.get().pushRules['global'] ||
|
||||
!MatrixClientPeg.get().pushRules['global'].override) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.pushRules ||
|
||||
!cli.pushRules['global'] ||
|
||||
!cli.pushRules['global'].override) {
|
||||
return null;
|
||||
}
|
||||
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
||||
for (const rule of cli.pushRules['global'].override) {
|
||||
if (isRuleForRoom(roomId, rule)) {
|
||||
if (isMuteRule(rule) && rule.enabled) {
|
||||
return rule;
|
||||
|
||||
@@ -48,6 +48,7 @@ import SettingsStore from "./settings/SettingsStore";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import {CHAT_EFFECTS} from "./effects"
|
||||
import CallHandler from "./CallHandler";
|
||||
import {guessAndSetDMRoom} from "./Rooms";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
@@ -1112,6 +1113,24 @@ export const Commands = [
|
||||
return success();
|
||||
},
|
||||
}),
|
||||
new Command({
|
||||
command: "converttodm",
|
||||
description: _td("Converts the room to a DM"),
|
||||
category: CommandCategories.other,
|
||||
runFn: function(roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return success(guessAndSetDMRoom(room, true));
|
||||
},
|
||||
}),
|
||||
new Command({
|
||||
command: "converttoroom",
|
||||
description: _td("Converts the DM to a room"),
|
||||
category: CommandCategories.other,
|
||||
runFn: function(roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return success(guessAndSetDMRoom(room, false));
|
||||
},
|
||||
}),
|
||||
|
||||
// Command definitions for autocompletion ONLY:
|
||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as Roles from './Roles';
|
||||
import {isValid3pidInvite} from "./RoomInvite";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
@@ -477,6 +478,11 @@ function textForWidgetEvent(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function textForWidgetLayoutEvent(event) {
|
||||
const senderName = event.sender?.name || event.getSender();
|
||||
return _t("%(senderName)s has updated the widget layout", {senderName});
|
||||
}
|
||||
|
||||
function textForMjolnirEvent(event) {
|
||||
const senderName = event.getSender();
|
||||
const {entity: prevEntity} = event.getPrevContent();
|
||||
@@ -583,6 +589,7 @@ const stateHandlers = {
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
|
||||
};
|
||||
|
||||
// Add all the Mjolnir stuff to the renderer
|
||||
|
||||
79
src/VoipUserMapper.ts
Normal file
79
src/VoipUserMapper.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
|
||||
// Functions for mapping users & rooms for the voip_mxid_translate_pattern
|
||||
// config option
|
||||
|
||||
export function voipUserMapperEnabled(): boolean {
|
||||
return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined;
|
||||
}
|
||||
|
||||
// only exported for tests
|
||||
export function userToVirtualUser(userId: string, templateString?: string): string {
|
||||
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
|
||||
if (!templateString) return null;
|
||||
return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase());
|
||||
}
|
||||
|
||||
// only exported for tests
|
||||
export function virtualUserToUser(userId: string, templateString?: string): string {
|
||||
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
|
||||
if (!templateString) return null;
|
||||
|
||||
const regexString = templateString.replace('${mxid}', '(.+)');
|
||||
|
||||
const match = userId.match('^' + regexString + '$');
|
||||
if (!match) return null;
|
||||
|
||||
return decodeURIComponent(match[1].replace(/=/g, '%'));
|
||||
}
|
||||
|
||||
async function getOrCreateVirtualRoomForUser(userId: string):Promise<string> {
|
||||
const virtualUser = userToVirtualUser(userId);
|
||||
if (!virtualUser) return null;
|
||||
|
||||
return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
|
||||
}
|
||||
|
||||
export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
|
||||
const user = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!user) return null;
|
||||
return getOrCreateVirtualRoomForUser(user);
|
||||
}
|
||||
|
||||
export function roomForVirtualRoom(roomId: string):string {
|
||||
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!virtualUser) return null;
|
||||
const realUser = virtualUserToUser(virtualUser);
|
||||
const room = findDMForUser(MatrixClientPeg.get(), realUser);
|
||||
if (room) {
|
||||
return room.roomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isVirtualRoom(roomId: string):boolean {
|
||||
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!virtualUser) return null;
|
||||
const realUser = virtualUserToUser(virtualUser);
|
||||
return Boolean(realUser);
|
||||
}
|
||||
@@ -168,6 +168,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
||||
key: Key.U,
|
||||
}],
|
||||
description: _td("Upload a file"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
modifiers: [CMD_OR_CTRL],
|
||||
key: Key.F,
|
||||
}],
|
||||
description: _td("Search (must be enabled)"),
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
const blob = new Blob([this._keyBackupInfo.recovery_key], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'recovery-key.txt');
|
||||
FileSaver.saveAs(blob, 'security-key.txt');
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
@@ -238,7 +238,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"We'll store an encrypted copy of your keys on our server. " +
|
||||
"Secure your backup with a recovery passphrase.",
|
||||
"Secure your backup with a Security Phrase.",
|
||||
)}</p>
|
||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
||||
|
||||
@@ -252,10 +252,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a recovery passphrase")}
|
||||
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
label={_td("Enter a Security Phrase")}
|
||||
labelEnterPassword={_td("Enter a Security Phrase")}
|
||||
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,7 +270,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||
{_t("Set up with a recovery key")}
|
||||
{_t("Set up with a Security Key")}
|
||||
</AccessibleButton>
|
||||
</details>
|
||||
</form>;
|
||||
@@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
"Please enter your recovery passphrase a second time to confirm.",
|
||||
"Please enter your Security Phrase a second time to confirm.",
|
||||
)}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
@@ -319,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||
placeholder={_t("Repeat your recovery passphrase...")}
|
||||
placeholder={_t("Repeat your Security Phrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -338,15 +338,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
_renderPhaseShowKey() {
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Your recovery key is a safety net - you can use it to restore " +
|
||||
"access to your encrypted messages if you forget your recovery passphrase.",
|
||||
"Your Security Key is a safety net - you can use it to restore " +
|
||||
"access to your encrypted messages if you forget your Security Phrase.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
|
||||
)}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
||||
{_t("Your recovery key")}
|
||||
{_t("Your Security Key")}
|
||||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKey">
|
||||
@@ -369,12 +369,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
let introText;
|
||||
if (this.state.copied) {
|
||||
introText = _t(
|
||||
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
|
||||
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
} else if (this.state.downloaded) {
|
||||
introText = _t(
|
||||
"Your recovery key is in your <b>Downloads</b> folder.",
|
||||
"Your Security Key is in your <b>Downloads</b> folder.",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
}
|
||||
@@ -433,14 +433,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
_titleForPhase(phase) {
|
||||
switch (phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Secure your backup with a recovery passphrase');
|
||||
return _t('Secure your backup with a Security Phrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
return _t('Confirm your recovery passphrase');
|
||||
return _t('Confirm your Security Phrase');
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
return _t('Warning!');
|
||||
case PHASE_SHOWKEY:
|
||||
case PHASE_KEEPITSAFE:
|
||||
return _t('Make a copy of your recovery key');
|
||||
return _t('Make a copy of your Security Key');
|
||||
case PHASE_BACKINGUP:
|
||||
return _t('Starting backup...');
|
||||
case PHASE_DONE:
|
||||
|
||||
@@ -235,7 +235,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'recovery-key.txt');
|
||||
FileSaver.saveAs(blob, 'security-key.txt');
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
@@ -593,10 +593,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a recovery passphrase")}
|
||||
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
label={_td("Enter a Security Phrase")}
|
||||
labelEnterPassword={_td("Enter a Security Phrase")}
|
||||
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||
</span>;
|
||||
|
||||
const newMethodDetected = <p>{_t(
|
||||
"A new recovery passphrase and key for Secure Messages have been detected.",
|
||||
"A new Security Phrase and key for Secure Messages have been detected.",
|
||||
)}</p>;
|
||||
|
||||
const hackWarning = <p className="warning">{_t(
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||
>
|
||||
<div>
|
||||
<p>{_t(
|
||||
"This session has detected that your recovery passphrase and key " +
|
||||
"This session has detected that your Security Phrase and key " +
|
||||
"for Secure Messages have been removed.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
|
||||
@@ -397,7 +397,8 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||
return {left, top, chevronOffset};
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
@@ -416,6 +417,41 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonLeft = elementRect.left + window.pageXOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the left edge of the menu to the left edge of the button
|
||||
menuOptions.left = buttonLeft;
|
||||
// Align the menu vertically above the menu
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
|
||||
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||
const button = useRef<T>(null);
|
||||
|
||||
@@ -45,7 +45,7 @@ class FilePanel extends React.Component {
|
||||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
if (room.roomId !== this.props.roomId) return;
|
||||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
if (ev.isBeingDecrypted()) {
|
||||
|
||||
45
src/components/structures/HostSignupAction.tsx
Normal file
45
src/components/structures/HostSignupAction.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {}
|
||||
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconHosting"
|
||||
label={_t("Upgrade to pro")}
|
||||
onClick={this.openDialog}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
}, () => {
|
||||
if (oldStage != stageType) this._setFocus();
|
||||
if (oldStage !== stageType) {
|
||||
this._setFocus();
|
||||
} else if (
|
||||
!stageState.error && this._stageComponent.current &&
|
||||
this._stageComponent.current.attemptFailed
|
||||
) {
|
||||
this._stageComponent.current.attemptFailed();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||
|
||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||
useEffect(onResize, [expanded]);
|
||||
useEffect(onResize, [expanded, onResize]);
|
||||
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
@@ -54,6 +54,7 @@ import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPa
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
@@ -140,7 +141,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||
protected readonly _compactLayoutWatcherRef: string;
|
||||
protected compactLayoutWatcherRef: string;
|
||||
protected resizer: Resizer;
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -157,18 +158,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = React.createRef();
|
||||
@@ -176,6 +165,24 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
// Call `onSync` with the current state as well
|
||||
this.onSync(
|
||||
this._matrixClient.getSyncState(),
|
||||
null,
|
||||
this._matrixClient.getSyncStateData(),
|
||||
);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
this.compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
@@ -186,7 +193,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
|
||||
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
@@ -209,10 +216,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
|
||||
_createResizer() {
|
||||
let size;
|
||||
let collapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
toggleSize: 260 - 50,
|
||||
onCollapsed: (collapsed) => {
|
||||
if (collapsed) {
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
dis.dispatch({action: "hide_left_panel"}, true);
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
} else {
|
||||
@@ -227,7 +236,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
this.props.resizeNotifier.startResizing();
|
||||
},
|
||||
onResizeStop: () => {
|
||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
};
|
||||
@@ -419,6 +428,14 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.F:
|
||||
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) {
|
||||
dis.dispatch({
|
||||
action: 'focus_search',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.BACKTICK:
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
@@ -632,6 +649,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityProt
|
||||
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
@@ -217,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private screenAfterLogin?: IScreen;
|
||||
private windowWidth: number;
|
||||
private pageChanging: boolean;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: NodeJS.Timeout;
|
||||
private focusComposer: boolean;
|
||||
@@ -322,13 +325,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
Lifecycle.attemptTokenLogin(
|
||||
this.props.realQueryParams,
|
||||
this.props.defaultDeviceDisplayName,
|
||||
).then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.getFragmentAfterLogin(),
|
||||
).then(async (loggedIn) => {
|
||||
if (this.props.realQueryParams?.loginToken) {
|
||||
// remove the loginToken from the URL regardless
|
||||
this.props.onTokenLoginCompleted();
|
||||
}
|
||||
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
// the 'loading' state.
|
||||
return;
|
||||
if (loggedIn) {
|
||||
this.tokenLogin = true;
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.restoreFromLocalStorage({
|
||||
ignoreGuest: true,
|
||||
});
|
||||
return this.postLoginSetup();
|
||||
}
|
||||
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
@@ -352,6 +363,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
private async postLoginSetup() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillUpdate(props, state) {
|
||||
@@ -703,8 +750,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.state.resizeNotifier.notifyLeftHandleResized();
|
||||
});
|
||||
break;
|
||||
case Action.OpenDialPad:
|
||||
Modal.createTrackedDialog('Dial pad', '', DialPadModal, {}, "mx_Dialog_dialPadWrapper");
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
if (
|
||||
// Skip this handling for token login as that always calls onLoggedIn itself
|
||||
!this.tokenLogin &&
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
this.state.view !== Views.LOGIN &&
|
||||
this.state.view !== Views.REGISTER &&
|
||||
@@ -1182,6 +1234,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
) {
|
||||
showAnalyticsToast(this.props.config.piwik?.policyUrl);
|
||||
}
|
||||
if (SdkConfig.get().mobileGuideToast) {
|
||||
// The toast contains further logic to detect mobile platforms,
|
||||
// check if it has been dismissed before, etc.
|
||||
showMobileGuideToast();
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
@@ -1318,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
cli.on('Session.logged_out', function(errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
// A modal might have been open when we were logged out by the server
|
||||
Modal.closeCurrentModal('Session.logged_out');
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
|
||||
console.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
@@ -1328,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
title: _t('Signed Out'),
|
||||
description: _t('For security, this session has been signed out. Please sign in again.'),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: 'logout',
|
||||
});
|
||||
@@ -1597,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
|
||||
|
||||
let threepidInvite: IThreepidInvite;
|
||||
// if we landed here from a 3PID invite, persist it
|
||||
if (params.signurl && params.email) {
|
||||
threepidInvite = ThreepidInviteStore.instance
|
||||
.storeInvite(roomString, params as IThreepidInviteWireFormat);
|
||||
}
|
||||
// otherwise check that this room doesn't already have a known invite
|
||||
if (!threepidInvite) {
|
||||
const invites = ThreepidInviteStore.instance.getInvites();
|
||||
threepidInvite = invites.find(invite => invite.roomId === roomString);
|
||||
}
|
||||
|
||||
// on our URLs there might be a ?via=matrix.org or similar to help
|
||||
// joins to the room succeed. We'll pass these through as an array
|
||||
@@ -1829,40 +1896,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
await this.postLoginSetup();
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
@@ -1906,6 +1940,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
<E2eSetup
|
||||
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||
accountPassword={this.accountPassword}
|
||||
tokenLogin={!!this.tokenLogin}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === Views.LOGGED_IN) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
@@ -207,11 +208,13 @@ export default class MessagePanel extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -224,6 +227,14 @@ export default class MessagePanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case "scroll_to_bottom":
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onShowTypingNotificationsChange = () => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
||||
@@ -39,7 +39,7 @@ class NotificationPanel extends React.Component {
|
||||
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications in this room.')}</p>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
</div>);
|
||||
|
||||
let content;
|
||||
|
||||
@@ -30,7 +30,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
@@ -186,7 +185,7 @@ export default class RightPanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onCloseUserInfo = () => {
|
||||
onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
@@ -198,31 +197,21 @@ export default class RightPanel extends React.Component {
|
||||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else if (this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
} else if (
|
||||
this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
this.state.verificationRequest && this.state.verificationRequest.pending
|
||||
) {
|
||||
// When the user clicks close on the encryption panel cancel the pending request first if any
|
||||
this.state.verificationRequest.cancel();
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room/group, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel;
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: isEncryptionPhase ? this.state.member : null,
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
@@ -260,7 +249,7 @@ export default class RightPanel extends React.Component {
|
||||
user={this.state.member}
|
||||
room={this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
@@ -276,7 +265,7 @@ export default class RightPanel extends React.Component {
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupRoomInfo:
|
||||
|
||||
@@ -477,7 +477,7 @@ export default class RoomDirectory extends React.Component {
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
getRow(room) {
|
||||
createRoomCells(room) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
@@ -487,7 +487,11 @@ export default class RoomDirectory extends React.Component {
|
||||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
if (room.world_readable && !hasJoinedRoom) {
|
||||
// Element Web currently does not allow guests to join rooms, so we
|
||||
// instead show them preview buttons for all rooms. If the room is not
|
||||
// world readable, a modal will appear asking you to register first. If
|
||||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
||||
);
|
||||
@@ -496,7 +500,7 @@ export default class RoomDirectory extends React.Component {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest || room.guest_can_join) {
|
||||
} else if (!isGuest) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
||||
);
|
||||
@@ -519,31 +523,56 @@ export default class RoomDirectory extends React.Component {
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatar_url, 32, 32, "crop",
|
||||
);
|
||||
return (
|
||||
<tr key={ room.room_id }
|
||||
return [
|
||||
<div key={ `${room.room_id}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ room.num_joined_members }
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_preview">{previewButton}</td>
|
||||
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
|
||||
</tr>
|
||||
);
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl }
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_preview` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{previewButton}
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_join` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{joinOrViewButton}
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
collectScrollPanel = (element) => {
|
||||
@@ -602,7 +631,8 @@ export default class RoomDirectory extends React.Component {
|
||||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
|
||||
const cells = (this.state.publicRooms || [])
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
@@ -613,14 +643,12 @@ export default class RoomDirectory extends React.Component {
|
||||
}
|
||||
|
||||
let scrollpanel_content;
|
||||
if (rows.length === 0 && !this.state.loading) {
|
||||
if (cells.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>;
|
||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
||||
{ cells }
|
||||
</div>;
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
|
||||
@@ -21,15 +21,15 @@ limitations under the License.
|
||||
// - Search results component
|
||||
// - Drag and drop
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {_t} from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
@@ -40,8 +40,8 @@ import Tinter from '../../Tinter';
|
||||
import rateLimitedFunc from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
@@ -50,13 +50,13 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {SettingLevel} from "../../settings/SettingLevel";
|
||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
@@ -67,17 +67,18 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import {containsEmoji} from '../../effects/utils';
|
||||
import {CHAT_EFFECTS} from '../../effects';
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
@@ -266,12 +267,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move into constructor
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
if (this.state.room) {
|
||||
this.checkWidgets(this.state.room);
|
||||
@@ -280,8 +275,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
|
||||
})
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(room),
|
||||
});
|
||||
};
|
||||
|
||||
private onReadReceiptsChange = () => {
|
||||
@@ -418,11 +414,17 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(this.state.room),
|
||||
});
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
|
||||
};
|
||||
|
||||
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
@@ -488,7 +490,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private shouldShowApps(room: Room) {
|
||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false;
|
||||
|
||||
// Check if user has previously chosen to hide the app drawer for this
|
||||
// room. If so, do not show apps
|
||||
@@ -497,10 +499,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
|
||||
// This is confusing, but it means to say that we default to the tray being
|
||||
// hidden unless the user clicked to open it.
|
||||
return hideWidgetDrawer === "false";
|
||||
const isManuallyShown = hideWidgetDrawer === "false";
|
||||
|
||||
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
return widgets.length > 0 || isManuallyShown;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
|
||||
const call = this.getCallForRoom();
|
||||
const callState = call ? call.state : null;
|
||||
this.setState({
|
||||
@@ -608,6 +615,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||
}
|
||||
@@ -748,6 +762,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -835,6 +852,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
// called when state.room is first initialised (either at initial load,
|
||||
// after a successful peek, or after we join the room).
|
||||
private onRoomLoaded = (room: Room) => {
|
||||
// Attach a widget store listener only when we get a room
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
this.onWidgetLayoutChange(); // provoke an update
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.loadMembersIfJoined(room);
|
||||
@@ -897,6 +918,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detach the listener if the room is changing for some reason
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
room: room,
|
||||
}, () => {
|
||||
|
||||
@@ -20,7 +20,6 @@ import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import * as sdk from "../../index";
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -28,7 +28,6 @@ import Modal from "../../Modal";
|
||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
@@ -51,6 +50,8 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import HostSignupAction from "./HostSignupAction";
|
||||
import {IHostSignupConfig} from "../views/dialogs/HostSignupDialogTypes";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
@@ -272,7 +273,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
@@ -292,24 +293,19 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
} else if (signupLink) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (hostSignupConfig) {
|
||||
if (hostSignupConfig && hostSignupConfig.url) {
|
||||
// If hostSignup.domains is set to a non-empty array, only show
|
||||
// dialog if the user is on the domain or a subdomain.
|
||||
const hostSignupDomains = hostSignupConfig.domains || [];
|
||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||
if (!hostSignupDomains || validDomains.length > 0) {
|
||||
topSection = <div onClick={this.onCloseMenu}>
|
||||
<HostSignupAction />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class E2eSetup extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -33,6 +34,7 @@ export default class E2eSetup extends React.Component {
|
||||
<CreateCrossSigningDialog
|
||||
onFinished={this.props.onFinished}
|
||||
accountPassword={this.props.accountPassword}
|
||||
tokenLogin={this.props.tokenLogin}
|
||||
/>
|
||||
</CompleteSecurityBody>
|
||||
</AuthPage>
|
||||
|
||||
@@ -340,8 +340,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
};
|
||||
|
||||
onTryRegisterClick = ev => {
|
||||
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
const hasPasswordFlow = this.state.flows?.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows?.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
// If has no password flow but an SSO flow guess that the user wants to register with SSO.
|
||||
// TODO: instead hide the Register button if registration is disabled by checking with the server,
|
||||
// has no specific errCode currently and uses M_FORBIDDEN.
|
||||
|
||||
@@ -120,9 +120,9 @@ export default class SetupEncryptionBody extends React.Component {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key");
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
|
||||
@@ -72,9 +72,12 @@ export default class SoftLogout extends React.Component {
|
||||
|
||||
this._initLogin();
|
||||
|
||||
MatrixClientPeg.get().countSessionsNeedingBackup().then(remaining => {
|
||||
this.setState({keyBackupNeeded: remaining > 0});
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isCryptoEnabled()) {
|
||||
cli.countSessionsNeedingBackup().then(remaining => {
|
||||
this.setState({ keyBackupNeeded: remaining > 0 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClearAll = () => {
|
||||
|
||||
@@ -609,8 +609,12 @@ export class SSOAuthEntry extends React.Component {
|
||||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
attemptFailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -618,12 +622,35 @@ export class SSOAuthEntry extends React.Component {
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onStartAuthClick = () => {
|
||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||
// certainly will need to open the thing in a new tab to avoid losing application
|
||||
// context.
|
||||
|
||||
window.open(this._ssoUrl, '_blank');
|
||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
@@ -656,10 +683,28 @@ export class SSOAuthEntry extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.attemptFailed) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ _t("Something went wrong in confirming your identity. Cancel and try again.") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{ errorSection }
|
||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,8 +755,7 @@ export class FallbackAuthEntry extends React.Component {
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
|
||||
@@ -196,7 +196,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
||||
@@ -194,7 +194,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
||||
@@ -20,6 +20,8 @@ import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
@@ -46,14 +48,30 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onTransferClick = () => {
|
||||
Modal.createTrackedDialog(
|
||||
'Transfer Call', '', InviteDialog, {kind: KIND_CALL_TRANSFER, call: this.props.call},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
||||
const handler = this.props.call.isRemoteOnHold() ? this.onUnholdClick : this.onHoldClick;
|
||||
|
||||
let transferItem;
|
||||
if (this.props.call.opponentCanBeTransferred()) {
|
||||
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
||||
{_t("Transfer")}
|
||||
</MenuItem>;
|
||||
}
|
||||
|
||||
return <ContextMenu {...this.props}>
|
||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||
{holdUnholdCaption}
|
||||
</MenuItem>
|
||||
{transferItem}
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
||||
|
||||
59
src/components/views/context_menus/DialpadContextMenu.tsx
Normal file
59
src/components/views/context_menus/DialpadContextMenu.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Dialpad from '../voip/DialPad';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class DialpadContextMenu extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
}
|
||||
}
|
||||
|
||||
onDigitPress = (digit) => {
|
||||
this.props.call.sendDtmfDigit(digit);
|
||||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
<Dialpad onDigitPress={this.onDigitPress} hasDialAndDelete={false} />
|
||||
</div>
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,17 @@ import {MatrixCapabilities} from "matrix-widget-api";
|
||||
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
||||
import {ChevronFace} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import {IApp} from "../../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import Modal from "../../../Modal";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
app: IApp;
|
||||
@@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
let unpinButton;
|
||||
if (showUnpin) {
|
||||
const onUnpinClick = () => {
|
||||
WidgetStore.instance.unpinWidget(room.roomId, app.id);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -127,7 +127,8 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
console.info("Revoking permission for widget to load: " + app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[app.eventId] = false;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).catch(err => {
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
@@ -137,13 +138,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
|
||||
}
|
||||
|
||||
const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId);
|
||||
const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id);
|
||||
|
||||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -153,7 +154,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
let moveRightButton;
|
||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
@@ -701,6 +703,97 @@ class VerificationExplorer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component {
|
||||
static getLabel() {
|
||||
return _t("Active Widgets");
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
editWidget: null, // set to an IApp when editing
|
||||
};
|
||||
}
|
||||
|
||||
onWidgetStoreUpdate = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onQueryChange = (query) => {
|
||||
this.setState({query});
|
||||
};
|
||||
|
||||
onEditWidget = (widget) => {
|
||||
this.setState({editWidget: widget});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||
this.setState({editWidget: null});
|
||||
} else {
|
||||
this.props.onBack();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
}
|
||||
|
||||
render() {
|
||||
const room = this.props.room;
|
||||
|
||||
const editWidget = this.state.editWidget;
|
||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||
if (editWidget && widgets.includes(editWidget)) {
|
||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||
if (!stateEv) { // "should never happen"
|
||||
return <div>
|
||||
{_t("There was an error finding this widget.")}
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <SendCustomEvent
|
||||
onBack={this.onBack}
|
||||
room={room}
|
||||
forceStateEvent={true}
|
||||
inputs={{
|
||||
eventType: stateEv.getType(),
|
||||
evContent: JSON.stringify(stateEv.getContent(), null, '\t'),
|
||||
stateKey: stateEv.getStateKey(),
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
<FilteredList query={this.state.query} onChange={this.onQueryChange}>
|
||||
{widgets.map(w => {
|
||||
return <button
|
||||
className='mx_DevTools_RoomStateExplorer_button'
|
||||
key={w.url + w.eventId}
|
||||
onClick={() => this.onEditWidget(w)}
|
||||
>{w.url}</button>;
|
||||
})}
|
||||
</FilteredList>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
@@ -708,6 +801,7 @@ const Entries = [
|
||||
AccountDataExplorer,
|
||||
ServersInRoomList,
|
||||
VerificationExplorer,
|
||||
WidgetExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
|
||||
@@ -50,6 +50,10 @@ export default class ErrorDialog extends React.Component {
|
||||
button: null,
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
@@ -64,7 +68,7 @@ export default class ErrorDialog extends React.Component {
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onClick} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import {
|
||||
IHostSignupConfig,
|
||||
IPostmessage,
|
||||
IPostmessageResponseData,
|
||||
PostmessageAction,
|
||||
} from "./HostSignupDialogTypes";
|
||||
|
||||
const HOST_SIGNUP_KEY = "host_signup";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
completed: boolean;
|
||||
error: string;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
||||
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
private readonly config: IHostSignupConfig;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
completed: false,
|
||||
error: null,
|
||||
minimized: false,
|
||||
};
|
||||
|
||||
this.config = SdkConfig.get().hostSignup;
|
||||
}
|
||||
|
||||
private messageHandler = async (message: IPostmessage) => {
|
||||
if (!this.config.url.startsWith(message.origin)) {
|
||||
return;
|
||||
}
|
||||
switch (message.data.action) {
|
||||
case PostmessageAction.HostSignupAccountDetailsRequest:
|
||||
this.onAccountDetailsRequest();
|
||||
break;
|
||||
case PostmessageAction.Maximize:
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.Minimize:
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.SetupComplete:
|
||||
this.setState({
|
||||
completed: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.CloseDialog:
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private maximizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Maximize,
|
||||
});
|
||||
}
|
||||
|
||||
private minimizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Minimize,
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialog = async () => {
|
||||
window.removeEventListener("message", this.messageHandler);
|
||||
// Ensure we destroy the host signup persisted element
|
||||
PersistedElement.destroyElement("host_signup");
|
||||
// Finally clear the flag in
|
||||
return HostSignupStore.instance.setHostSignupActive(false);
|
||||
}
|
||||
|
||||
private onCloseClick = async () => {
|
||||
if (this.state.completed) {
|
||||
// We're done, close
|
||||
return this.closeDialog();
|
||||
} else {
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Confirm abort of host creation"),
|
||||
description: _t(
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
),
|
||||
button: _t("Abort"),
|
||||
onFinished: result => {
|
||||
if (result) {
|
||||
return this.closeDialog();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage = (message: IPostmessageResponseData) => {
|
||||
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
|
||||
}
|
||||
|
||||
private async sendAccountDetails() {
|
||||
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
if (!openIdToken || !openIdToken.access_token) {
|
||||
console.warn("Failed to connect to homeserver for OpenID token.")
|
||||
this.setState({
|
||||
completed: true,
|
||||
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.HostSignupAccountDetails,
|
||||
account: {
|
||||
accessToken: await MatrixClientPeg.get().getAccessToken(),
|
||||
name: OwnProfileStore.instance.displayName,
|
||||
openIdToken: openIdToken.access_token,
|
||||
serverName: await MatrixClientPeg.get().getDomain(),
|
||||
userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
termsAccepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onAccountDetailsDialogFinished = async (result) => {
|
||||
if (result) {
|
||||
return this.sendAccountDetails();
|
||||
}
|
||||
return this.closeDialog();
|
||||
}
|
||||
|
||||
private onAccountDetailsRequest = () => {
|
||||
const textComponent = (
|
||||
<>
|
||||
<p>
|
||||
{_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
"account to fetch verified email addresses. This data is not stored.", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{},
|
||||
{
|
||||
cookiePolicyLink: () => (
|
||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Cookie Policy")}
|
||||
</a>
|
||||
),
|
||||
privacyPolicyLink: () => (
|
||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Privacy Policy")}
|
||||
</a>
|
||||
),
|
||||
termsOfServiceLink: () => (
|
||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Terms of Service")}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("You should know"),
|
||||
description: textComponent,
|
||||
button: _t("Continue"),
|
||||
onFinished: this.onAccountDetailsDialogFinished,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (HostSignupStore.instance.isHostSignupActive) {
|
||||
// Run the close dialog actions if we're still active, otherwise good to go
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_HostSignup_persisted">
|
||||
<PersistedElement key={HOST_SIGNUP_KEY} persistKey={HOST_SIGNUP_KEY}>
|
||||
<div className={classNames({ "mx_Dialog_wrapper": !this.state.minimized })}>
|
||||
<div
|
||||
className={classNames("mx_Dialog",
|
||||
{
|
||||
"mx_HostSignupDialog_minimized": this.state.minimized,
|
||||
"mx_HostSignupDialog": !this.state.minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("%(hostSignupBrand)s Setup", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_HostSignup_maximize_button"
|
||||
onClick={this.maximizeDialog}
|
||||
aria-label={_t("Maximize dialog")}
|
||||
title={_t("Maximize dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
|
||||
<AccessibleButton
|
||||
onClick={this.minimizeDialog}
|
||||
className="mx_HostSignup_minimize_button"
|
||||
aria-label={_t("Minimize dialog")}
|
||||
title={_t("Minimize dialog")}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onCloseClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
aria-label={_t("Close dialog")}
|
||||
title={_t("Close dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div>
|
||||
{this.state.error}
|
||||
</div>
|
||||
}
|
||||
{!this.state.error &&
|
||||
<iframe
|
||||
src={this.config.url}
|
||||
ref={this.iframeRef}
|
||||
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PersistedElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum PostmessageAction {
|
||||
CloseDialog = "close_dialog",
|
||||
HostSignupAccountDetails = "host_signup_account_details",
|
||||
HostSignupAccountDetailsRequest = "host_signup_account_details_request",
|
||||
Minimize = "host_signup_minimize",
|
||||
Maximize = "host_signup_maximize",
|
||||
SetupComplete = "setup_complete",
|
||||
}
|
||||
|
||||
interface IAccountData {
|
||||
accessToken: string;
|
||||
name: string;
|
||||
openIdToken: string;
|
||||
serverName: string;
|
||||
userLocalpart: string;
|
||||
termsAccepted: boolean;
|
||||
}
|
||||
|
||||
export interface IPostmessageRequestData {
|
||||
action: PostmessageAction;
|
||||
}
|
||||
|
||||
export interface IPostmessageResponseData {
|
||||
action: PostmessageAction;
|
||||
account?: IAccountData;
|
||||
}
|
||||
|
||||
export interface IPostmessage {
|
||||
data: IPostmessageRequestData;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface IHostSignupConfig {
|
||||
brand: string;
|
||||
cookiePolicyUrl: string;
|
||||
domains: Array<string>;
|
||||
privacyPolicyUrl: string;
|
||||
termsOfServiceUrl: string;
|
||||
url: string;
|
||||
}
|
||||
@@ -41,12 +41,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
@@ -310,6 +312,9 @@ interface IInviteDialogProps {
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
call: MatrixCall,
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: string,
|
||||
}
|
||||
@@ -345,6 +350,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
|
||||
@@ -702,6 +709,29 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
});
|
||||
};
|
||||
|
||||
_transferCall = async () => {
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
errorText: _t("A call can only be transferred to a single user."),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (e) => {
|
||||
if (this.state.busy) return;
|
||||
const value = e.target.value.trim();
|
||||
@@ -1217,7 +1247,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
title = _t("Invite to this room");
|
||||
|
||||
if (identityServersEnabled) {
|
||||
@@ -1251,6 +1281,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
} else {
|
||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|
||||
@@ -35,13 +35,13 @@ import {
|
||||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
widgetRoomId?: string;
|
||||
sourceWidgetId: string;
|
||||
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
|
||||
public render() {
|
||||
const templated = this.widget.getCompleteUrl({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
widgetRoomId: this.props.widgetRoomId,
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
{_t("We recommend you change your password and Security Key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
|
||||
@@ -44,7 +44,8 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
@@ -73,6 +74,7 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
|
||||
@@ -151,13 +151,13 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
||||
|
||||
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
if (!valid && !this.state.defaultChosen) {
|
||||
this.fieldRef.current.focus();
|
||||
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(this.validatedConf);
|
||||
this.props.onFinished(this.state.defaultChosen ? this.defaultServer : this.validatedConf);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
export default class TextInputDialog extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -29,6 +30,7 @@ export default class TextInputDialog extends React.Component {
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
busyMessage: PropTypes.string, // pass _td string
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
@@ -40,6 +42,7 @@ export default class TextInputDialog extends React.Component {
|
||||
title: "",
|
||||
value: "",
|
||||
description: "",
|
||||
busyMessage: _td("Loading..."),
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
@@ -51,6 +54,7 @@ export default class TextInputDialog extends React.Component {
|
||||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
busy: false,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
@@ -66,11 +70,13 @@ export default class TextInputDialog extends React.Component {
|
||||
onOk = async ev => {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
this.setState({ busy: true });
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +131,8 @@ export default class TextInputDialog extends React.Component {
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
primaryButton={this.state.busy ? _t(this.props.busyMessage) : this.props.button}
|
||||
disabled={this.state.busy}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
|
||||
@@ -70,26 +70,26 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
return (
|
||||
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("A widget would like to verify your identity")}>
|
||||
title={_t("Allow this widget to verify your identity")}>
|
||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||
</p>
|
||||
<p className="text-muted">
|
||||
{/* cheap trim to just get the path */}
|
||||
{this.props.widget.templateUrl.split("?")[0].split("#")[0]}
|
||||
</p>
|
||||
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember my selection for this widget")} />
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Allow")}
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._onAllow}
|
||||
cancelButton={_t("Deny")}
|
||||
onCancel={this._onDeny}
|
||||
additive={
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.rememberSelection}
|
||||
toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember this")} />}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@@ -199,11 +199,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||
} else if (this.state.recoveryKeyCorrect) {
|
||||
return _t("Looks good!");
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
return _t("Wrong Recovery Key");
|
||||
return _t("Wrong Security Key");
|
||||
} else if (this.state.recoveryKeyValid === null) {
|
||||
return '';
|
||||
} else {
|
||||
return _t("Invalid Recovery Key");
|
||||
return _t("Invalid Security Key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct recovery passphrase.",
|
||||
"Please verify that you entered the correct Security Phrase.",
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
@@ -298,7 +298,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
|
||||
<Field
|
||||
type="text"
|
||||
type="password"
|
||||
label={_t('Security Key')}
|
||||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
|
||||
@@ -34,6 +34,7 @@ import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -96,6 +97,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else if (this.props.tokenLogin) {
|
||||
// We are hoping the grace period is active
|
||||
await makeRequest({});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
@@ -144,6 +148,12 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
|
||||
@@ -297,19 +297,19 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
} else if (this.state.restoreError) {
|
||||
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
|
||||
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
|
||||
title = _t("Recovery key mismatch");
|
||||
title = _t("Security Key mismatch");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
"Backup could not be decrypted with this Security Key: " +
|
||||
"please verify that you entered the correct Security Key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Incorrect recovery passphrase");
|
||||
title = _t("Incorrect Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery passphrase: " +
|
||||
"please verify that you entered the correct recovery passphrase.",
|
||||
"Backup could not be decrypted with this Security Phrase: " +
|
||||
"please verify that you entered the correct Security Phrase.",
|
||||
)}</p>
|
||||
</div>;
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter recovery passphrase");
|
||||
title = _t("Enter Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: you should only set up key backup " +
|
||||
@@ -351,7 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery passphrase.",
|
||||
"messaging by entering your Security Phrase.",
|
||||
)}</p>
|
||||
|
||||
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
@@ -371,8 +371,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
/>
|
||||
</form>
|
||||
{_t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"If you've forgotten your Security Phrase you can "+
|
||||
"<button1>use your Security Key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>"
|
||||
, {}, {
|
||||
button1: s => <AccessibleButton className="mx_linkButton"
|
||||
@@ -390,7 +390,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter recovery key");
|
||||
title = _t("Enter Security Key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
@@ -399,11 +399,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid Security Key!")}
|
||||
</div>;
|
||||
} else {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid Security Key")}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery key.",
|
||||
"messaging by entering your Security Key.",
|
||||
)}</p>
|
||||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
@@ -434,7 +434,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
/>
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your recovery key you can "+
|
||||
"If you've forgotten your Security Key you can "+
|
||||
"<button>set up new recovery options</button>"
|
||||
, {}, {
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
|
||||
@@ -33,7 +33,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement, {getPersistKey} from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
@@ -240,7 +239,8 @@ export default class AppTile extends React.Component {
|
||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[this.props.app.eventId] = true;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).then(() => {
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
|
||||
// Fetch a token for the integration manager, now that we're allowed to
|
||||
|
||||
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "..//dialogs/BaseDialog"
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL;
|
||||
}
|
||||
|
||||
export enum Tabs {
|
||||
Screens = "screens",
|
||||
Windows = "windows",
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourceIProps {
|
||||
source: DesktopCapturerSource;
|
||||
onSelect(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
this.props.onSelect(this.props.source);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_desktopCapturerSourcePicker_stream_button"
|
||||
title={this.props.source.name}
|
||||
onClick={this.onClick} >
|
||||
<img
|
||||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourcePickerIState {
|
||||
selectedTab: Tabs;
|
||||
sources: Array<DesktopCapturerSource>;
|
||||
}
|
||||
export interface DesktopCapturerSourcePickerIProps {
|
||||
onFinished(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export default class DesktopCapturerSourcePicker extends React.Component<
|
||||
DesktopCapturerSourcePickerIProps,
|
||||
DesktopCapturerSourcePickerIState
|
||||
> {
|
||||
interval;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedTab: Tabs.Screens,
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// setInterval() first waits and then executes, therefore
|
||||
// we call getDesktopCapturerSources() here without any delay.
|
||||
// Otherwise the dialog would be left empty for some time.
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
|
||||
// We update the sources every 500ms to get newer thumbnails
|
||||
this.interval = setInterval(async () => {
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onSelect = (source) => {
|
||||
this.props.onFinished(source);
|
||||
}
|
||||
|
||||
onScreensClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Screens});
|
||||
}
|
||||
|
||||
onWindowsClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Windows});
|
||||
}
|
||||
|
||||
onCloseClick = (ev) => {
|
||||
this.props.onFinished(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
let sources;
|
||||
if (this.state.selectedTab === Tabs.Screens) {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("screen");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
} else {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("window");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
}
|
||||
|
||||
const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel";
|
||||
const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : "");
|
||||
const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : "");
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_desktopCapturerSourcePicker"
|
||||
onFinished={this.onCloseClick}
|
||||
title={_t("Share your screen")}
|
||||
>
|
||||
<div className="mx_desktopCapturerSourcePicker_tabLabels">
|
||||
<AccessibleButton
|
||||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{_t("Screens")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{_t("Windows")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
{ sources }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export default class EditableItemList extends React.Component {
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
||||
@@ -15,14 +15,15 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useRef, useState} from 'react';
|
||||
import {EventType} from 'matrix-js-sdk/src/@types/event';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useTimeout} from "../../../hooks/useTimeout";
|
||||
import Analytics from "../../../Analytics";
|
||||
import CountlyAnalytics from '../../../CountlyAnalytics';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
export const AVATAR_SIZE = 52;
|
||||
|
||||
@@ -50,6 +51,11 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
||||
|
||||
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
|
||||
|
||||
const {room} = useContext(RoomContext);
|
||||
const canSetAvatar = room?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
|
||||
if (!canSetAvatar) return <React.Fragment>{ children }</React.Fragment>;
|
||||
|
||||
const visible = !!label && (hover || show);
|
||||
return <React.Fragment>
|
||||
<input
|
||||
type="file"
|
||||
@@ -82,11 +88,13 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
||||
>
|
||||
{ children }
|
||||
|
||||
<Tooltip
|
||||
label={label}
|
||||
visible={!!label && (hover || show)}
|
||||
forceOnRight
|
||||
/>
|
||||
<div className={classNames("mx_Tooltip", {
|
||||
"mx_Tooltip_visible": visible,
|
||||
"mx_Tooltip_invisible": !visible,
|
||||
})}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
{ label }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import ResizeObserver from 'resize-observer-polyfill';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
@@ -61,6 +62,9 @@ export default class PersistedElement extends React.Component {
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
|
||||
// z-index for the element. Defaults to 9.
|
||||
zIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -165,6 +169,7 @@ export default class PersistedElement extends React.Component {
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
Object.assign(child.style, {
|
||||
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
|
||||
position: 'absolute',
|
||||
top: parentRect.top + 'px',
|
||||
left: parentRect.left + 'px',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -23,23 +23,11 @@ import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+]).*?)(?=\/|\?|$)/;
|
||||
|
||||
class Pill extends React.Component {
|
||||
static isPillUrl(url) {
|
||||
return !!getPrimaryPermalinkEntity(url);
|
||||
}
|
||||
|
||||
static isMessagePillUrl(url) {
|
||||
return !!REGEX_LOCAL_PERMALINK.exec(url);
|
||||
}
|
||||
|
||||
static roomNotifPos(text) {
|
||||
return text.indexOf("@room");
|
||||
}
|
||||
@@ -56,7 +44,7 @@ class Pill extends React.Component {
|
||||
static propTypes = {
|
||||
// The Type of this Pill. If url is given, this is auto-detected.
|
||||
type: PropTypes.string,
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
// The URL to pillify (no validation is done)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
inMessage: PropTypes.bool,
|
||||
@@ -90,12 +78,9 @@ class Pill extends React.Component {
|
||||
|
||||
if (nextProps.url) {
|
||||
if (nextProps.inMessage) {
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
|
||||
|
||||
resourceId = matrixToMatch[1]; // The room/user ID
|
||||
prefix = matrixToMatch[2]; // The first character of prefix
|
||||
const parts = parseAppLocalLink(nextProps.url);
|
||||
resourceId = parts.primaryEntityId; // The room/user ID
|
||||
prefix = parts.sigil; // The first character of prefix
|
||||
} else {
|
||||
resourceId = getPrimaryPermalinkEntity(nextProps.url);
|
||||
prefix = resourceId ? resourceId[0] : undefined;
|
||||
|
||||
@@ -15,19 +15,40 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { chunk } from "lodash";
|
||||
import classNames from "classnames";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import classNames from "classnames";
|
||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
}
|
||||
|
||||
const getIcon = (brand: IdentityProviderBrand | string) => {
|
||||
switch (brand) {
|
||||
case IdentityProviderBrand.Apple:
|
||||
return require(`../../../../res/img/element-icons/brands/apple.svg`);
|
||||
case IdentityProviderBrand.Facebook:
|
||||
return require(`../../../../res/img/element-icons/brands/facebook.svg`);
|
||||
case IdentityProviderBrand.Github:
|
||||
return require(`../../../../res/img/element-icons/brands/github.svg`);
|
||||
case IdentityProviderBrand.Gitlab:
|
||||
return require(`../../../../res/img/element-icons/brands/gitlab.svg`);
|
||||
case IdentityProviderBrand.Google:
|
||||
return require(`../../../../res/img/element-icons/brands/google.svg`);
|
||||
case IdentityProviderBrand.Twitter:
|
||||
return require(`../../../../res/img/element-icons/brands/twitter.svg`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
matrixClient,
|
||||
loginType,
|
||||
@@ -37,7 +58,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
mini,
|
||||
...props
|
||||
}) => {
|
||||
const kind = primary ? "primary" : "primary_outline";
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
|
||||
const onClick = () => {
|
||||
@@ -45,30 +65,35 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
};
|
||||
|
||||
let icon;
|
||||
if (typeof idp?.icon === "string" && (idp.icon.startsWith("mxc://") || idp.icon.startsWith("https://"))) {
|
||||
icon = <img
|
||||
src={matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true)}
|
||||
height="24"
|
||||
width="24"
|
||||
alt={label}
|
||||
/>;
|
||||
let brandClass;
|
||||
const brandIcon = idp ? getIcon(idp.brand) : null;
|
||||
if (brandIcon) {
|
||||
const brandName = idp.brand.split(".").pop();
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_SSOButton", {
|
||||
[brandClass]: brandClass,
|
||||
mx_SSOButton_mini: mini,
|
||||
mx_SSOButton_default: !idp,
|
||||
mx_SSOButton_primary: primary,
|
||||
});
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
{ label }
|
||||
</AccessibleButton>
|
||||
@@ -83,6 +108,8 @@ interface IProps {
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
if (providers.length < 2) {
|
||||
@@ -97,17 +124,24 @@ const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAf
|
||||
</div>;
|
||||
}
|
||||
|
||||
const rows = Math.ceil(providers.length / MAX_PER_ROW);
|
||||
const size = Math.ceil(providers.length / rows);
|
||||
|
||||
return <div className="mx_SSOButtons">
|
||||
{ providers.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
{ chunk(providers, size).map(chunk => (
|
||||
<div key={chunk[0].id} className="mx_SSOButtons_row">
|
||||
{ chunk.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
||||
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import HostSignupDialog from "../dialogs/HostSignupDialog";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
const HostSignupContainer = () => {
|
||||
const [isActive, setIsActive] = useState(HostSignupStore.instance.isHostSignupActive);
|
||||
useEventEmitter(HostSignupStore.instance, UPDATE_EVENT, () => {
|
||||
setIsActive(HostSignupStore.instance.isHostSignupActive);
|
||||
});
|
||||
|
||||
return <div className="mx_HostSignupContainer">
|
||||
{isActive &&
|
||||
<HostSignupDialog />
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default HostSignupContainer
|
||||
@@ -19,6 +19,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@@ -33,10 +35,15 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||
const url = this.props.mxEvent.getContent()['url'];
|
||||
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
|
||||
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const widgetId = this.props.mxEvent.getStateKey();
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find(w => w.id === widgetId);
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
|
||||
joinCopy = _t('Join the conference from the room information card on the right');
|
||||
} else if (!widget) {
|
||||
joinCopy = null;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
|
||||
@@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
|
||||
}
|
||||
|
||||
_applyFormatting() {
|
||||
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
|
||||
this.activateSpoilers([this._content.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
@@ -91,29 +92,140 @@ export default class TextualBody extends React.Component {
|
||||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (blocks.length > 0) {
|
||||
// Handle expansion and add buttons
|
||||
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
|
||||
if (pres.length > 0) {
|
||||
for (let i = 0; i < pres.length; i++) {
|
||||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
this._handleCodeBlockExpansion(pres[i]);
|
||||
this._addCodeExpansionButton(div, pres[i]);
|
||||
this._addCodeCopyButton(div);
|
||||
if (showLineNumbers) {
|
||||
this._addLineNumbers(pres[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Highlight code
|
||||
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
// If the code already has the hljs class we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (codes[i].className.includes("hljs")) continue;
|
||||
this._highlightCode(codes[i]);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
} else {
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
}
|
||||
|
||||
button.onclick = async () => {
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
pre.className = "";
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
} else {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
}
|
||||
|
||||
// By expanding/collapsing we changed
|
||||
// the height, therefore we call this
|
||||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_addCodeCopyButton(div) {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
|
||||
|
||||
// Check if expansion button exists. If so
|
||||
// we put the copy button to the bottom
|
||||
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
|
||||
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
|
||||
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_wrapInDiv(pre) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
pre.parentNode.replaceChild(div, pre);
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(pre);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
_handleCodeBlockExpansion(pre) {
|
||||
if (!SettingsStore.getValue("expandCodeByDefault")) {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
}
|
||||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.split(/\n/).length;
|
||||
// Iterate through lines starting with 1 (number of the first line is 1)
|
||||
for (let i = 1; i < number; i++) {
|
||||
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
_highlightCode(code) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = code.className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,38 +366,6 @@ export default class TextualBody extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
onCancelClick = event => {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
@@ -410,7 +490,10 @@ export default class TextualBody extends React.Component {
|
||||
ref: this._content,
|
||||
});
|
||||
if (this.props.replacingEventId) {
|
||||
body = [body, this._renderEditedMarker()];
|
||||
body = <>
|
||||
{body}
|
||||
{this._renderEditedMarker()}
|
||||
</>;
|
||||
}
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
|
||||
@@ -33,6 +33,7 @@ interface IProps {
|
||||
previousPhase?: RightPanelPhases;
|
||||
closeLabel?: string;
|
||||
onClose?(): void;
|
||||
refireParams?;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
@@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
|
||||
withoutScrollContainer,
|
||||
previousPhase,
|
||||
children,
|
||||
refireParams,
|
||||
}) => {
|
||||
let backButton;
|
||||
if (previousPhase) {
|
||||
@@ -63,6 +65,7 @@ const BaseCard: React.FC<IProps> = ({
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
};
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
|
||||
|
||||
@@ -37,13 +37,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -76,8 +77,9 @@ export const useWidgets = (room: Room) => {
|
||||
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
||||
}, [room]);
|
||||
|
||||
useEffect(updateApps, [room]);
|
||||
useEffect(updateApps, [room, updateApps]);
|
||||
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
|
||||
|
||||
return apps;
|
||||
};
|
||||
@@ -102,10 +104,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const togglePin = isPinned
|
||||
? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
|
||||
: () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); };
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
@@ -120,7 +122,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
/>;
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
@@ -184,9 +186,18 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
||||
}
|
||||
};
|
||||
|
||||
let copyLayoutBtn = null;
|
||||
if (apps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||
copyLayoutBtn = (
|
||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||
{ _t("Set my room layout for everyone") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
|
||||
{ copyLayoutBtn }
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
|
||||
@@ -60,6 +60,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
@@ -1534,6 +1535,24 @@ const UserInfo: React.FC<Props> = ({
|
||||
|
||||
const classes = ["mx_UserInfo"];
|
||||
|
||||
let refireParams;
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
}
|
||||
|
||||
let content;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
@@ -1553,19 +1572,13 @@ const UserInfo: React.FC<Props> = ({
|
||||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
onClose={onClose}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
let closeLabel = undefined;
|
||||
if (phase === RightPanelPhases.EncryptionPanel) {
|
||||
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
|
||||
@@ -1581,6 +1594,7 @@ const UserInfo: React.FC<Props> = ({
|
||||
onClose={onClose}
|
||||
closeLabel={closeLabel}
|
||||
previousPhase={previousPhase}
|
||||
refireParams={refireParams}
|
||||
>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
|
||||
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {useWidgets} from "./RoomSummaryCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useWidgets } from "./RoomSummaryCard";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
||||
@@ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
topic: this.state.originalTopic,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
@@ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, '');
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||
}
|
||||
|
||||
if (this.state.originalTopic !== this.state.topic) {
|
||||
@@ -120,17 +125,21 @@ export default class RoomProfileSettings extends React.Component {
|
||||
};
|
||||
|
||||
_onDisplayNameChanged = (e) => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({displayName: e.target.value});
|
||||
if (this.state.originalDisplayName === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onTopicChanged = (e) => {
|
||||
this.setState({
|
||||
topic: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({topic: e.target.value});
|
||||
if (this.state.originalTopic === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onAvatarChanged = (e) => {
|
||||
@@ -158,6 +167,33 @@ export default class RoomProfileSettings extends React.Component {
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
|
||||
let profileSettingsButtons;
|
||||
if (
|
||||
this.state.canSetName ||
|
||||
this.state.canSetTopic ||
|
||||
this.state.canSetAvatar
|
||||
) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
@@ -172,7 +208,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||
<Field label={_t("Room Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
|
||||
<Field id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
</div>
|
||||
@@ -183,22 +219,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ profileSettingsButtons }
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,12 +28,13 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -62,13 +63,13 @@ export default class AppsDrawer extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
@@ -102,11 +103,10 @@ export default class AppsDrawer extends React.Component {
|
||||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
// persist to localStorage
|
||||
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||
this.state.apps.map(app => app.id),
|
||||
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
]));
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
);
|
||||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
@@ -128,8 +128,6 @@ export default class AppsDrawer extends React.Component {
|
||||
this._loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -147,24 +145,16 @@ export default class AppsDrawer extends React.Component {
|
||||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
try {
|
||||
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||
// Every app was included in the last split, reuse the last sizes
|
||||
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
|
||||
sizes.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// this is expected
|
||||
}
|
||||
|
||||
if (this.state.apps) {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
} else if (this.state.apps) {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
distributors.forEach(d => d.item.clearSize());
|
||||
distributors.forEach(d => d.start());
|
||||
@@ -190,7 +180,7 @@ export default class AppsDrawer extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
this.setState({
|
||||
@@ -248,10 +238,11 @@ export default class AppsDrawer extends React.Component {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
id={"apps-drawer_" + this.props.room.roomId}
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
@@ -272,7 +263,7 @@ export default class AppsDrawer extends React.Component {
|
||||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
id,
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
className,
|
||||
@@ -281,7 +272,24 @@ const PersistentVResizer = ({
|
||||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
defaultHeight = clamp(defaultHeight, 0, 100);
|
||||
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
|
||||
} else {
|
||||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
|
||||
@@ -562,7 +562,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||
|
||||
private async tabCompleteName(event: React.KeyboardEvent) {
|
||||
try {
|
||||
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
const {model} = this.props;
|
||||
const caret = this.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
|
||||
@@ -37,6 +37,7 @@ import {E2E_STATE} from "./E2EIcon";
|
||||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
@@ -65,6 +66,7 @@ const stateEventTileTypes = {
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
|
||||
@@ -109,9 +109,12 @@ function HangupButton(props) {
|
||||
|
||||
dis.dispatch({
|
||||
action,
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
// hangup the call for this room. NB. We use the room in props as the room ID
|
||||
// as call.roomId may be the 'virtual room', and the dispatch actions always
|
||||
// use the user-facing room (there was a time when we deliberately used
|
||||
// call.roomId and *not* props.roomId, but that was for the old
|
||||
// style Freeswitch conference calls and those times are gone.)
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -423,7 +426,8 @@ export default class MessageComposer extends React.Component {
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
||||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs'>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
|
||||
@@ -46,6 +46,7 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
@@ -89,10 +90,44 @@ interface ITagAesthetics {
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
||||
const TAG_AESTHETICS: {
|
||||
interface ITagAestheticsMap {
|
||||
// @ts-ignore - TS wants this to be a string but we know better
|
||||
[tagId: TagID]: ITagAesthetics;
|
||||
} = {
|
||||
}
|
||||
|
||||
// If we have no dialer support, we just show the create chat dialog
|
||||
const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
};
|
||||
|
||||
// If we have dialer support, show a context menu so the user can pick between
|
||||
// the dialer and the create chat dialog
|
||||
const dmAddRoomContextMenu = (onFinished: () => void) => {
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start a Conversation")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.dispatch({action: "view_create_chat"});
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Open dial pad")}
|
||||
iconClassName="mx_RoomList_iconDialpad"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.OpenDialPad);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
};
|
||||
|
||||
const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
isInvite: true,
|
||||
@@ -108,9 +143,8 @@ const TAG_AESTHETICS: {
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
},
|
||||
// Either onAddRoom or addRoomContextMenu are set depending on whether we
|
||||
// have dialer support.
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
@@ -178,6 +212,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private tagAesthetics: ITagAestheticsMap;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -187,6 +222,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
};
|
||||
|
||||
// shallow-copy from the template as we need to make modifications to it
|
||||
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
||||
this.updateDmAddRoomAction();
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
@@ -202,6 +241,17 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
||||
}
|
||||
|
||||
private updateDmAddRoomAction() {
|
||||
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu;
|
||||
} else {
|
||||
dmTagAesthetics.onAddRoom = dmOnAddRoom;
|
||||
}
|
||||
|
||||
this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics;
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoomDelta) {
|
||||
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
||||
@@ -214,6 +264,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
});
|
||||
}
|
||||
} else if (payload.action === Action.PstnSupportUpdated) {
|
||||
this.updateDmAddRoomAction();
|
||||
this.updateLists();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -355,7 +408,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
|
||||
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
||||
? customTagAesthetics(orderedTagId)
|
||||
: TAG_AESTHETICS[orderedTagId];
|
||||
: this.tagAesthetics[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
components.push(<RoomSublist
|
||||
@@ -402,8 +455,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||
<AccessibleButton
|
||||
|
||||
@@ -29,7 +29,7 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
@@ -51,7 +51,6 @@ import IconizedContextMenu, {
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -99,12 +98,23 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room) => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
@@ -128,6 +138,26 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(prevProps.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
|
||||
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
@@ -140,11 +170,18 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
public componentWillUnmount() {
|
||||
if (this.props.room) {
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
||||
@@ -156,13 +156,14 @@ export default class SendMessageComposer extends React.Component {
|
||||
this.onVerticalArrow(event, true);
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
this.onVerticalArrow(event, false);
|
||||
} else if (this._prepareToEncrypt) {
|
||||
this._prepareToEncrypt();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
} else if (this._prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,6 +403,7 @@ export default class SendMessageComposer extends React.Component {
|
||||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -264,7 +264,7 @@ export default class Stickerpicker extends React.Component {
|
||||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
|
||||
<AppTile
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class WhoIsTypingTile extends React.Component {
|
||||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
if (room && room.roomId === this.props.room.roomId) {
|
||||
if (room?.roomId === this.props.room?.roomId) {
|
||||
const userId = event.getSender();
|
||||
// remove user from usersTyping
|
||||
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
||||
|
||||
@@ -65,7 +65,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
||||
|
||||
const avatarClasses = classNames({
|
||||
"mx_AvatarSetting_avatar": true,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("views.settings.BridgeTile")
|
||||
export default class BridgeTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
}
|
||||
|
||||
_toggleVisible() {
|
||||
this.setState({
|
||||
visible: !this.state.visible,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.ev.getContent();
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
const networkName = network ? network.displayname || network.id : protocolName;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = _t("This bridge was provisioned by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
}
|
||||
|
||||
const bot = _t("This bridge is managed by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(this.props.ev.getSender())}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div class="noProtocolIcon"></div>;
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
const metadataClassname = "metadata" + (this.state.visible ? " visible" : "");
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
<span>{_t("Workspace: %(networkName)s", {networkName})}</span>
|
||||
<span className="channel">{_t("Channel: %(channelName)s", {channelName})}</span>
|
||||
</p>
|
||||
<p className={metadataClassname}>
|
||||
{creator} {bot}
|
||||
</p>
|
||||
<AccessibleButton className="mx_showMore" kind="secondary" onClick={this._toggleVisible.bind(this)}>
|
||||
{ this.state.visible ? _t("Show less") : _t("Show more") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
||||
167
src/components/views/settings/BridgeTile.tsx
Normal file
167
src/components/views/settings/BridgeTile.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
interface IProps {
|
||||
ev: MatrixEvent;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should match https://github.com/matrix-org/matrix-doc/blob/hs/msc-bridge-inf/proposals/2346-bridge-info-state-event.md#mbridge
|
||||
*/
|
||||
interface IBridgeStateEvent {
|
||||
bridgebot: string;
|
||||
creator?: string;
|
||||
protocol: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
network?: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
channel: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class BridgeTile extends React.PureComponent<IProps> {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const content: IBridgeStateEvent = this.props.ev.getContent();
|
||||
// Validate
|
||||
if (!content.channel?.id || !content.protocol?.id) {
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} has missing content. Tile will not render`);
|
||||
return null;
|
||||
}
|
||||
if (!content.bridgebot) {
|
||||
// Bridgebot was not required previously, so in order to not break rooms we are allowing
|
||||
// the sender to be used in place. When the proposal is merged, this should be removed.
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which`
|
||||
+ "is deprecated behaviour. Using sender for now.");
|
||||
content.bridgebot = this.props.ev.getSender();
|
||||
}
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
}
|
||||
|
||||
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.bridgebot)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar_url) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar_url, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div className="noProtocolIcon"></div>;
|
||||
}
|
||||
let networkItem = null;
|
||||
if (network) {
|
||||
const networkName = network.displayname || network.id;
|
||||
let networkLink = <span>{networkName}</span>;
|
||||
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
|
||||
networkLink = <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
|
||||
}
|
||||
networkItem = _t("Workspace: <networkLink/>", {}, {
|
||||
networkLink: () => networkLink,
|
||||
});
|
||||
}
|
||||
|
||||
let channelLink = <span>{channelName}</span>;
|
||||
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
|
||||
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
{networkItem}
|
||||
<span className="channel">{_t("Channel: <channelLink/>", {}, {
|
||||
channelLink: () => channelLink,
|
||||
})}</span>
|
||||
</p>
|
||||
<ul className="metadata">
|
||||
{creator} {bot}
|
||||
</ul>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
||||
@@ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
@@ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component {
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
|
||||
@@ -424,7 +424,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||
<p>{_t(
|
||||
"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.",
|
||||
"unique Security Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
<details>
|
||||
|
||||
@@ -35,6 +35,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
'MessageComposerInput.surroundWith',
|
||||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
@@ -47,12 +48,15 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||
'alwaysShowTimestamps',
|
||||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
||||
static GENERAL_SETTINGS = [
|
||||
|
||||
@@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, CSSProperties, ReactNode } from 'react';
|
||||
import React, { createRef, CSSProperties } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
@@ -27,9 +27,10 @@ import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
import {aboveLeftOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||
import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
@@ -60,6 +61,7 @@ interface IState {
|
||||
callState: CallState,
|
||||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
showDialpad: boolean,
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
@@ -102,6 +104,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private contentRef = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
@@ -115,6 +118,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
callState: this.props.call.state,
|
||||
controlsVisible: true,
|
||||
showMoreMenu: false,
|
||||
showDialpad: false,
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
@@ -208,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -226,7 +231,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private showControls() {
|
||||
if (this.state.showMoreMenu) return;
|
||||
if (this.state.showMoreMenu || this.state.showDialpad) return;
|
||||
|
||||
if (!this.state.controlsVisible) {
|
||||
this.setState({
|
||||
@@ -239,6 +244,29 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private onDialpadClick = () => {
|
||||
if (!this.state.showDialpad) {
|
||||
if (this.controlsHideTimer) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
this.controlsHideTimer = null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showDialpad: true,
|
||||
controlsVisible: true,
|
||||
});
|
||||
} else {
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
|
||||
this.setState({
|
||||
showDialpad: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onMicMuteClick = () => {
|
||||
const newVal = !this.state.micMuted;
|
||||
|
||||
@@ -265,6 +293,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialpad = () => {
|
||||
this.setState({
|
||||
showDialpad: false,
|
||||
});
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private closeContextMenu = () => {
|
||||
this.setState({
|
||||
showMoreMenu: false,
|
||||
@@ -306,37 +341,52 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSecondaryRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.secondaryCall.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
}
|
||||
|
||||
private onSecondaryCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.secondaryCall.roomId);
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.props.call.roomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||
const callRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
const callRoom = client.getRoom(callRoomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||
|
||||
let dialPad;
|
||||
let contextMenu;
|
||||
|
||||
if (this.state.showDialpad) {
|
||||
dialPad = <DialpadContextMenu
|
||||
{...alwaysAboveRightOf(
|
||||
this.dialpadButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
onFinished={this.closeDialpad}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (this.state.showMoreMenu) {
|
||||
contextMenu = <CallContextMenu
|
||||
{...aboveLeftOf(
|
||||
{...alwaysAboveLeftOf(
|
||||
this.contextMenuButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
@@ -384,8 +434,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
onClick={this.onVidMuteClick}
|
||||
/> : null;
|
||||
|
||||
// The 'more' button actions are only relevant in a connected call
|
||||
// The dial pad & 'more' button actions are only relevant in a connected call
|
||||
// When not connected, we have to put something there to make the flexbox alignment correct
|
||||
const dialpadButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
isExpanded={this.state.showDialpad}
|
||||
/> : <div className="mx_CallView_callControls_button mx_CallView_callControls_button_dialpad_hidden" />;
|
||||
|
||||
const contextMenuButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
@@ -396,7 +453,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
// in the near future, the dial pad button will go on the left. For now, it's the nothing button
|
||||
// because something needs to have margin-right: auto to make the alignment correct.
|
||||
const callControls = <div className={callControlsClasses}>
|
||||
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
||||
{dialpadButton}
|
||||
<AccessibleButton
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
@@ -406,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: callRoomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -423,7 +480,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
|
||||
let onHoldText = null;
|
||||
if (this.state.isRemoteOnHold) {
|
||||
onHoldText = _t("You held the call <a>Resume</a>", {}, {
|
||||
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
|
||||
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
|
||||
onHoldText = _t(holdString, {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
@@ -435,6 +494,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldContent = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
@@ -453,6 +513,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
if (!this.state.vidMuted) {
|
||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
||||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
@@ -468,7 +531,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
|
||||
{localVideoFeed}
|
||||
{onHoldContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
@@ -478,20 +541,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
let secondaryCallAvatar: ReactNode;
|
||||
|
||||
if (this.props.secondaryCall) {
|
||||
const secAvatarSize = this.props.pipMode ? 40 : 100;
|
||||
secondaryCallAvatar = <div className="mx_CallView_voice_secondaryAvatarContainer"
|
||||
style={{width: secAvatarSize, height: secAvatarSize}}
|
||||
>
|
||||
<RoomAvatar
|
||||
room={secCallRoom}
|
||||
height={secAvatarSize}
|
||||
width={secAvatarSize}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
@@ -502,7 +551,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
{secondaryCallAvatar}
|
||||
</div>
|
||||
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||
{callControls}
|
||||
@@ -546,7 +594,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{_t("%(name)s paused", { name: secCallRoom.name })}
|
||||
{_t("%(name)s on hold", { name: secCallRoom.name })}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
@@ -571,6 +619,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
return <div className={"mx_CallView " + myClassName}>
|
||||
{header}
|
||||
{contentView}
|
||||
{dialPad}
|
||||
{contextMenu}
|
||||
</div>;
|
||||
}
|
||||
|
||||
85
src/components/views/voip/DialPad.tsx
Normal file
85
src/components/views/voip/DialPad.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
|
||||
|
||||
enum DialPadButtonKind {
|
||||
Digit,
|
||||
Delete,
|
||||
Dial,
|
||||
}
|
||||
|
||||
interface IButtonProps {
|
||||
kind: DialPadButtonKind;
|
||||
digit?: string;
|
||||
onButtonPress: (string) => void;
|
||||
}
|
||||
|
||||
class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||
onClick = () => {
|
||||
this.props.onButtonPress(this.props.digit);
|
||||
}
|
||||
|
||||
render() {
|
||||
switch (this.props.kind) {
|
||||
case DialPadButtonKind.Digit:
|
||||
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
|
||||
{this.props.digit}
|
||||
</AccessibleButton>;
|
||||
case DialPadButtonKind.Delete:
|
||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_deleteButton"
|
||||
onClick={this.onClick}
|
||||
/>;
|
||||
case DialPadButtonKind.Dial:
|
||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onDigitPress: (string) => void;
|
||||
hasDialAndDelete: boolean;
|
||||
onDeletePress?: (string) => void;
|
||||
onDialPress?: (string) => void;
|
||||
}
|
||||
|
||||
export default class Dialpad extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
const buttonNodes = [];
|
||||
|
||||
for (const button of BUTTONS) {
|
||||
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
|
||||
digit={button} onButtonPress={this.props.onDigitPress}
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.hasDialAndDelete) {
|
||||
buttonNodes.push(<DialPadButton key="del" kind={DialPadButtonKind.Delete}
|
||||
onButtonPress={this.props.onDeletePress}
|
||||
/>);
|
||||
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
|
||||
onButtonPress={this.props.onDialPress}
|
||||
/>);
|
||||
}
|
||||
|
||||
return <div className="mx_DialPad">
|
||||
{buttonNodes}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
112
src/components/views/voip/DialPadModal.tsx
Normal file
112
src/components/views/voip/DialPadModal.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import DialPad from './DialPad';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: '',
|
||||
}
|
||||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.setState({value: ev.target.value});
|
||||
}
|
||||
|
||||
onFormSubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.onDialPress();
|
||||
}
|
||||
|
||||
onDigitPress = (digit) => {
|
||||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
if (this.state.value.length === 0) return;
|
||||
this.setState({value: this.state.value.slice(0, -1)});
|
||||
}
|
||||
|
||||
onDialPress = async () => {
|
||||
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
|
||||
'm.id.phone': this.state.value,
|
||||
});
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||
title: _t("Unable to look up phone number"),
|
||||
description: _t("There was an error looking up the phone number"),
|
||||
});
|
||||
}
|
||||
const userId = results[0].userid;
|
||||
|
||||
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="mx_DialPadModal">
|
||||
<div className="mx_DialPadModal_header">
|
||||
<div>
|
||||
<span className="mx_DialPadModal_title">{_t("Dial pad")}</span>
|
||||
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
|
||||
</div>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<Field className="mx_DialPadModal_field" id="dialpad_number"
|
||||
value={this.state.value} autoFocus={true}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="mx_DialPadModal_horizSep" />
|
||||
<div className="mx_DialPadModal_dialPad">
|
||||
<DialPad hasDialAndDelete={true}
|
||||
onDigitPress={this.onDigitPress}
|
||||
onDeletePress={this.onDeletePress}
|
||||
onDialPress={this.onDialPress}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
||||
@@ -94,4 +94,16 @@ export enum Action {
|
||||
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
|
||||
*/
|
||||
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
|
||||
|
||||
/**
|
||||
* Opens the modal dial pad
|
||||
*/
|
||||
OpenDialPad = "open_dial_pad",
|
||||
|
||||
/**
|
||||
* Fired when CallHandler has checked for PSTN protocol support
|
||||
* payload: none
|
||||
* XXX: Is an action the right thing for this?
|
||||
*/
|
||||
PstnSupportUpdated = "pstn_support_updated",
|
||||
}
|
||||
|
||||
174
src/effects/fireworks/index.ts
Normal file
174
src/effects/fireworks/index.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import ICanvasEffect from '../ICanvasEffect';
|
||||
|
||||
export type FireworksOptions = {
|
||||
/**
|
||||
* max fireworks count
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* gravity value that firework adds to shift from it's start position
|
||||
*/
|
||||
gravity: number;
|
||||
}
|
||||
|
||||
type FireworksParticle = {
|
||||
/**
|
||||
* color
|
||||
*/
|
||||
color: string;
|
||||
/**
|
||||
* x,y are the point where the particle starts to position on canvas
|
||||
*/
|
||||
x: number;
|
||||
y: number;
|
||||
/**
|
||||
* vx,vy shift values from x and y
|
||||
*/
|
||||
vx: number;
|
||||
vy: number;
|
||||
/**
|
||||
* the alpha opacity of the firework particle (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number;
|
||||
/**
|
||||
* w,h width and height
|
||||
*/
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export const DefaultOptions: FireworksOptions = {
|
||||
maxCount: 500,
|
||||
gravity: 0.05,
|
||||
};
|
||||
|
||||
export default class Fireworks implements ICanvasEffect {
|
||||
private readonly options: FireworksOptions;
|
||||
|
||||
constructor(options: { [key: string]: any }) {
|
||||
this.options = {...DefaultOptions, ...options};
|
||||
}
|
||||
|
||||
private context: CanvasRenderingContext2D | null = null;
|
||||
private supportsAnimationFrame = window.requestAnimationFrame;
|
||||
private particles: Array<FireworksParticle> = [];
|
||||
public isRunning: boolean;
|
||||
|
||||
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.isRunning = true;
|
||||
this.context = canvas.getContext('2d');
|
||||
this.supportsAnimationFrame.call(window, this.updateWorld);
|
||||
if (timeout) {
|
||||
window.setTimeout(this.stop, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private updateWorld = () => {
|
||||
if (!this.isRunning && this.particles.length === 0) return;
|
||||
this.update();
|
||||
this.paint();
|
||||
this.supportsAnimationFrame.call(window, this.updateWorld);
|
||||
}
|
||||
|
||||
private update = () => {
|
||||
if (this.particles.length < this.options.maxCount && this.isRunning) {
|
||||
this.createFirework();
|
||||
}
|
||||
const alive = [];
|
||||
for (let i=0; i<this.particles.length; i++) {
|
||||
if (this.move(this.particles[i])) {
|
||||
alive.push(this.particles[i]);
|
||||
}
|
||||
}
|
||||
this.particles = alive;
|
||||
}
|
||||
|
||||
private paint = () => {
|
||||
if (!this.context || !this.context.canvas) return;
|
||||
this.context.globalCompositeOperation = 'destination-out';
|
||||
this.context.fillStyle = "rgba(0,0,0,0.5)";
|
||||
this.context.fillRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
this.context.globalCompositeOperation = 'lighter';
|
||||
for (let i=0; i<this.particles.length; i++) {
|
||||
this.drawParticle(this.particles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private createFirework = () => {
|
||||
if (!this.context || !this.context.canvas) return;
|
||||
const width = this.context.canvas.width;
|
||||
const height = this.context.canvas.height;
|
||||
const xPoint = Math.random() * (width - 200) + 100;
|
||||
const yPoint = Math.random() * (height - 200) + 100;
|
||||
const nFire = Math.random() * 50 + 100;
|
||||
const color = "rgb("+(~~(Math.random()*200+55))+","
|
||||
+(~~(Math.random()*200+55))+","+(~~(Math.random()*200+55))+")";
|
||||
for (let i=0; i<nFire; i++) {
|
||||
const particle = <FireworksParticle>{};
|
||||
particle.color = color;
|
||||
particle.w = particle.h = Math.random() * 4 + 1;
|
||||
particle.x = xPoint - particle.w / 2;
|
||||
particle.y = yPoint - particle.h / 2;
|
||||
particle.vx = (Math.random()-0.5)*10;
|
||||
particle.vy = (Math.random()-0.5)*10;
|
||||
particle.alpha = Math.random()*.5+.5;
|
||||
const vy = Math.sqrt(25 - particle.vx * particle.vx);
|
||||
if (Math.abs(particle.vy) > vy) {
|
||||
particle.vy = particle.vy > 0 ? vy: -vy;
|
||||
}
|
||||
this.particles.push(particle);
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async () => {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private drawParticle = (particle: FireworksParticle): void => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
this.context.save();
|
||||
this.context.beginPath();
|
||||
|
||||
this.context.translate(particle.x + particle.w / 2, particle.y + particle.h / 2);
|
||||
this.context.arc(0, 0, particle.w, 0, Math.PI * 2);
|
||||
this.context.fillStyle = particle.color;
|
||||
this.context.globalAlpha = particle.alpha;
|
||||
|
||||
this.context.closePath();
|
||||
this.context.fill();
|
||||
this.context.restore();
|
||||
}
|
||||
|
||||
|
||||
private move = (particle: FireworksParticle) => {
|
||||
particle.x += particle.vx;
|
||||
particle.vy += this.options.gravity;
|
||||
particle.y += particle.vy;
|
||||
particle.alpha -= 0.01;
|
||||
return !(particle.x <= -particle.w || particle.x >= screen.width ||
|
||||
particle.y >= screen.height ||
|
||||
particle.alpha <= 0);
|
||||
}
|
||||
}
|
||||
@@ -47,23 +47,47 @@ type ConfettiOptions = {
|
||||
/**
|
||||
* max confetti count
|
||||
*/
|
||||
maxCount: number,
|
||||
maxCount: number;
|
||||
/**
|
||||
* particle animation speed
|
||||
*/
|
||||
speed: number,
|
||||
speed: number;
|
||||
/**
|
||||
* the confetti animation frame interval in milliseconds
|
||||
*/
|
||||
frameInterval: number,
|
||||
frameInterval: number;
|
||||
/**
|
||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number,
|
||||
alpha: number;
|
||||
/**
|
||||
* use gradient instead of solid particle color
|
||||
*/
|
||||
gradient: boolean,
|
||||
gradient: boolean;
|
||||
};
|
||||
type FireworksOptions = {
|
||||
/**
|
||||
* max fireworks count
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* gravity value that firework adds to shift from it's start position
|
||||
*/
|
||||
gravity: number;
|
||||
}
|
||||
type SnowfallOptions = {
|
||||
/**
|
||||
* The maximum number of snowflakes to render at a given time
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* The amount of gravity to apply to the snowflakes
|
||||
*/
|
||||
gravity: number;
|
||||
/**
|
||||
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||
*/
|
||||
maxDrift: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,6 +108,29 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
|
||||
gradient: false,
|
||||
},
|
||||
} as Effect<ConfettiOptions>,
|
||||
{
|
||||
emojis: ['🎆'],
|
||||
msgType: 'nic.custom.fireworks',
|
||||
command: 'fireworks',
|
||||
description: () => _td("Sends the given message with fireworks"),
|
||||
fallbackMessage: () => _t("sends fireworks") + " 🎆",
|
||||
options: {
|
||||
maxCount: 500,
|
||||
gravity: 0.05,
|
||||
},
|
||||
} as Effect<FireworksOptions>,
|
||||
{
|
||||
emojis: ['❄', '🌨'],
|
||||
msgType: 'io.element.effect.snowfall',
|
||||
command: 'snowfall',
|
||||
description: () => _td("Sends the given message with snowfall"),
|
||||
fallbackMessage: () => _t("sends snowfall") + " ❄",
|
||||
options: {
|
||||
maxCount: 200,
|
||||
gravity: 0.05,
|
||||
maxDrift: 5,
|
||||
},
|
||||
} as Effect<SnowfallOptions>,
|
||||
];
|
||||
|
||||
|
||||
|
||||
148
src/effects/snowfall/index.ts
Normal file
148
src/effects/snowfall/index.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import ICanvasEffect from '../ICanvasEffect';
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
|
||||
export type SnowfallOptions = {
|
||||
/**
|
||||
* The maximum number of snowflakes to render at a given time
|
||||
*/
|
||||
maxCount: number;
|
||||
/**
|
||||
* The amount of gravity to apply to the snowflakes
|
||||
*/
|
||||
gravity: number;
|
||||
/**
|
||||
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||
*/
|
||||
maxDrift: number;
|
||||
}
|
||||
|
||||
type Snowflake = {
|
||||
x: number;
|
||||
y: number;
|
||||
xCol: number;
|
||||
diameter: number;
|
||||
maximumDrift: number;
|
||||
gravity: number;
|
||||
}
|
||||
|
||||
export const DefaultOptions: SnowfallOptions = {
|
||||
maxCount: 200,
|
||||
gravity: 0.05,
|
||||
maxDrift: 5,
|
||||
};
|
||||
|
||||
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
|
||||
|
||||
export default class Snowfall implements ICanvasEffect {
|
||||
private readonly options: SnowfallOptions;
|
||||
|
||||
constructor(options: { [key: string]: any }) {
|
||||
this.options = {...DefaultOptions, ...options};
|
||||
}
|
||||
|
||||
private context: CanvasRenderingContext2D | null = null;
|
||||
private particles: Array<Snowflake> = [];
|
||||
private lastAnimationTime: number;
|
||||
|
||||
public isRunning: boolean;
|
||||
|
||||
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.context = canvas.getContext('2d');
|
||||
this.particles = [];
|
||||
const count = this.options.maxCount;
|
||||
while (this.particles.length < count) {
|
||||
this.particles.push(this.resetParticle({} as Snowflake, canvas.width, canvas.height));
|
||||
}
|
||||
this.isRunning = true;
|
||||
requestAnimationFrame(this.renderLoop);
|
||||
if (timeout) {
|
||||
window.setTimeout(this.stop, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async () => {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private resetParticle = (particle: Snowflake, width: number, height: number): Snowflake => {
|
||||
particle.x = Math.random() * width;
|
||||
particle.y = Math.random() * -height;
|
||||
particle.xCol = particle.x;
|
||||
particle.diameter = (Math.random() * 7) + 4;
|
||||
particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5;
|
||||
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
|
||||
return particle;
|
||||
}
|
||||
|
||||
private renderLoop = (): void => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
if (this.particles.length === 0) {
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
} else {
|
||||
const timeDelta = Date.now() - this.lastAnimationTime;
|
||||
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
|
||||
// Clear the screen first
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
|
||||
this.lastAnimationTime = Date.now();
|
||||
this.animateAndRenderSnowflakes();
|
||||
}
|
||||
requestAnimationFrame(this.renderLoop);
|
||||
}
|
||||
};
|
||||
|
||||
private animateAndRenderSnowflakes() {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
const height = this.context.canvas.height;
|
||||
for (const particle of arrayFastClone(this.particles)) {
|
||||
particle.y += particle.gravity;
|
||||
|
||||
// We treat the drift as a sine function to have a more fluid-like movement instead
|
||||
// of a pong-like movement off walls of the X column. This means that for
|
||||
// $x=A\sin(\frac{2\pi}{P}y)$ we use the `maximumDrift` as the amplitude (A) and a
|
||||
// large multiplier to create a very long waveform through P.
|
||||
const peakDistance = 75 * particle.maximumDrift;
|
||||
const PI2 = Math.PI * 2;
|
||||
particle.x = particle.maximumDrift * Math.sin((PI2 / peakDistance) * particle.y);
|
||||
particle.x += particle.xCol; // bring the particle to the right place
|
||||
|
||||
const radius = particle.diameter / 2;
|
||||
this.context.save();
|
||||
this.context.beginPath();
|
||||
this.context.ellipse(particle.x, particle.y, radius, radius, 0, 0, 360);
|
||||
this.context.fillStyle = '#eaeaea'; // grey so it shows up on the light theme
|
||||
this.context.fill();
|
||||
this.context.closePath();
|
||||
this.context.restore();
|
||||
|
||||
// Remove any dead snowflakes
|
||||
const maxBounds = radius * 4; // make sure it's *really* off screen
|
||||
if (particle.y > (height + maxBounds)) {
|
||||
const idx = this.particles.indexOf(particle);
|
||||
this.particles.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,12 @@ import {useEffect, useState} from "react";
|
||||
import SettingsStore from '../settings/SettingsStore';
|
||||
|
||||
// Hook to fetch the value of a setting and dynamically update when it changes
|
||||
export const useSettingValue = (settingName: string, roomId: string = null, excludeDefault = false) => {
|
||||
const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault));
|
||||
export const useSettingValue = <T>(settingName: string, roomId: string = null, excludeDefault = false) => {
|
||||
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
|
||||
|
||||
useEffect(() => {
|
||||
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
|
||||
setValue(SettingsStore.getValue(settingName, roomId, excludeDefault));
|
||||
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
|
||||
});
|
||||
// clean-up
|
||||
return () => {
|
||||
|
||||
28
src/hooks/useStateCallback.ts
Normal file
28
src/hooks/useStateCallback.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Dispatch, SetStateAction, useState} from "react";
|
||||
|
||||
// Hook to simplify interactions with a store-backed state values
|
||||
// Returns value and method to change the state value
|
||||
export const useStateCallback = <T>(initialValue: T, callback: (v: T) => void): [T, Dispatch<SetStateAction<T>>] => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const interceptSetValue = (newVal: T) => {
|
||||
setValue(newVal);
|
||||
callback(newVal);
|
||||
};
|
||||
return [value, interceptSetValue];
|
||||
};
|
||||
@@ -948,5 +948,7 @@
|
||||
"Confirm adding phone number": "Confirma l'addició del número de telèfon",
|
||||
"Add Email Address": "Afegeix una adreça de correu electrònic",
|
||||
"Confirm": "Confirma",
|
||||
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic."
|
||||
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.",
|
||||
"Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon",
|
||||
"Unable to access microphone": "No s'ha pogut accedir al micròfon"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"Custom Server Options": "Vlastní nastavení serveru",
|
||||
"Add a widget": "Přidat widget",
|
||||
"Accept": "Přijmout",
|
||||
"%(targetName)s accepted an invitation.": "Uživatel %(targetName)s přijal pozvání.",
|
||||
"%(targetName)s accepted an invitation.": "%(targetName)s přijal/a pozvání.",
|
||||
"Account": "Účet",
|
||||
"Access Token:": "Přístupový token:",
|
||||
"Add": "Přidat",
|
||||
@@ -86,10 +86,10 @@
|
||||
"Bans user with given id": "Vykáže uživatele s daným id",
|
||||
"Cannot add any more widgets": "Nelze přidat žádné další widgety",
|
||||
"Change Password": "Změnit heslo",
|
||||
"%(senderName)s changed their profile picture.": "Uživatel %(senderName)s změnil svůj profilový obrázek.",
|
||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "Uživatel %(senderDisplayName)s změnil název místnosti na %(roomName)s.",
|
||||
"%(senderDisplayName)s removed the room name.": "Uživatel %(senderDisplayName)s odstranil název místnosti.",
|
||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "Uživatel %(senderDisplayName)s změnil téma na „%(topic)s“.",
|
||||
"%(senderName)s changed their profile picture.": "%(senderName)s změnil/a svůj profilový obrázek.",
|
||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s změnil/a název místnosti na %(roomName)s.",
|
||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstranil/a název místnosti.",
|
||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s změnil/a téma na „%(topic)s“.",
|
||||
"Changes your display nickname": "Změní vaši zobrazovanou přezdívku",
|
||||
"Command error": "Chyba příkazu",
|
||||
"Commands": "Příkazy",
|
||||
@@ -113,7 +113,7 @@
|
||||
"Email address": "E-mailová adresa",
|
||||
"Emoji": "Emoji",
|
||||
"Enable automatic language detection for syntax highlighting": "Zapnout automatické rozpoznávání jazyků pro zvýrazňování syntaxe",
|
||||
"%(senderName)s ended the call.": "Uživatel %(senderName)s ukončil hovor.",
|
||||
"%(senderName)s ended the call.": "%(senderName)s ukončil/a hovor.",
|
||||
"Enter passphrase": "Zadejte heslo",
|
||||
"Error decrypting attachment": "Chyba při dešifrování přílohy",
|
||||
"Error: Problem communicating with the given homeserver.": "Chyba: problém v komunikaci s daným domovským serverem.",
|
||||
@@ -136,12 +136,12 @@
|
||||
"Forget room": "Zapomenout místnost",
|
||||
"For security, this session has been signed out. Please sign in again.": "Z bezpečnostních důvodů bylo toto přihlášení ukončeno. Přihlašte se prosím znovu.",
|
||||
"and %(count)s others...|other": "a %(count)s další...",
|
||||
"%(widgetName)s widget modified by %(senderName)s": "Uživatel %(senderName)s upravil widget %(widgetName)s",
|
||||
"%(widgetName)s widget removed by %(senderName)s": "Uživatel %(senderName)s odstranil widget %(widgetName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "Uživatel %(senderName)s přidal widget %(widgetName)s",
|
||||
"%(widgetName)s widget modified by %(senderName)s": "%(senderName)s upravil/a widget %(widgetName)s",
|
||||
"%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstranil/a widget %(widgetName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "%(senderName)s přidal/a widget %(widgetName)s",
|
||||
"Automatically replace plain text Emoji": "Automaticky nahrazovat textové emoji",
|
||||
"Failed to upload image": "Obrázek se nepodařilo nahrát",
|
||||
"%(senderName)s answered the call.": "Uživatel %(senderName)s přijal hovor.",
|
||||
"%(senderName)s answered the call.": "%(senderName)s přijal/a hovor.",
|
||||
"Click to mute audio": "Klepněte pro vypnutí zvuku",
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "E-mailovou adresu se nepodařilo ověřit. Přesvědčte se, že jste klepli na odkaz v e-mailové zprávě",
|
||||
"Guests cannot join this room even if explicitly invited.": "Hosté nemohou vstoupit do této místnosti, i když jsou přímo pozváni.",
|
||||
@@ -156,12 +156,12 @@
|
||||
"Incorrect username and/or password.": "Nesprávné uživatelské jméno nebo heslo.",
|
||||
"Incorrect verification code": "Nesprávný ověřovací kód",
|
||||
"Invalid Email Address": "Neplatná e-mailová adresa",
|
||||
"%(senderName)s invited %(targetName)s.": "Uživatel %(senderName)s pozval uživatele %(targetName)s.",
|
||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s pozval/a uživatele %(targetName)s.",
|
||||
"Invites": "Pozvánky",
|
||||
"Invites user with given id to current room": "Pozve do aktuální místnosti uživatele s daným id",
|
||||
"Join Room": "Vstoupit do místnosti",
|
||||
"%(targetName)s joined the room.": "Uživatel %(targetName)s vstoupil do místnosti.",
|
||||
"%(senderName)s kicked %(targetName)s.": "Uživatel %(senderName)s vykopl uživatele %(targetName)s.",
|
||||
"%(targetName)s joined the room.": "%(targetName)s vstoupil/a do místnosti.",
|
||||
"%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopl/a uživatele %(targetName)s.",
|
||||
"Kick": "Vykopnout",
|
||||
"Kicks user with given id": "Vykopne uživatele s daným id",
|
||||
"Last seen": "Naposledy aktivní",
|
||||
@@ -184,7 +184,7 @@
|
||||
"Passwords can't be empty": "Hesla nemohou být prázdná",
|
||||
"Permissions": "Oprávnění",
|
||||
"Phone": "Telefon",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "Uživatel %(senderName)s změnil úroveň oprávnění o %(powerLevelDiffText)s.",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s změnil/a úroveň oprávnění o %(powerLevelDiffText)s.",
|
||||
"Define the power level of a user": "Stanovte úroveň oprávnění uživatele",
|
||||
"Failed to change power level": "Nepodařilo se změnit úroveň oprávnění",
|
||||
"Power level must be positive integer.": "Úroveň oprávnění musí být kladné celé číslo.",
|
||||
@@ -201,15 +201,15 @@
|
||||
"%(roomName)s is not accessible at this time.": "Místnost %(roomName)s není v tuto chvíli dostupná.",
|
||||
"Save": "Uložit",
|
||||
"Send Reset Email": "Poslat resetovací e-mail",
|
||||
"%(senderDisplayName)s sent an image.": "Uživatel %(senderDisplayName)s poslal obrázek.",
|
||||
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "Uživatel %(senderName)s pozval uživatele %(targetDisplayName)s ke vstupu do místnosti.",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal/a obrázek.",
|
||||
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval/a uživatele %(targetDisplayName)s ke vstupu do místnosti.",
|
||||
"Server error": "Chyba serveru",
|
||||
"Server may be unavailable, overloaded, or search timed out :(": "Server může být nedostupný, přetížený nebo vyhledávání vypršelo :(",
|
||||
"Server may be unavailable, overloaded, or you hit a bug.": "Server může být nedostupný, přetížený nebo jste narazili na chybu.",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server je nedostupný, přetížený nebo se něco pokazilo.",
|
||||
"Session ID": "ID sezení",
|
||||
"%(senderName)s set a profile picture.": "Uživatel %(senderName)s si nastavil profilový obrázek.",
|
||||
"%(senderName)s set their display name to %(displayName)s.": "Uživatel %(senderName)s si změnil zobrazované jméno na %(displayName)s.",
|
||||
"%(senderName)s set a profile picture.": "%(senderName)s si nastavil/a profilový obrázek.",
|
||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si změnil/a zobrazované jméno na %(displayName)s.",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Zobrazovat čas v 12hodinovém formátu (např. 2:30 odp.)",
|
||||
"Sign in": "Přihlásit",
|
||||
"Sign out": "Odhlásit",
|
||||
@@ -235,13 +235,13 @@
|
||||
"Online": "Online",
|
||||
"Offline": "Offline",
|
||||
"Check for update": "Zkontrolovat aktualizace",
|
||||
"%(targetName)s accepted the invitation for %(displayName)s.": "Uživatel %(targetName)s přijal pozvání pro %(displayName)s.",
|
||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal/a pozvání pro %(displayName)s.",
|
||||
"Active call (%(roomName)s)": "Probíhající hovor (%(roomName)s)",
|
||||
"%(senderName)s banned %(targetName)s.": "Uživatel %(senderName)s vykázal uživatele %(targetName)s.",
|
||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal/a uživatele %(targetName)s.",
|
||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Nelze se připojit k domovskému serveru přes HTTP, pokud je v adresním řádku HTTPS. Buď použijte HTTPS, nebo <a>povolte nezabezpečené skripty</a>.",
|
||||
"Click here to fix": "Pro opravu klepněte zde",
|
||||
"Click to mute video": "Klepněte pro zakázání videa",
|
||||
"click to reveal": "pro odhlení klepněte",
|
||||
"click to reveal": "klepněte pro odhalení",
|
||||
"Click to unmute video": "Klepněte pro povolení videa",
|
||||
"Click to unmute audio": "Klepněte pro povolení zvuku",
|
||||
"Displays action": "Zobrazí akci",
|
||||
@@ -258,7 +258,7 @@
|
||||
"Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
|
||||
"Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.",
|
||||
"Unban": "Přijmout zpět",
|
||||
"%(senderName)s unbanned %(targetName)s.": "Uživatel %(senderName)s přijal zpět uživatele %(targetName)s.",
|
||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal/a zpět uživatele %(targetName)s.",
|
||||
"Unable to capture screen": "Nepodařilo se zachytit obrazovku",
|
||||
"Unable to enable Notifications": "Nepodařilo se povolit oznámení",
|
||||
"unknown caller": "neznámý volající",
|
||||
@@ -305,11 +305,11 @@
|
||||
"Reason": "Důvod",
|
||||
"VoIP conference started.": "VoIP konference započata.",
|
||||
"VoIP conference finished.": "VoIP konference ukončena.",
|
||||
"%(targetName)s left the room.": "Uživatel %(targetName)s opustil místnost.",
|
||||
"%(targetName)s left the room.": "%(targetName)s opustil/a místnost.",
|
||||
"You are already in a call.": "Již máte probíhající hovor.",
|
||||
"%(senderName)s requested a VoIP conference.": "Uživatel %(senderName)s požádal o VoIP konferenci.",
|
||||
"%(senderName)s removed their profile picture.": "Uživatel %(senderName)s odstranil svůj profilový obrázek.",
|
||||
"%(targetName)s rejected the invitation.": "Uživatel %(targetName)s odmítl pozvání.",
|
||||
"%(senderName)s removed their profile picture.": "%(senderName)s odstranil/a svůj profilový obrázek.",
|
||||
"%(targetName)s rejected the invitation.": "%(targetName)s odmítl/a pozvání.",
|
||||
"Communities": "Skupiny",
|
||||
"Message Pinning": "Připíchnutí zprávy",
|
||||
"Your browser does not support the required cryptography extensions": "Váš prohlížeč nepodporuje požadovaná kryptografická rozšíření",
|
||||
@@ -320,20 +320,20 @@
|
||||
"Admin Tools": "Nástroje pro správce",
|
||||
"No pinned messages.": "Žádné připíchnuté zprávy.",
|
||||
"Pinned Messages": "Připíchnuté zprávy",
|
||||
"%(senderName)s removed their display name (%(oldDisplayName)s).": "Uživatel %(senderName)s odstranil své zobrazované jméno (%(oldDisplayName)s).",
|
||||
"%(senderName)s withdrew %(targetName)s's invitation.": "Uživatel %(senderName)s zrušil pozvání pro uživatele %(targetName)s.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "Uživatel %(senderName)s nastavil viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "Uživatel %(senderName)s nastavil viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
|
||||
"%(senderName)s made future room history visible to all room members.": "Uživatel %(senderName)s nastavil viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
|
||||
"%(senderName)s made future room history visible to anyone.": "Uživatel %(senderName)s nastavil viditelnost budoucích zpráv pro kohokoliv.",
|
||||
"%(senderName)s changed the pinned messages for the room.": "Uživatel %(senderName)s změnil připíchnuté zprávy této místnosti.",
|
||||
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil/a své zobrazované jméno (%(oldDisplayName)s).",
|
||||
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil/a pozvání pro uživatele %(targetName)s.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
|
||||
"%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
|
||||
"%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil/a viditelnost budoucích zpráv pro kohokoliv.",
|
||||
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil/a připíchnuté zprávy této místnosti.",
|
||||
"Authentication check failed: incorrect password?": "Kontrola ověření selhala: špatné heslo?",
|
||||
"You need to be able to invite users to do that.": "Pro tuto akci musíte mít právo zvát uživatele.",
|
||||
"Delete Widget": "Smazat widget",
|
||||
"Error decrypting image": "Chyba při dešifrování obrázku",
|
||||
"Error decrypting video": "Chyba při dešifrování videa",
|
||||
"%(senderDisplayName)s removed the room avatar.": "Uživatel %(senderDisplayName)s odstranil avatar místnosti.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "Uživatel %(senderDisplayName)s změnil avatar místnosti na <img/>",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil/a avatar místnosti.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil/a avatar místnosti na <img/>",
|
||||
"Copied!": "Zkopírováno!",
|
||||
"Failed to copy": "Nepodařilo se zkopírovat",
|
||||
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Smazáním widgetu ho odstraníte všem uživatelům v této místnosti. Opravdu chcete tento widget smazat?",
|
||||
@@ -384,9 +384,9 @@
|
||||
"Invalid community ID": "Neplatné ID skupiny",
|
||||
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' není platné ID skupiny",
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "Nové ID skupiny (např. +neco:%(localDomain)s)",
|
||||
"%(senderName)s sent an image": "Uživatel %(senderName)s poslal obrázek",
|
||||
"%(senderName)s sent a video": "Uživatel %(senderName)s poslal video",
|
||||
"%(senderName)s uploaded a file": "Uživatel %(senderName)s nahrál soubor",
|
||||
"%(senderName)s sent an image": "%(senderName)s poslal/a obrázek",
|
||||
"%(senderName)s sent a video": "%(senderName)s poslal/a video",
|
||||
"%(senderName)s uploaded a file": "%(senderName)s nahrál/a soubor",
|
||||
"Disinvite this user?": "Odvolat pozvání tohoto uživatele?",
|
||||
"Kick this user?": "Vykopnout tohoto uživatele?",
|
||||
"Unban this user?": "Přijmout zpět tohoto uživatele?",
|
||||
@@ -398,7 +398,7 @@
|
||||
"You have <a>disabled</a> URL previews by default.": "<a>Vypnuli</a> jste automatické náhledy webových adres.",
|
||||
"You have <a>enabled</a> URL previews by default.": "<a>Zapnuli</a> jste automatické náhledy webových adres.",
|
||||
"URL Previews": "Náhledy webových adres",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "Uživatel %(senderDisplayName)s změnil avatar místnosti %(roomName)s",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil/a avatar místnosti %(roomName)s",
|
||||
"Add an Integration": "Přidat začlenění",
|
||||
"An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s jsme poslali e-mail",
|
||||
"File to import": "Soubor k importu",
|
||||
@@ -466,18 +466,18 @@
|
||||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s krát vstoupili",
|
||||
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)svstoupili",
|
||||
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil",
|
||||
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil/a",
|
||||
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s krát opustili",
|
||||
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sopustili",
|
||||
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s krát opustil",
|
||||
"%(oneUser)sleft %(count)s times|one": "%(oneUser)sopustil",
|
||||
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s krát vstoupili a opustili",
|
||||
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)svstoupili a opustili",
|
||||
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil a opustil",
|
||||
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil a opustil",
|
||||
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a a opustil/a",
|
||||
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil/a a opustil/a",
|
||||
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s krát opustili a znovu vstoupili",
|
||||
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sopustili a znovu vstoupili",
|
||||
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil a znovu vstoupil",
|
||||
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil/a a znovu vstoupil/a",
|
||||
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sopustil a znovu vstoupil",
|
||||
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s %(count)s krát odmítli pozvání",
|
||||
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)sodmítli pozvání",
|
||||
@@ -487,10 +487,10 @@
|
||||
"%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)smeli stažené pozvání",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)směl %(count)s krát stažené pozvání",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)směl stažené pozvání",
|
||||
"were invited %(count)s times|other": "byli %(count)s krát pozvaní",
|
||||
"were invited %(count)s times|one": "byli pozvaní",
|
||||
"was invited %(count)s times|other": "byl %(count)s krát pozvaný",
|
||||
"was invited %(count)s times|one": "byl pozvaný",
|
||||
"were invited %(count)s times|other": "byli %(count)s krát pozváni",
|
||||
"were invited %(count)s times|one": "byli pozváni",
|
||||
"was invited %(count)s times|other": "byl %(count)s krát pozván",
|
||||
"was invited %(count)s times|one": "byl pozván",
|
||||
"were banned %(count)s times|other": "mělid %(count)s krát zakázaný vstup",
|
||||
"were banned %(count)s times|one": "měli zakázaný vstup",
|
||||
"was banned %(count)s times|other": "měl %(count)s krát zakázaný vstup",
|
||||
@@ -503,14 +503,14 @@
|
||||
"were kicked %(count)s times|one": "byli vyhozeni",
|
||||
"was kicked %(count)s times|other": "byl %(count)s krát vyhozen",
|
||||
"was kicked %(count)s times|one": "byl vyhozen",
|
||||
"%(severalUsers)schanged their name %(count)s times|other": "Uživatelé %(severalUsers)s si %(count)s krát změnili jméno",
|
||||
"%(severalUsers)schanged their name %(count)s times|one": "Uživatelé %(severalUsers)s si změnili jméno",
|
||||
"%(oneUser)schanged their name %(count)s times|other": "Uživatel %(oneUser)s si %(count)s krát změnil jméno",
|
||||
"%(oneUser)schanged their name %(count)s times|one": "Uživatel %(oneUser)s si změnil jméno",
|
||||
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s si %(count)s krát změnili jméno",
|
||||
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s si změnili jméno",
|
||||
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a jméno",
|
||||
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s si změnil/ jméno",
|
||||
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)ssi %(count)s krát změnili avatary",
|
||||
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)ssi změnili avatary",
|
||||
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)ssi %(count)s krát změnil avatar",
|
||||
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)ssi změnil avatar",
|
||||
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a avatar",
|
||||
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s si změnil/a avatar",
|
||||
"%(items)s and %(count)s others|other": "%(items)s a %(count)s další",
|
||||
"%(items)s and %(count)s others|one": "%(items)s a jeden další",
|
||||
"%(items)s and %(lastItem)s": "%(items)s a také %(lastItem)s",
|
||||
@@ -539,7 +539,7 @@
|
||||
"To get started, please pick a username!": "Začněte tím, že si zvolíte uživatelské jméno!",
|
||||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Toto bude název vašeho účtu na domovském serveru <span></span>, anebo si můžete zvolit <a>jiný server</a>.",
|
||||
"If you already have a Matrix account you can <a>log in</a> instead.": "Pokud už účet v síti Matrix máte, můžete se ihned <a>Přihlásit</a>.",
|
||||
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil",
|
||||
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a",
|
||||
"Private Chat": "Soukromá konverzace",
|
||||
"Public Chat": "Veřejná konverzace",
|
||||
"You must <a>register</a> to use this functionality": "Pro využívání této funkce se <a>zaregistrujte</a>",
|
||||
@@ -564,7 +564,7 @@
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Tyto místnosti se zobrazují všem členům na stránce skupiny. Členové skupiny mohou vstoupit do místnosti klepnutím.",
|
||||
"Featured Rooms:": "Hlavní místnosti:",
|
||||
"Featured Users:": "Významní uživatelé:",
|
||||
"%(inviter)s has invited you to join this community": "Uživatel %(inviter)s vás pozval do této skupiny",
|
||||
"%(inviter)s has invited you to join this community": "%(inviter)s vás pozval/a do této skupiny",
|
||||
"You are an administrator of this community": "Jste správcem této skupiny",
|
||||
"You are a member of this community": "Jste členem této skupiny",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Vaše skupina nemá vyplněný dlouhý popis, který je součástí HTML stránky skupiny a která se zobrazuje jejím členům.<br />Klepnutím zde otevřete nastavení, kde ho můžete doplnit!",
|
||||
@@ -753,7 +753,7 @@
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
||||
"Missing roomId.": "Chybějící ID místnosti.",
|
||||
"Opens the Developer Tools dialog": "Otevře dialog nástrojů pro vývojáře",
|
||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil zobrazované jméno na %(displayName)s.",
|
||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil/a zobrazované jméno na %(displayName)s.",
|
||||
"Always show encryption icons": "Vždy zobrazovat ikonu stavu šifrovaní",
|
||||
"Send analytics data": "Odesílat analytická data",
|
||||
"Enable widget screenshots on supported widgets": "Povolit screenshot widgetu pro podporované widgety",
|
||||
@@ -764,7 +764,7 @@
|
||||
"Demote": "Degradovat",
|
||||
"Share Link to User": "Sdílet odkaz na uživatele",
|
||||
"Send an encrypted reply…": "Odeslat šifrovanou odpověď …",
|
||||
"Send an encrypted message…": "Odeslat šifrovanou zprávu …",
|
||||
"Send an encrypted message…": "Odeslat šifrovanou zprávu…",
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) viděl %(dateTime)s",
|
||||
"Replying": "Odpovídá",
|
||||
"Share room": "Sdílet místnost",
|
||||
@@ -860,7 +860,7 @@
|
||||
"Security & Privacy": "Zabezpečení",
|
||||
"Encryption": "Šifrování",
|
||||
"Once enabled, encryption cannot be disabled.": "Po zapnutí, už nepůjde šifrování vypnout.",
|
||||
"Encrypted": "Šifrování",
|
||||
"Encrypted": "Šifrováno",
|
||||
"General": "Obecné",
|
||||
"General failure": "Nějaká chyba",
|
||||
"This homeserver does not support login using email address.": "Tento domovský serveru neumožňuje přihlášení pomocí e-mailu.",
|
||||
@@ -943,14 +943,14 @@
|
||||
"Upgrades a room to a new version": "Upgraduje místnost na novou verzi",
|
||||
"This room has no topic.": "Tato místnost nemá žádné specifické téma.",
|
||||
"Sets the room name": "Nastaví název místnosti",
|
||||
"%(senderDisplayName)s upgraded this room.": "Uživatel %(senderDisplayName)s upgradoval místnost.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "Uživatel %(senderDisplayName)s zveřejnil místnost pro všechny s odkazem.",
|
||||
"%(senderDisplayName)s made the room invite only.": "Uživatel %(senderDisplayName)s zpřístupnil místnost pouze na pozvání.",
|
||||
"%(senderDisplayName)s changed the join rule to %(rule)s": "Uživatel %(senderDisplayName)s změnil pravidlo k připojení na %(rule)s",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "Uživatel %(senderDisplayName)s povolil přístup hostům.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "Uživatel %(senderDisplayName)s zakázal přístup hostům.",
|
||||
"%(senderDisplayName)s changed guest access to %(rule)s": "Uživatel %(senderDisplayName)s změnil pravidlo pro přístup hostů na %(rule)s",
|
||||
"%(senderName)s set the main address for this room to %(address)s.": "Uživatel %(senderName)s hlavní adresu této místnosti na %(address)s.",
|
||||
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgradoval/a místnost.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s zveřejnil/a místnost pro všechny s odkazem.",
|
||||
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s zpřístupnil/a místnost pouze na pozvání.",
|
||||
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo k připojení na %(rule)s",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s povolil/a přístup hostům.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s zakázal/a přístup hostům.",
|
||||
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo pro přístup hostů na %(rule)s",
|
||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil/a hlavní adresu této místnosti na %(address)s.",
|
||||
"%(senderName)s removed the main address for this room.": "%(senderName)s zrušil hlavní adresu této místnosti.",
|
||||
"%(displayName)s is typing …": "%(displayName)s píše …",
|
||||
"%(names)s and %(count)s others are typing …|other": "%(names)s a %(count)s dalších píše …",
|
||||
@@ -1136,7 +1136,7 @@
|
||||
"Render simple counters in room header": "Zobrazovat stavová počítadla v hlavičce místnosti",
|
||||
"Enable Community Filter Panel": "Povolit panel Filtr skupiny",
|
||||
"Show developer tools": "Zobrazit nástroje pro vývojáře",
|
||||
"Encrypted messages in group chats": "Šifrované zprávy ve skupinových konverzacích",
|
||||
"Encrypted messages in group chats": "Šifrované zprávy ve skupinách",
|
||||
"Open Devtools": "Otevřít nástroje pro vývojáře",
|
||||
"Credits": "Poděkování",
|
||||
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Na adrese %(host)s už jste použili %(brand)s se zapnutou volbou načítání členů místností až při prvním zobrazení. V této verzi je načítání členů až při prvním zobrazení vypnuté. Protože je s tímto nastavením lokální vyrovnávací paměť nekompatibilní, %(brand)s potřebuje znovu synchronizovat údaje z vašeho účtu.",
|
||||
@@ -1231,10 +1231,10 @@
|
||||
"You cannot modify widgets in this room.": "V této místnosti nemůžete manipulovat s widgety.",
|
||||
"Sends the given message coloured as a rainbow": "Pošle zprávu v barvách duhy",
|
||||
"Sends the given emote coloured as a rainbow": "Pošle reakci v barvách duhy",
|
||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "Uživatel %(senderDisplayName)s přidal této místnosti příslušnost ke skupině %(groups)s.",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "Uživatel %(senderDisplayName)s odebral této místnosti příslušnost ke skupině %(groups)s.",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Uživatel %(senderDisplayName)s přidal této místnosti příslušnost ke skupině %(newGroups)s a odebral k %(oldGroups)s.",
|
||||
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
|
||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(groups)s.",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s odebral/a této místnosti příslušnost ke skupině %(groups)s.",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(newGroups)s a odebral/a k %(oldGroups)s.",
|
||||
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil/a pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
|
||||
"No homeserver URL provided": "Nebyla zadána URL adresa domovského server",
|
||||
"Unexpected error resolving homeserver configuration": "Chyba při zjišťování konfigurace domovského serveru",
|
||||
"The user's homeserver does not support the version of the room.": "Uživatelův domovský server nepodporuje verzi této místnosti.",
|
||||
@@ -1253,19 +1253,19 @@
|
||||
"Join the conversation with an account": "Připojte se ke konverzaci s účtem",
|
||||
"Sign Up": "Zaregistrovat se",
|
||||
"Sign In": "Přihlásit se",
|
||||
"You were kicked from %(roomName)s by %(memberName)s": "Uživatel %(memberName)s vás vykopl z místnosti %(roomName)s",
|
||||
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s vás vykopl/a z místnosti %(roomName)s",
|
||||
"Reason: %(reason)s": "Důvod: %(reason)s",
|
||||
"Forget this room": "Zapomenout na tuto místnost",
|
||||
"Re-join": "Znovu vstoupit",
|
||||
"You were banned from %(roomName)s by %(memberName)s": "Uživatel %(memberName)s vás vykázal z místnosti %(roomName)s",
|
||||
"You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vás vykázal/a z místnosti %(roomName)s",
|
||||
"Something went wrong with your invite to %(roomName)s": "S vaší pozvánkou do místnosti %(roomName)s se něco pokazilo",
|
||||
"You can only join it with a working invite.": "Vstoupit můžete jen s funkční pozvánkou.",
|
||||
"You can still join it because this is a public room.": "I přesto můžete vstoupit, protože tato místnost je veřejná.",
|
||||
"Join the discussion": "Zapojit se do diskuze",
|
||||
"Try to join anyway": "Stejně se pokusit vstoupit",
|
||||
"Do you want to chat with %(user)s?": "Chcete si povídat s uživatelem %(user)s?",
|
||||
"Do you want to chat with %(user)s?": "Chcete si povídat s %(user)s?",
|
||||
"Do you want to join %(roomName)s?": "Chcete vstoupit do místnosti %(roomName)s?",
|
||||
"<userName/> invited you": "Uživatel <userName/> vás pozval",
|
||||
"<userName/> invited you": "<userName/> vás pozval/a",
|
||||
"You're previewing %(roomName)s. Want to join it?": "Nahlížíte do místnosti %(roomName)s. Chcete do ní vstoupit?",
|
||||
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s si nelze jen tak prohlížet. Chcete do ní vstoupit?",
|
||||
"This room doesn't exist. Are you sure you're at the right place?": "Tato místnost neexistuje. Jste si jistí, že jste na správném místě?",
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"Invited by %(sender)s": "Pozván od uživatele %(sender)s",
|
||||
"Error updating flair": "Nepovedlo se změnit příslušnost ke skupině",
|
||||
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Pro tuto místnost se nepovedlo změnit příslušnost ke skupině. Možná to server neumožňuje, nebo došlo k dočasné chybě.",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "Uživatel <reactors/><reactedWith>reagoval s %(shortName)s</reactedWith>",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagoval/a s %(shortName)s</reactedWith>",
|
||||
"edited": "upraveno",
|
||||
"Maximize apps": "Maximalizovat aplikace",
|
||||
"Rotate Left": "Otočit doleva",
|
||||
@@ -1509,31 +1509,31 @@
|
||||
"Show image": "Zobrazit obrázek",
|
||||
"You verified %(name)s": "Ověřili jste %(name)s",
|
||||
"You cancelled verifying %(name)s": "Zrušili jste ověření %(name)s",
|
||||
"%(name)s cancelled verifying": "Uživatel %(name)s zrušil ověření",
|
||||
"%(name)s cancelled verifying": "%(name)s zrušil/a ověření",
|
||||
"You accepted": "Přijali jste",
|
||||
"%(name)s accepted": "Uživatel %(name)s přijal",
|
||||
"%(name)s accepted": "%(name)s přijal/a",
|
||||
"You cancelled": "Zrušili jste",
|
||||
"%(name)s cancelled": "Uživatel %(name)s zrušil",
|
||||
"%(name)s cancelled": "%(name)s zrušil/a",
|
||||
"%(name)s wants to verify": "%(name)s chce ověřit",
|
||||
"You sent a verification request": "Poslali jste požadavek na ověření",
|
||||
"Show all": "Zobrazit vše",
|
||||
"Edited at %(date)s. Click to view edits.": "Upraveno v %(date)s. Klinutím zobrazíte změny.",
|
||||
"Frequently Used": "Často používané",
|
||||
"Smileys & People": "Obličeje & Lidé",
|
||||
"Animals & Nature": "Zvířata & Příroda",
|
||||
"Food & Drink": "Jídlo & Nápoje",
|
||||
"Smileys & People": "Obličeje a lidé",
|
||||
"Animals & Nature": "Zvířata a příroda",
|
||||
"Food & Drink": "Jídlo a nápoje",
|
||||
"Activities": "Aktivity",
|
||||
"Travel & Places": "Cestování & Místa",
|
||||
"Travel & Places": "Cestování a místa",
|
||||
"Objects": "Objekty",
|
||||
"Symbols": "Symboly",
|
||||
"Flags": "Vlajky",
|
||||
"Quick Reactions": "Rychlé reakce",
|
||||
"Cancel search": "Zrušit hledání",
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Vyrobte prosím <newIssueLink>nové issue</newIssueLink> na GitHubu abychom mohli chybu opravit.",
|
||||
"%(severalUsers)smade no changes %(count)s times|other": "Uživatelé %(severalUsers)s neudělali %(count)s krát žádnou změnu",
|
||||
"%(severalUsers)smade no changes %(count)s times|one": "Uživatelé %(severalUsers)s neudělali žádnou změnu",
|
||||
"%(oneUser)smade no changes %(count)s times|other": "Uživatel %(oneUser)s neudělal %(count)s krát žádnou změnu",
|
||||
"%(oneUser)smade no changes %(count)s times|one": "Uživatel %(oneUser)s neudělal žádnou změnu",
|
||||
"%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu",
|
||||
"%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s neudělali žádnou změnu",
|
||||
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal/a %(count)s krát žádnou změnu",
|
||||
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal/a žádnou změnu",
|
||||
"e.g. my-room": "např. moje-mistnost",
|
||||
"Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek",
|
||||
"Terms of Service": "Podmínky použití",
|
||||
@@ -1555,7 +1555,7 @@
|
||||
"Explore": "Procházet",
|
||||
"Filter": "Filtr místností",
|
||||
"Filter rooms…": "Najít místnost…",
|
||||
"%(creator)s created and configured the room.": "%(creator)s vytvořil a nakonfiguroval místnost.",
|
||||
"%(creator)s created and configured the room.": "%(creator)s vytvořil/a a nakonfiguroval/a místnost.",
|
||||
"Preview": "Náhled",
|
||||
"View": "Zobrazit",
|
||||
"Find a room…": "Najít místnost…",
|
||||
@@ -1593,23 +1593,23 @@
|
||||
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s spustil hovor. (není podporováno tímto prohlížečem)",
|
||||
"%(senderName)s placed a video call.": "%(senderName)s spustil videohovor.",
|
||||
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s spustil videohovor. (není podporováno tímto prohlížečem)",
|
||||
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranit pravidlo blokující uživatele odpovídající %(glob)s",
|
||||
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil/a pravidlo blokující uživatele odpovídající %(glob)s",
|
||||
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s odstranil pravidlo blokující místnosti odpovídající %(glob)s",
|
||||
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s odstranil pravidlo blokující servery odpovídající %(glob)s",
|
||||
"%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s odstranil blokující pravidlo %(glob)s",
|
||||
"%(senderName)s updated an invalid ban rule": "%(senderName)s aktualizoval neplatné pravidlo blokování",
|
||||
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s vytvořil blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
|
||||
"Try out new ways to ignore people (experimental)": "Vyzkoušejte nové metody ignorování lidí (experimentální)",
|
||||
"Match system theme": "Nastavit podle vzhledu systému",
|
||||
"My Ban List": "Můj seznam zablokovaných",
|
||||
@@ -1650,7 +1650,7 @@
|
||||
"Subscribed lists": "Odebírané seznamy",
|
||||
"Subscribe": "Odebírat",
|
||||
"This message cannot be decrypted": "Zprávu nelze rozšifrovat",
|
||||
"Unencrypted": "Nešifrované",
|
||||
"Unencrypted": "Nezašifrované",
|
||||
"<userName/> wants to chat": "<userName/> si chce psát",
|
||||
"Start chatting": "Zahájit konverzaci",
|
||||
"Failed to connect to integration manager": "Nepovedlo se připojit ke správci integrací",
|
||||
@@ -1745,7 +1745,7 @@
|
||||
"Waiting for %(displayName)s to verify…": "Čekám až nás %(displayName)s ověří…",
|
||||
"They match": "Odpovídají",
|
||||
"They don't match": "Neodpovídají",
|
||||
"To be secure, do this in person or use a trusted way to communicate.": "Aby to bylo bezpečné, udělejte to osobně nebo použijte důvěryhodný komunikační prostředek.",
|
||||
"To be secure, do this in person or use a trusted way to communicate.": "Aby bylo ověření bezpečné, proveďte ho osobně nebo použijte důvěryhodný komunikační prostředek.",
|
||||
"Lock": "Zámek",
|
||||
"Verify yourself & others to keep your chats safe": "Ověřte sebe a ostatní, aby byla vaše komunikace bezpečná",
|
||||
"Other users may not trust it": "Ostatní uživatelé této relaci nemusí věřit",
|
||||
@@ -1755,7 +1755,7 @@
|
||||
"This bridge is managed by <user />.": "Toto propojení spravuje <user />.",
|
||||
"Workspace: %(networkName)s": "Pracovní oblast: %(networkName)s",
|
||||
"Channel: %(channelName)s": "Kanál: %(channelName)s",
|
||||
"Show less": "Skrýt detaily",
|
||||
"Show less": "Zobrazit méně",
|
||||
"Show more": "Více",
|
||||
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Změna hesla resetuje šifrovací klíče pro všechny vaše relace. Pokud si nejdřív nevyexportujete klíče místností a po změně je znovu neimportujete, nedostanete se k historickým zprávám. V budoucnu se toto zjednoduší.",
|
||||
"Cross-signing and secret storage are enabled.": "Cross-signing a bezpečné úložiště jsou zapnuté.",
|
||||
@@ -1801,12 +1801,12 @@
|
||||
"Session key:": "Klíč relace:",
|
||||
"Message search": "Vyhledávání ve zprávách",
|
||||
"Cross-signing": "Cross-signing",
|
||||
"A session's public name is visible to people you communicate with": "Lidé, se kterými komunikujete, mohou veřejný název zobrazit",
|
||||
"A session's public name is visible to people you communicate with": "Lidé, se kterými komunikujete, mohou zobrazit veřejný název",
|
||||
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "Tato místnost je propojena s následujícími platformami. <a>Více informací</a>",
|
||||
"This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "Tato místnost není propojená s žádnými dalšími platformami. <a>Více informací.</a>",
|
||||
"Bridges": "Propojení",
|
||||
"This user has not verified all of their sessions.": "Tento uživatel zatím neověřil všechny své relace.",
|
||||
"You have not verified this user.": "Tohoto uživatele jste neověřil.",
|
||||
"You have not verified this user.": "Tohoto uživatele jste neověřili.",
|
||||
"You have verified this user. This user has verified all of their sessions.": "Tohoto uživatele jste ověřili a on ověřil všechny své relace.",
|
||||
"Someone is using an unknown session": "Někdo používá neznámou relaci",
|
||||
"This room is end-to-end encrypted": "Místnost je šifrovaná end-to-end",
|
||||
@@ -1820,15 +1820,15 @@
|
||||
"Encrypted by a deleted session": "Šifrované smazanou relací",
|
||||
"Invite only": "Pouze na pozvání",
|
||||
"Send a reply…": "Odpovědět…",
|
||||
"Send a message…": "Napsat zprávu…",
|
||||
"Send a message…": "Odeslat zprávu…",
|
||||
"Direct Messages": "Přímé zprávy",
|
||||
"Reject & Ignore user": "Odmítnout & ignorovat uživatele",
|
||||
"Reject & Ignore user": "Odmítnout a ignorovat uživatele",
|
||||
"Unknown Command": "Neznámý příkaz",
|
||||
"Unrecognised command: %(commandText)s": "Nerozpoznaný příkaz: %(commandText)s",
|
||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Můžete použít <code>/help</code> na vypsání všech příkazů. Nebo jste text chtěli odeslat jako zprávu?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Tip: Zprávu můžete začít <code>//</code>, pokud chcete aby začínala lomítkem.",
|
||||
"Send as message": "Odeslat jako zprávu",
|
||||
"Waiting for %(displayName)s to accept…": "Čekáme, než %(displayName)s přijme…",
|
||||
"Waiting for %(displayName)s to accept…": "Čekáme, než %(displayName)s výzvu přijme…",
|
||||
"Start Verification": "Začít s ověřením",
|
||||
"Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Vaše zprávy jsou zabezpečené - pouze vy a jejich příjemci máte klíče potřebné k jejich přečtení.",
|
||||
"Verify User": "Ověřit uživatele",
|
||||
@@ -1922,7 +1922,7 @@
|
||||
"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.": "Relace, kterou se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emoji, což je to, co %(brand)s podporuje. Zkuste použít jiného klienta.",
|
||||
"Verify by scanning": "Ověřte naskenováním",
|
||||
"You declined": "Odmítli jste",
|
||||
"%(name)s declined": "Uživatel %(name)s odmítl",
|
||||
"%(name)s declined": "%(name)s odmítl/a",
|
||||
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Uschovejte si kopii na bezpečném místě, například ve správci hesel nebo v trezoru.",
|
||||
"Your recovery key": "Váš obnovovací klíč",
|
||||
"Copy": "Zkopírovat",
|
||||
@@ -1968,9 +1968,9 @@
|
||||
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal/a této místnosti alternativní adresu %(addresses)s.",
|
||||
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral/a této místnosti alternativní adresy %(addresses)s.",
|
||||
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral/a této místnosti alternativní adresu %(addresses)s.",
|
||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil/a této místnosti alternativní adresy.",
|
||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil/a této místnosti hlavní a alternativní adresy.",
|
||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s změnil/a této místnosti adresy.",
|
||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil/a alternativní adresy této místnosti.",
|
||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil/a hlavní a alternativní adresy této místnosti.",
|
||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s změnil/a adresy této místnosti.",
|
||||
"Manually Verify by Text": "Manuální textové ověření",
|
||||
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
|
||||
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
|
||||
@@ -2041,7 +2041,7 @@
|
||||
"Where you’re logged in": "Kde jste přihlášení",
|
||||
"You've successfully verified your device!": "Úspěšně jste ověřili vaše zařízení!",
|
||||
"Start verification again from the notification.": "Začít proces ověření znovu pomocí notifikace.",
|
||||
"Start verification again from their profile.": "Začít proces ověření znovu z jejich profilu.",
|
||||
"Start verification again from their profile.": "Proces ověření začněte znovu z profilu kontaktu.",
|
||||
"Verification timed out.": "Ověření vypršelo.",
|
||||
"You cancelled verification on your other session.": "Na druhé relace jste proces ověření zrušili.",
|
||||
"%(displayName)s cancelled verification.": "%(displayName)s zrušil/a proces ověření.",
|
||||
@@ -2064,7 +2064,7 @@
|
||||
"%(networkName)s rooms": "místnosti v %(networkName)s",
|
||||
"Matrix rooms": "místnosti na Matrixu",
|
||||
"Enable end-to-end encryption": "Povolit E2E šifrování",
|
||||
"You can’t disable this later. Bridges & most bots won’t work yet.": "Už to v budoucnu nepůjde vypnout. Většina botů a propojení zatím nefunguje.",
|
||||
"You can’t disable this later. Bridges & most bots won’t work yet.": "Toto nelze později vypnout. Většina botů a propojení zatím nefunguje.",
|
||||
"Server did not require any authentication": "Server nevyžadoval žádné ověření",
|
||||
"Server did not return valid authentication information.": "Server neposkytl platné informace o ověření.",
|
||||
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Potvrďte deaktivaci účtu použtím Jednotného přihlášení.",
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"Change notification settings": "Upravit nastavení oznámení",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototyp komunit verze 2. Vyžaduje kompatibilní domovský server. Experimentální - používejte opatrně.",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototyp skupin verze 2. Vyžaduje kompatibilní domovský server. Experimentální - používejte opatrně.",
|
||||
"Use custom size": "Použít vlastní velikost",
|
||||
"Use a more compact ‘Modern’ layout": "Používat kompaktní ‘Moderní’ vzhled",
|
||||
"Use a system font": "Používat systémové nastavení písma",
|
||||
@@ -2251,9 +2251,9 @@
|
||||
"Feedback": "Zpětná vazba",
|
||||
"Feedback sent": "Zpětná vazba byla odeslána",
|
||||
"Security & privacy": "Zabezpečení",
|
||||
"All settings": "Nastavení",
|
||||
"All settings": "Všechna nastavení",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Napište jméno nebo emailovou adresu uživatele se kterým chcete začít konverzaci (např. <userId/>).",
|
||||
"Start a new chat": "Založit nový chat",
|
||||
"Start a new chat": "Založit novou konverzaci",
|
||||
"Which officially provided instance you are using, if any": "Kterou oficiální instanci Riot.im používáte (a jestli vůbec)",
|
||||
"Change the topic of this room": "Změnit téma této místnosti",
|
||||
"%(senderName)s declined the call.": "%(senderName)s odmítl/a hovor.",
|
||||
@@ -2291,11 +2291,11 @@
|
||||
"Show Widgets": "Zobrazit widgety",
|
||||
"Hide Widgets": "Skrýt widgety",
|
||||
"Room settings": "Nastavení místnosti",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Pomocí <a>desktopové aplikace</a> zobrazíte všechny šifrované soubory",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Pro zobrazení všech šifrovaných souborů použijte <a>desktopovou aplikaci</a>",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Připojte soubory z chatu nebo je jednoduše přetáhněte kamkoli do místnosti.",
|
||||
"No files visible in this room": "V této místnosti nejsou viditelné žádné soubory",
|
||||
"Show files": "Zobrazit soubory",
|
||||
"%(count)s people|other": "%(count)s lidé",
|
||||
"%(count)s people|other": "%(count)s lidé/í",
|
||||
"About": "O",
|
||||
"You’re all caught up": "Vše vyřízeno",
|
||||
"You have no visible notifications in this room.": "V této místnosti nemáte žádná viditelná oznámení.",
|
||||
@@ -2306,7 +2306,7 @@
|
||||
"Backup version:": "Verze zálohy:",
|
||||
"Algorithm:": "Algoritmus:",
|
||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Tuto možnost můžete povolit, pokud bude místnost použita pouze pro spolupráci s interními týmy na vašem domovském serveru. Toto nelze později změnit.",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Blokovat komukoli, kdo není součástí serveru %(serverName)s, aby se nikdy nepřipojil do této místnosti.",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Blokovat komukoli, kdo není součástí serveru %(serverName)s, aby se připojil do této místnosti.",
|
||||
"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.": "Zálohujte šifrovací klíče s daty vašeho účtu pro případ, že ztratíte přístup k relacím. Vaše klíče budou zabezpečeny jedinečným klíčem pro obnovení.",
|
||||
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Níže můžete spravovat názvy a odhlásit se ze svých relací nebo <a>je ověřit v uživatelském profilu</a>.",
|
||||
"or another cross-signing capable Matrix client": "nebo jiný Matrix klient schopný cross-signing",
|
||||
@@ -2326,13 +2326,13 @@
|
||||
"Upload a file": "Nahrát soubor",
|
||||
"You've reached the maximum number of simultaneous calls.": "Dosáhli jste maximálního počtu souběžných hovorů.",
|
||||
"Too Many Calls": "Přiliš mnoho hovorů",
|
||||
"Community and user menu": "Nabídka komunity a uživatele",
|
||||
"Community and user menu": "Nabídka skupiny a uživatele",
|
||||
"User menu": "Uživatelská nabídka",
|
||||
"Switch theme": "Přepnout téma",
|
||||
"Switch to dark mode": "Přepnout do tmavého režimu",
|
||||
"Switch to light mode": "Přepnout do světlého režimu",
|
||||
"User settings": "Uživatelská nastavení",
|
||||
"Community settings": "Nastavení komunity",
|
||||
"Community settings": "Nastavení skupiny",
|
||||
"Confirm your recovery passphrase": "Potvrďte vaši frázi pro obnovení",
|
||||
"Repeat your recovery passphrase...": "Opakujte přístupovou frázi pro obnovení...",
|
||||
"Please enter your recovery passphrase a second time to confirm.": "Potvrďte prosím podruhé svou frázi pro obnovení.",
|
||||
@@ -2357,8 +2357,8 @@
|
||||
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Smazat adresu místnosti %(alias)s a odebrat %(name)s z adresáře?",
|
||||
"Self-verification request": "Požadavek na sebeověření",
|
||||
"%(creator)s created this DM.": "%(creator)s vytvořil tuto přímou zprávu.",
|
||||
"You do not have permission to create rooms in this community.": "Nemáte oprávnění k vytváření místností v této komunitě.",
|
||||
"Cannot create rooms in this community": "V této komunitě nelze vytvořit místnosti",
|
||||
"You do not have permission to create rooms in this community.": "Nemáte oprávnění k vytváření místností v této skupině.",
|
||||
"Cannot create rooms in this community": "V této skupině nelze vytvořit místnosti",
|
||||
"Great, that'll help people know it's you": "Skvělé, to pomůže lidem zjistit, že jste to vy",
|
||||
"Add a photo so people know it's you.": "Přidejte fotku, aby lidé věděli, že jste to vy.",
|
||||
"Explore Public Rooms": "Prozkoumat veřejné místnosti",
|
||||
@@ -2399,10 +2399,10 @@
|
||||
"Invite by email": "Pozvat emailem",
|
||||
"Comment": "Komentář",
|
||||
"Add comment": "Přidat komentář",
|
||||
"Update community": "Aktualizovat komunitu",
|
||||
"Update community": "Aktualizovat skupinu",
|
||||
"Create a room in %(communityName)s": "Vytvořit místnost v %(communityName)s",
|
||||
"Your server requires encryption to be enabled in private rooms.": "Váš server vyžaduje povolení šifrování v soukromých místnostech.",
|
||||
"An image will help people identify your community.": "Obrázek pomůže lidem identifikovat vaši komunitu.",
|
||||
"An image will help people identify your community.": "Obrázek pomůže lidem identifikovat vaši skupinu.",
|
||||
"Invite people to join %(communityName)s": "Pozvat lidi do %(communityName)s",
|
||||
"Send %(count)s invites|one": "Poslat %(count)s pozvánku",
|
||||
"Send %(count)s invites|other": "Poslat %(count)s pozvánek",
|
||||
@@ -2421,7 +2421,7 @@
|
||||
"Use email to optionally be discoverable by existing contacts.": "Pomocí e-mailu můžete být volitelně viditelní pro existující kontakty.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Použijte e-mail nebo telefon, abyste byli volitelně viditelní pro stávající kontakty.",
|
||||
"Sign in with SSO": "Přihlásit pomocí SSO",
|
||||
"Create community": "Vytvořit komunitu",
|
||||
"Create community": "Vytvořit skupinu",
|
||||
"Add an email to be able to reset your password.": "Přidejte email, abyste mohli obnovit své heslo.",
|
||||
"That phone number doesn't look quite right, please check and try again": "Toto telefonní číslo nevypadá úplně správně, zkontrolujte ho a zkuste to znovu",
|
||||
"Forgot password?": "Zapomenuté heslo?",
|
||||
@@ -2435,7 +2435,7 @@
|
||||
"Successfully restored %(sessionCount)s keys": "Úspěšně obnoveno %(sessionCount)s klíčů",
|
||||
"Keys restored": "Klíče byly obnoveny",
|
||||
"You're all caught up.": "Vše vyřízeno.",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Při aktualizaci komunity došlo k chybě. Server nemůže zpracovat váš požadavek.",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Při aktualizaci skupiny došlo k chybě. Server nemůže zpracovat váš požadavek.",
|
||||
"There are two ways you can provide feedback and help us improve %(brand)s.": "Jsou dva způsoby, jak můžete poskytnout zpětnou vazbu a pomoci nám vylepšit %(brand)s.",
|
||||
"Rate %(brand)s": "Ohodnotit %(brand)s",
|
||||
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "V této relaci jste již dříve používali novější verzi %(brand)s. Chcete-li tuto verzi znovu použít s šifrováním, budete se muset odhlásit a znovu přihlásit.",
|
||||
@@ -2446,7 +2446,7 @@
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Tato verze %(brand)s nepodporuje hledání v šifrovaných zprávách",
|
||||
"Information": "Informace",
|
||||
"%(count)s results|one": "%(count)s výsledek",
|
||||
"Explore community rooms": "Prozkoumat místnosti komunity",
|
||||
"Explore community rooms": "Prozkoumat místnosti skupin",
|
||||
"Role": "Role",
|
||||
"Madagascar": "Madagaskar",
|
||||
"Macedonia": "Makedonie",
|
||||
@@ -2751,5 +2751,222 @@
|
||||
"Unable to query secret storage status": "Nelze zjistit stav úložiště klíčů",
|
||||
"Update %(brand)s": "Aktualizovat %(brand)s",
|
||||
"You can also set up Secure Backup & manage your keys in Settings.": "Zabezpečené zálohování a správu klíčů můžete také nastavit v Nastavení.",
|
||||
"Set a Security Phrase": "Nastavit bezpečnostní frázi"
|
||||
"Set a Security Phrase": "Nastavit bezpečnostní frázi",
|
||||
"Use this when referencing your community to others. The community ID cannot be changed.": "Použijte toto, když odkazujete svou skupinu na ostatní. ID skupiny nelze změnit.",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Udejte prosím co nejvíce podrobností, abychom mohli problém vystopovat.",
|
||||
"Start a conversation with someone using their name or username (like <userId/>).": "Začněte konverzaci s někým pomocí jeho jména nebo uživatelského jména (například <userId />).",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Níže se podělte s vašimi zkušenostmi s %(brand)s.",
|
||||
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s nastavil seznam přístupů serveru pro tuto místnost.",
|
||||
"What's the name of your community or team?": "Jak se jmenuje vaše skupina nebo tým?",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Pozvěte někoho pomocí svého jména, uživatelského jména (například <userId />) nebo <a>sdílejte tuto místnost</a>.",
|
||||
"Confirm by comparing the following with the User Settings in your other session:": "Potvrďte porovnáním následujícího s uživatelským nastavením v jiné relaci:",
|
||||
"Data on this screen is shared with %(widgetDomain)s": "Data na této obrazovce jsou sdílena s %(widgetDomain)s",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Jen upozornění, pokud nepřidáte e-mail a zapomenete heslo, můžete <b>trvale ztratit přístup ke svému účtu</b>.",
|
||||
"Your area is experiencing difficulties connecting to the internet.": "Ve vaší oblasti dochází k problémům s připojením k internetu.",
|
||||
"A connection error occurred while trying to contact the server.": "Při pokusu o kontakt se serverem došlo k chybě připojení.",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "Server není nakonfigurován tak, aby indikoval, v čem je problém (CORS).",
|
||||
"Recent changes that have not yet been received": "Nedávné změny, které dosud nebyly přijaty",
|
||||
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Nelze získat přístup k úložišti klíčů. Ověřte, zda jste zadali správnou přístupovou frázi pro obnovení.",
|
||||
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Pokračujte zadáním bezpečnostní fráze nebo <button>použijte váš bezpečnostní klíč</button> pro pokračování.",
|
||||
"Restoring keys from backup": "Obnovení klíčů ze zálohy",
|
||||
"Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Zálohu nebylo možné dešifrovat pomocí této přístupové fráze pro obnovení: ověřte, zda jste zadali správnou přístupovou frázi pro obnovení.",
|
||||
"%(completed)s of %(total)s keys restored": "Obnoveno %(completed)s z %(total)s klíčů",
|
||||
"Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Uložte bezpečnostní klíč někam na bezpečné místo, například do správce hesel nebo do trezoru, který slouží k ochraně vašich šifrovaných dat.",
|
||||
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Váš klíč pro obnovení je bezpečnostní sítí - můžete jej použít k obnovení přístupu k šifrovaným zprávám, pokud zapomenete přístupovou frázi pro obnovení.",
|
||||
"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.": "Byla zjištěna data ze starší verze %(brand)s. To bude mít za následek nefunkčnost end-to-end kryptografie ve starší verzi. End-to-end šifrované zprávy vyměněné nedávno při používání starší verze nemusí být v této verzi dešifrovatelné. To může také způsobit selhání zpráv vyměňovaných s touto verzí. Pokud narazíte na problémy, odhlaste se a znovu se přihlaste. Chcete-li zachovat historii zpráv, exportujte a znovu importujte klíče.",
|
||||
"Failed to find the general chat for this community": "Nepodařilo se najít obecný chat pro tuto skupinu",
|
||||
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Na náš server uložíme zašifrovanou kopii vašich klíčů. Zabezpečte zálohu pomocí přístupové fráze pro obnovení.",
|
||||
"Enter your recovery passphrase a second time to confirm it.": "Zadejte podruhé přístupovou frázi pro obnovení a potvrďte ji.",
|
||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Pokud nyní nebudete pokračovat, můžete ztratit šifrované zprávy a data, pokud ztratíte přístup ke svým přihlašovacím údajům.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Zprávy v této místnosti jsou šifrovány end-to-end. Když se lidé připojí, můžete je ověřit v jejich profilu, stačí klepnout na jejich avatara.",
|
||||
"Revoke permissions": "Odvolat oprávnění",
|
||||
"Continuing without email": "Pokračuje se bez e-mailu",
|
||||
"You can change this later if needed.": "V případě potřeby to můžete později změnit.",
|
||||
"Video conference started by %(senderName)s": "Videokonferenci byla zahájena uživatelem %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Videokonference byla aktualizována uživatelem %(senderName)s",
|
||||
"Video conference ended by %(senderName)s": "Videokonference byla ukončena uživatelem %(senderName)s",
|
||||
"Unpin": "Odepnout",
|
||||
"Fill Screen": "Vyplnit obrazovku",
|
||||
"Voice Call": "Hlasový hovor",
|
||||
"Video Call": "Videohovor",
|
||||
"%(senderName)s ended the call": "%(senderName)s ukončil/a hovor",
|
||||
"You ended the call": "Ukončili jste hovor",
|
||||
"New version of %(brand)s is available": "K dispozici je nová verze %(brand)s",
|
||||
"Error leaving room": "Při opouštění místnosti došlo k chybě",
|
||||
"See messages posted to your active room": "Zobrazit zprávy odeslané do vaší aktivní místnosti",
|
||||
"See messages posted to this room": "Zobrazit zprávy odeslané do této místnosti",
|
||||
"See when the avatar changes in your active room": "Podívejte se, kdy se změní avatar ve vaší aktivní místnosti",
|
||||
"Change the avatar of your active room": "Změňte avatar vaší aktivní místnosti",
|
||||
"See when the avatar changes in this room": "Podívejte se, kdy se změní avatar v této místnosti",
|
||||
"Change the avatar of this room": "Změňte avatar této místnosti",
|
||||
"See when the name changes in your active room": "Podívejte se, kdy se ve vaší aktivní místnosti změní název",
|
||||
"Change the name of your active room": "Změňte název své aktivní místnosti",
|
||||
"Change the name of this room": "Změňte název této místnosti",
|
||||
"A browser extension is preventing the request.": "Rozšíření prohlížeče brání požadavku.",
|
||||
"Your firewall or anti-virus is blocking the request.": "Váš firewall nebo antivirový program blokuje požadavek.",
|
||||
"The server (%(serverName)s) took too long to respond.": "Serveru (%(serverName)s) trvalo příliš dlouho, než odpověděl.",
|
||||
"Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Váš server neodpovídá na některé vaše požadavky. Níže jsou některé z nejpravděpodobnějších důvodů.",
|
||||
"<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 add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML pro vaši stránku skupiny</h1>\n<p>\n Pomocí popisu můžete představit nové členy skupiny nebo distribuovat\n některé důležité <a href=\"foo\">odkazy</a>\n</p>\n<p>\n Můžete dokonce přidat obrázky pomocí Matrix URL <img src=\"mxc://url\" />\n</p>\n",
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Soukromé místnosti lze najít a připojit se do nich pouze na pozvání. Veřejné místnosti může najít a připojit se do nich kdokoli v této skupině.",
|
||||
"Community ID: +<localpart />:%(domain)s": "ID skupiny: +<localpart />:%(domain)s",
|
||||
"The operation could not be completed": "Operace nemohla být dokončena",
|
||||
"Failed to save your profile": "Váš profil se nepodařilo uložit",
|
||||
"%(peerName)s held the call": "%(peerName)s podržel hovor",
|
||||
"You held the call <a>Resume</a>": "Podrželi jste hovor <a>Pokračovat</a>",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Můžete použít vlastní volbu serveru a přihlásit se k jiným Matrix serverům zadáním adresy URL domovského serveru. To vám umožní používat Element s existujícím Matrix účtem na jiném domovském serveru.",
|
||||
"Unpin a widget to view it in this panel": "Odepněte widget, aby mohl být zobrazen v tomto panelu",
|
||||
"You can only pin up to %(count)s widgets|other": "Můžete připnout až %(count)s widgetů",
|
||||
"Edit widgets, bridges & bots": "Upravujte widgety, mosty a boty",
|
||||
"a device cross-signing signature": "zařízení používající cross-signing podpis",
|
||||
"This widget would like to:": "Tento widget by chtěl:",
|
||||
"Modal Widget": "Modální widget",
|
||||
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Toto můžete deaktivovat, pokud bude místnost použita pro spolupráci s externími týmy, které mají svůj vlastní domovský server. Toto nelze později změnit.",
|
||||
"Join the conference from the room information card on the right": "Připojte se ke konferenci z informační karty místnosti napravo",
|
||||
"Join the conference at the top of this room": "Připojte se ke konferenci v horní části této místnosti",
|
||||
"There are advanced notifications which are not shown here.": "Existují pokročilá oznámení, která se zde nezobrazují.",
|
||||
"Enable advanced debugging for the room list": "Povolit pokročilé ladění pro seznam místností",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Bezpečně uloží zašifrované zprávy v místním úložišti, aby se mohly objevit ve výsledcích vyhledávání, využívá se %(size)s k ukládání zpráv z %(rooms)s místnosti.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Bezpečně uloží zašifrované zprávy v místním úložišti, aby se mohly objevit ve výsledcích vyhledávání, využívá se %(size)s k ukládání zpráv z %(rooms)s místností.",
|
||||
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Možná jste je nakonfigurovali v jiném klientovi než %(brand)s. Nelze je zobrazit v %(brand)s, ale stále platí.",
|
||||
"well formed": "ve správném tvaru",
|
||||
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "Při vytváření vaší skupiny došlo k chybě. Název může být již obsazen nebo server nemůže zpracovat váš požadavek.",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "Zobrazit události <b>%(eventType)s</b> zveřejněné v této místnosti",
|
||||
"Send stickers to your active room as you": "Poslat nálepky do vaší aktivní místnosti jako vy",
|
||||
"Send stickers to this room as you": "Poslat samolepky jako vy do této místnosti",
|
||||
"Change the topic of your active room": "Změnit téma vaší aktivní místnosti",
|
||||
"Change which room you're viewing": "Změnit kterou místnost si prohlížíte",
|
||||
"Send stickers into your active room": "Poslat nálepky do vaší aktivní místnosti",
|
||||
"Send stickers into this room": "Poslat nálepky do této místnosti",
|
||||
"Send messages as you in this room": "Poslat zprávy jako vy v této místnosti",
|
||||
"Send messages as you in your active room": "Poslat zprávy jako vy ve vaší aktivní místnosti",
|
||||
"Send text messages as you in this room": "Poslat textové zprávy jako vy v této místnosti",
|
||||
"Send text messages as you in your active room": "Poslat textové zprávy jako vy ve vaší aktivní místnosti",
|
||||
"See text messages posted to this room": "Podívat se na textové zprávy odeslané do této místnosti",
|
||||
"See text messages posted to your active room": "Podívat se na textové zprávy odeslané do vaší aktivní místnosti",
|
||||
"Send images as you in this room": "Poslat obrázky jako vy v této místnosti",
|
||||
"Send images as you in your active room": "Poslat obrázky jako vy ve vaší aktivní místnosti",
|
||||
"See images posted to this room": "Podívat se na obrázky zveřejněné v této místnosti",
|
||||
"See images posted to your active room": "Podívat se na obrázky zveřejněné ve vaší aktivní místnosti",
|
||||
"Send videos as you in this room": "Poslat videa jako vy v této místnosti",
|
||||
"Send videos as you in your active room": "Podívat se na videa jako vy ve vaší aktivní místnosti",
|
||||
"See videos posted to this room": "Podívat se na videa zveřejněná v této místnosti",
|
||||
"See videos posted to your active room": "Podívat se na videa zveřejněná ve vaší aktivní místnosti",
|
||||
"Send general files as you in this room": "Poslat obecné soubory jako vy v této místnosti",
|
||||
"Send general files as you in your active room": "Poslat obecné soubory jako vy ve vaší aktivní místnosti",
|
||||
"See general files posted to this room": "Prohlédnout obecné soubory zveřejněné v této místnosti",
|
||||
"See general files posted to your active room": "Prohlédnout obecné soubory zveřejněné ve vaší aktivní místnosti",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Poslat zprávy <b>%(msgtype)s</b> jako vy v této místnosti",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Poslat zprávy <b>%(msgtype)s</b> jako vy ve vašá aktivní místnosti",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "Prohlédnout zprávy <b>%(msgtype)s</b> zveřejněné v této místnosti",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "Prohlédnout zprávy <b>%(msgtype)s</b> zveřejněné ve vaší aktivní místnosti",
|
||||
"Render LaTeX maths in messages": "Ve zprávách vykreslit matematické výrazy LaTeXu",
|
||||
"New spinner design": "Nový design ukazatele zpracování",
|
||||
"Show message previews for reactions in DMs": "Zobrazit náhledy zpráv pro reakce v přímých zprávách",
|
||||
"Show message previews for reactions in all rooms": "Zobrazit náhledy zpráv pro reakce ve všech místnostech",
|
||||
"Sends the given message with confetti": "Pošle zprávu s konfetami",
|
||||
"sends confetti": "pošle konfety",
|
||||
"Sends the given message with fireworks": "Pošle zprávu s ohňostrojem",
|
||||
"sends fireworks": "pošle ohňostroj",
|
||||
"The <b>%(capability)s</b> capability": "Schopnost <b>%(capability)s</b>",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "Zobrazit události <b>%(eventType)s</b> odeslané do vaší aktivní místnosti",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Poslat události <b>%(eventType)s</b> jako vy ve vaší aktivní místnosti",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Poslat události <b>%(eventType)s</b> jako vy v této místnosti",
|
||||
"with an empty state key": "s prázdným stavovým klíčem",
|
||||
"with state key %(stateKey)s": "se stavovým klíčem %(stateKey)s",
|
||||
"User signing private key:": "Podpisový klíč uživatele:",
|
||||
"Self signing private key:": "Vlastní podpisový klíč:",
|
||||
"Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Vloží ┬──┬ ノ( ゜-゜ノ) na začátek zprávy",
|
||||
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Vloží (╯°□°)╯︵ ┻━┻ na začátek zprávy",
|
||||
"Remain on your screen while running": "Při běhu zůstává na obrazovce",
|
||||
"Remain on your screen when viewing another room, when running": "Při prohlížení jiné místnosti zůstává při běhu na obrazovce",
|
||||
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s změnil/a seznam přístupů serveru pro tuto místnost.",
|
||||
"Offline encrypted messaging using dehydrated devices": "Offline šifrovaná komunikace pomocí dehydrovaných zařízení",
|
||||
"See emotes posted to your active room": "Prohlédněte si emoji zveřejněné ve vaší aktivní místnosti",
|
||||
"See emotes posted to this room": "Prohlédněte si emoji zveřejněné v této místnosti",
|
||||
"Send emotes as you in your active room": "Poslat emoji jako vy ve své aktivní místnosti",
|
||||
"Send emotes as you in this room": "Poslat emoji jako vy v této místnosti",
|
||||
"See when anyone posts a sticker to your active room": "Podívejte se, kdy někdo zveřejní nálepku ve vaší aktivní místnosti",
|
||||
"See when a sticker is posted in this room": "Podívejte se, kdy je zveřejněna nálepka v této místnosti",
|
||||
"See when the name changes in this room": "Podívejte se, kdy se změní název v této místnosti",
|
||||
"See when the topic changes in your active room": "Podívejte se, kdy se změní téma ve vaší aktivní místnosti",
|
||||
"See when the topic changes in this room": "Podívejte se, kdy se změní téma v této místnosti",
|
||||
"%(name)s on hold": "%(name)s podržen",
|
||||
"You held the call <a>Switch</a>": "Podrželi jste hovor <a>Přepnout</a>",
|
||||
"sends snowfall": "pošle sněžení",
|
||||
"Sends the given message with snowfall": "Pošle zprávu se sněžením",
|
||||
"You have no visible notifications.": "Nejsou dostupná žádná oznámení.",
|
||||
"Transfer": "Přepojit",
|
||||
"Failed to transfer call": "Hovor se nepodařilo přepojit",
|
||||
"A call can only be transferred to a single user.": "Hovor lze přepojit pouze jednomu uživateli.",
|
||||
"There was an error finding this widget.": "Při hledání tohoto widgetu došlo k chybě.",
|
||||
"Active Widgets": "Aktivní widgety",
|
||||
"Open dial pad": "Otevřít číselník",
|
||||
"Start a Conversation": "Zahájit konverzaci",
|
||||
"Dial pad": "Číselník",
|
||||
"There was an error looking up the phone number": "Při vyhledávání telefonního čísla došlo k chybě",
|
||||
"Unable to look up phone number": "Nelze nalézt telefonní číslo",
|
||||
"Channel: <channelLink/>": "Kanál: <channelLink/>",
|
||||
"Change which room, message, or user you're viewing": "Změňte, kterou místnost, zprávu nebo uživatele si prohlížíte",
|
||||
"Workspace: <networkLink/>": "Pracovní oblast: <networkLink/>",
|
||||
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Pokud jste zapomněli bezpečnostní klíč, můžete <button>nastavit nové možnosti obnovení</button>",
|
||||
"If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Pokud jste zapomněli bezpečnostní frázi, můžete <button1>použít bezpečnostní klíč</button1> nebo <button2>nastavit nové možnosti obnovení</button2>",
|
||||
"Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Zálohu nebylo možné dešifrovat pomocí této bezpečnostní fráze: ověřte, zda jste zadali správnou bezpečnostní frázi.",
|
||||
"Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Zálohu nebylo možné dešifrovat pomocí tohoto bezpečnostního klíče: ověřte, zda jste zadali správný bezpečnostní klíč.",
|
||||
"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 Security Key.": "Zálohujte šifrovací klíče s daty účtu pro případ, že ztratíte přístup k relacím. Vaše klíče budou zabezpečeny jedinečným bezpečnostním klíčem.",
|
||||
"Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Váš bezpečnostní klíč je bezpečnostní síť - můžete ji použít k obnovení přístupu k šifrovaným zprávám, pokud zapomenete bezpečnostní frázi.",
|
||||
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Na náš server uložíme zašifrovanou kopii vašich klíčů. Zabezpečte zálohu pomocí bezpečnostní fráze.",
|
||||
"Access your secure message history and set up secure messaging by entering your Security Key.": "Vstupte do historie zabezpečených zpráv a nastavte zabezpečené zprávy zadáním bezpečnostního klíče.",
|
||||
"Access your secure message history and set up secure messaging by entering your Security Phrase.": "Vstupte do historie zabezpečených zpráv a nastavte zabezpečené zprávy zadáním bezpečnostní fráze.",
|
||||
"Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Nelze získat přístup k zabezpečenému úložišti. Ověřte, zda jste zadali správnou bezpečnostní frázi.",
|
||||
"We recommend you change your password and Security Key in Settings immediately": "Doporučujeme vám okamžitě změnit heslo a bezpečnostní klíč v Nastavení",
|
||||
"This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Tato relace zjistila, že byla odstraněna vaše bezpečnostní fráze a klíč pro zabezpečené zprávy.",
|
||||
"A new Security Phrase and key for Secure Messages have been detected.": "Byla zjištěna nová bezpečnostní fráze a klíč pro zabezpečené zprávy.",
|
||||
"Make a copy of your Security Key": "Vytvořte kopii bezpečnostního klíče",
|
||||
"Confirm your Security Phrase": "Potvrďte svou bezpečnostní frázi",
|
||||
"Secure your backup with a Security Phrase": "Zabezpečte zálohu pomocí bezpečnostní fráze",
|
||||
"Repeat your Security Phrase...": "Zopakujte vaši bezpečnostní frázi...",
|
||||
"Set up with a Security Key": "Nastavit pomocí bezpečnostního klíče",
|
||||
"Use Security Key": "Použít bezpečnostní klíč",
|
||||
"This looks like a valid Security Key!": "Vypadá to jako platný bezpečnostní klíč!",
|
||||
"Invalid Security Key": "Neplatný bezpečnostní klíč",
|
||||
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Váš bezpečnostní klíč byl <b>zkopírován do schránky</b>, vložte jej do:",
|
||||
"Your Security Key is in your <b>Downloads</b> folder.": "Váš bezpečnostní klíč je ve složce <b>Stažené soubory</b>.",
|
||||
"Your Security Key": "Váš bezpečnostní klíč",
|
||||
"Please enter your Security Phrase a second time to confirm.": "Potvrďte prosím svou bezpečnostní frázi.",
|
||||
"Great! This Security Phrase looks strong enough.": "Skvělé! Tato bezpečnostní fráze vypadá dostatečně silně.",
|
||||
"Use Security Key or Phrase": "Použijte bezpečnostní klíč nebo frázi",
|
||||
"Not a valid Security Key": "Neplatný bezpečnostní klíč",
|
||||
"Enter Security Key": "Zadejte bezpečnostní klíč",
|
||||
"Enter Security Phrase": "Zadejte bezpečnostní frázi",
|
||||
"Incorrect Security Phrase": "Nesprávná bezpečnostní fráze",
|
||||
"Security Key mismatch": "Neshoda bezpečnostního klíče",
|
||||
"Wrong Security Key": "Špatný bezpečnostní klíč",
|
||||
"Set my room layout for everyone": "Nastavit všem rozložení mé místnosti",
|
||||
"%(senderName)s has updated the widget layout": "%(senderName)s aktualizoval rozložení widgetu",
|
||||
"Search (must be enabled)": "Hledat (musí být povoleno)",
|
||||
"Remember this": "Zapamatujte si toto",
|
||||
"The widget will verify your user ID, but won't be able to perform actions for you:": "Widget ověří vaše uživatelské ID, ale nebude za vás moci provádět akce:",
|
||||
"Allow this widget to verify your identity": "Povolte tomuto widgetu ověřit vaši identitu",
|
||||
"Use Ctrl + F to search": "Hledejte pomocí Ctrl + F",
|
||||
"Use Command + F to search": "Hledejte pomocí Command + F",
|
||||
"Converts the DM to a room": "Převede přímou zprávu na místnost",
|
||||
"Converts the room to a DM": "Převede místnost na přímou zprávu",
|
||||
"Mobile experience": "Zážitek na mobilních zařízeních",
|
||||
"Element Web is currently experimental on mobile. The native apps are recommended for most people.": "Element Web je v současné době experimentální na mobilních zařízeních. Nativní aplikace se doporučují pro většinu lidí.",
|
||||
"Use app for a better experience": "Pro lepší zážitek použijte aplikaci",
|
||||
"Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web je experimentální na mobilních zařízeních. Pro lepší zážitek a nejnovější funkce použijte naši bezplatnou nativní aplikaci.",
|
||||
"Use app": "Použijte aplikaci",
|
||||
"Something went wrong in confirming your identity. Cancel and try again.": "Při ověřování vaší identity se něco pokazilo. Zrušte to a zkuste to znovu.",
|
||||
"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.": "Váš domovský server odmítl váš pokus o přihlášení. Může to být způsobeno tím, že věci trvají příliš dlouho. Prosím zkuste to znovu. Pokud to bude pokračovat, obraťte se na správce domovského serveru.",
|
||||
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Váš domovský server nebyl dosažitelný a nemohl vás přihlásit. Zkuste to prosím znovu. Pokud to bude pokračovat, obraťte se na správce domovského serveru.",
|
||||
"Try again": "Zkuste to znovu",
|
||||
"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.": "Požádali jsme prohlížeč, aby si pamatoval, který domovský server používáte k přihlášení, ale váš prohlížeč to bohužel zapomněl. Přejděte na přihlašovací stránku a zkuste to znovu.",
|
||||
"We couldn't log you in": "Nemohli jsme vás přihlásit",
|
||||
"Show stickers button": "Tlačítko Zobrazit nálepky",
|
||||
"Windows": "Okna",
|
||||
"Screens": "Obrazovky",
|
||||
"Share your screen": "Sdílejte svou obrazovku",
|
||||
"Expand code blocks by default": "Ve výchozím nastavení rozbalit bloky kódu",
|
||||
"Show line numbers in code blocks": "Zobrazit čísla řádků v blocích kódu",
|
||||
"Recently visited rooms": "Nedávno navštívené místnosti"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user