Merge remote-tracking branch 'upstream/develop' into feature/incoming-call-toast

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-08-04 09:21:45 +02:00
commit d479373af9
No known key found for this signature in database
GPG key ID: CC823428E9B582FB
182 changed files with 5872 additions and 2200 deletions

View file

@ -56,7 +56,6 @@ limitations under the License.
import React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
import dis from './dispatcher/dispatcher';
@ -80,7 +79,6 @@ 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 VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
@ -132,14 +130,9 @@ interface ThirdpartyLookupResponse {
fields: ThirdpartyLookupResponseFields;
}
// Unlike 'CallType' in js-sdk, this one includes screen sharing
// (because a screen sharing call is only a screen sharing call to the caller,
// to the callee it's just a video call, at least as far as the current impl
// is concerned).
export enum PlaceCallType {
Voice = 'voice',
Video = 'video',
ScreenSharing = 'screensharing',
}
export enum CallHandlerEvent {
@ -494,28 +487,18 @@ export default class CallHandler extends EventEmitter {
break;
case CallState.Ended:
{
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
const hangupReason = call.hangupReason;
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && (
call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
)) {
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
this.play(AudioID.Busy);
let title;
let description;
if (call.hangupReason === CallErrorCode.UserHangup) {
title = _t("Call Declined");
description = _t("The other party declined the call.");
} else if (call.hangupReason === CallErrorCode.UserBusy) {
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
if (call.hangupReason === CallErrorCode.UserBusy) {
title = _t("User Busy");
description = _t("The user you called is busy.");
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
title = _t("Call Failed");
// XXX: full stop appended as some relic here, but these
// strings need proper input from design anyway, so let's
// not change this string until we have a proper one.
description = _t('The remote side failed to pick up') + '.';
} else {
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
title = _t("Call Failed");
description = _t("The call could not be established");
}
@ -524,7 +507,7 @@ export default class CallHandler extends EventEmitter {
title, description,
});
} else if (
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
) {
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title: _t("Answered Elsewhere"),
@ -754,25 +737,6 @@ export default class CallHandler extends EventEmitter {
call.placeVoiceCall();
} else if (type === 'video') {
call.placeVideoCall();
} else if (type === PlaceCallType.ScreenSharing) {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
this.removeCallForRoom(roomId);
console.log("Can't capture screen: " + screenCapErrorString);
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
title: _t('Unable to capture screen'),
description: screenCapErrorString,
});
return;
}
call.placeScreenSharingCall(
async (): Promise<DesktopCapturerSource> => {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
},
);
} else {
console.error("Unknown conf call type: " + type);
}

View file

@ -146,23 +146,23 @@ export default class IdentityAuthClient {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
<div>
<p>{ _t(
"This action requires accessing the default identity server " +
title: _t("Identity server has no terms of service"),
description: (
<div>
<p>{ _t(
"This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.", {},
{
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
},
) }</p>
<p>{ _t(
"Only continue if you trust the owner of the server.",
) }</p>
</div>
),
button: _t("Trust"),
{
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
},
) }</p>
<p>{ _t(
"Only continue if you trust the owner of the server.",
) }</p>
</div>
),
button: _t("Trust"),
});
const [confirmed] = await finished;
if (confirmed) {

View file

@ -25,11 +25,44 @@ import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher';
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from "./MatrixClientPeg";
// These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values
// to avoid the expense of looking up translations when they're not needed.
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
isVoice = false;
}
const isSupported = MatrixClientPeg.get().supportsVoip();
// This ladder could be reduced down to a couple string variables, however other languages
// can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) {
return () => _t("%(senderName)s placed a voice call.", {
senderName: getSenderName(),
});
} else if (isVoice && !isSupported) {
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
senderName: getSenderName(),
});
} else if (!isVoice && isSupported) {
return () => _t("%(senderName)s placed a video call.", {
senderName: getSenderName(),
});
} else if (!isVoice && !isSupported) {
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
senderName: getSenderName(),
});
}
}
function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null {
// XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender();
@ -567,6 +600,7 @@ interface IHandlers {
const handlers: IHandlers = {
'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent,
};
const stateHandlers: IHandlers = {

View file

@ -163,7 +163,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
modifiers: [Modifiers.SHIFT],
key: Key.PAGE_UP,
}],
description: _td("Jump to oldest unread message"),
description: _td("Jump to oldest unread message"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],

View file

@ -74,6 +74,14 @@ export default class CallEventGrouper extends EventEmitter {
return this.hangup?.getContent()?.reason;
}
public get rejectParty(): string {
return this.reject?.getSender();
}
public get gotRejected(): boolean {
return Boolean(this.reject);
}
/**
* Returns true if there are only events from the other side - we missed the call
*/

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { CSSProperties, RefObject, useRef, useState } from "react";
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
@ -80,6 +80,10 @@ export interface IProps extends IPosition {
managed?: boolean;
wrapperClassName?: string;
// If true, this context menu will be mounted as a child to the parent container. Otherwise
// it will be mounted to a container at the root of the DOM.
mountAsChild?: boolean;
// Function to be called on menu close
onFinished();
// on resize callback
@ -390,7 +394,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
}
render(): React.ReactChild {
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
if (this.props.mountAsChild) {
// Render as a child of the current parent
return this.renderMenu();
} else {
// Render as a child of a container at the root of the DOM
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
}
}
}
@ -461,10 +471,14 @@ type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val:
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
const button = useRef<T>(null);
const [isOpen, setIsOpen] = useState(false);
const open = () => {
const open = (ev?: SyntheticEvent) => {
ev?.preventDefault();
ev?.stopPropagation();
setIsOpen(true);
};
const close = () => {
const close = (ev?: SyntheticEvent) => {
ev?.preventDefault();
ev?.stopPropagation();
setIsOpen(false);
};

View file

@ -618,7 +618,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
for (const Grouper of groupers) {
if (Grouper.canStartGroup(this, mxEv)) {
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile);
grouper = new Grouper(
this,
mxEv,
prevEvent,
lastShownEvent,
this.props.layout,
nextEvent,
nextTile,
);
}
}
if (!grouper) {
@ -981,6 +989,7 @@ abstract class BaseGrouper {
public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout,
public readonly nextEvent?: MatrixEvent,
public readonly nextEventTile?: MatrixEvent,
) {
@ -1107,6 +1116,7 @@ class CreationGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={summaryText}
layout={this.layout}
>
{ eventTiles }
</EventListSummary>,
@ -1134,10 +1144,11 @@ class RedactionGrouper extends BaseGrouper {
ev: MatrixEvent,
prevEvent: MatrixEvent,
lastShownEvent: MatrixEvent,
layout: Layout,
nextEvent: MatrixEvent,
nextEventTile: MatrixEvent,
) {
super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
this.events = [ev];
}
@ -1202,6 +1213,7 @@ class RedactionGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
layout={this.layout}
>
{ eventTiles }
</EventListSummary>,
@ -1230,8 +1242,9 @@ class MemberGrouper extends BaseGrouper {
public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout,
) {
super(panel, event, prevEvent, lastShownEvent);
super(panel, event, prevEvent, lastShownEvent, layout);
this.events = [event];
}
@ -1306,6 +1319,7 @@ class MemberGrouper extends BaseGrouper {
events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
layout={this.layout}
>
{ eventTiles }
</MemberEventListSummary>,

View file

@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component<IProps> {
private readonly itemlist = createRef<HTMLOListElement>();
private unmounted = false;
private scrollTimeout: Timer;
// Are we currently trying to backfill?
private isFilling: boolean;
// Is the current fill request caused by a props update?
private isFillingDueToPropsUpdate = false;
// Did another request to check the fill state arrive while we were trying to backfill?
private fillRequestWhileRunning: boolean;
// Is that next fill request scheduled because of a props update?
private pendingFillDueToPropsUpdate: boolean;
private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: number;
@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component<IProps> {
// adding events to the top).
//
// This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll();
this.checkScroll(true);
this.updatePreventShrinking();
}
@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component<IProps> {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
public checkScroll = () => {
public checkScroll = (isFromPropsUpdate = false) => {
if (this.unmounted) {
return;
}
this.restoreSavedScrollState();
this.checkFillState();
this.checkFillState(0, isFromPropsUpdate);
};
// return true if the content is fully scrolled down right now; else false.
@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component<IProps> {
}
// check the scroll state and send out backfill requests if necessary.
public checkFillState = async (depth = 0): Promise<void> => {
public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise<void> => {
if (this.unmounted) {
return;
}
@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component<IProps> {
// don't allow more than 1 chain of calls concurrently
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
// However, we make an exception for when we're already filling due to a
// props (or children) update, because very often the children include
// spinners to say whether we're paginating or not, so this would cause
// infinite paginating.
if (isFirstCall) {
if (this.isFilling) {
if (this.isFilling && !this.isFillingDueToPropsUpdate) {
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
this.fillRequestWhileRunning = true;
this.pendingFillDueToPropsUpdate = isFromPropsUpdate;
return;
}
debuglog("isFilling: setting");
this.isFilling = true;
this.isFillingDueToPropsUpdate = isFromPropsUpdate;
}
const itemlist = this.itemlist.current;
@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component<IProps> {
if (isFirstCall) {
debuglog("isFilling: clearing");
this.isFilling = false;
this.isFillingDueToPropsUpdate = false;
}
if (this.fillRequestWhileRunning) {
const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
this.fillRequestWhileRunning = false;
this.checkFillState();
this.pendingFillDueToPropsUpdate = false;
this.checkFillState(0, refillDueToPropsUpdate);
}
};

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
import classNames from "classnames";
@ -44,11 +43,13 @@ import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms";
import { useDispatcher } from "../../hooks/useDispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
interface IHierarchyProps {
space: Room;
initialText?: string;
refreshToken?: any;
additionalButtons?: ReactNode;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
}
@ -315,18 +316,25 @@ export const HierarchyLevel = ({
</React.Fragment>;
};
// mutate argument refreshToken to force a reload
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
export const useSpaceSummary = (space: Room): [
null,
ISpaceSummaryRoom[],
Map<string, Map<string, ISpaceSummaryEvent>>?,
Map<string, Set<string>>?,
Map<string, Set<string>>?,
] | [Error] => {
// crude temporary refresh token approach until we have pagination and rework the data flow here
const [refreshToken, setRefreshToken] = useState(0);
useDispatcher(defaultDispatcher, (payload => {
if (payload.action === Action.UpdateSpaceHierarchy) {
setRefreshToken(t => t + 1);
}
}));
// TODO pagination
return useAsyncMemo(async () => {
try {
const data = await cli.getSpaceSummary(space.roomId);
const data = await space.client.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
const childParentRelations = new EnhancedMap<string, Set<string>>();
@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space,
initialText = "",
showRoom,
refreshToken,
additionalButtons,
children,
}) => {
@ -364,7 +371,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space);
const roomsMap = useMemo(() => {
if (!rooms) return null;

View file

@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceSettings,
} from "../../utils/space";
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
import MemberAvatar from "../views/avatars/MemberAvatar";
import { useStateToggle } from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile";
import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
import {
AddExistingToSpace,
defaultDmsRenderer,
defaultRoomsRenderer,
defaultSpacesRenderer,
} from "../views/dialogs/AddExistingToSpaceDialog";
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
@ -62,10 +72,8 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
interface IProps {
space: Room;
@ -92,28 +100,6 @@ enum Phase {
PrivateExistingRooms,
}
// XXX: Temporary for the Spaces Beta only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<hr />
<div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces",
});
}}>
{ _t("Feedback") }
</AccessibleButton>
</div>
</div>;
};
const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room);
const count = members.length;
@ -307,7 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div>;
};
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
@ -331,24 +317,32 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
closeMenu();
if (await showCreateNewRoom(space)) {
onNewRoomAdded();
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={async (e) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
const [added] = await showAddExistingRooms(space);
if (added) {
onNewRoomAdded();
}
showAddExistingRooms(space);
}}
/>
<IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
@ -389,11 +383,9 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButton;
if (canAddRooms) {
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
addRoomButton = <SpaceLandingAddButton space={space} />;
}
let settingsButton;
@ -416,6 +408,7 @@ const SpaceLanding = ({ space }) => {
};
return <div className="mx_SpaceRoomView_landing">
<SpaceFeedbackPrompt />
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<div className="mx_SpaceRoomView_landing_name">
<RoomName room={space}>
@ -440,15 +433,8 @@ const SpaceLanding = ({ space }) => {
</div>
) }
</RoomTopic>
<SpaceFeedbackPrompt />
<hr />
<SpaceHierarchy
space={space}
showRoom={showRoom}
refreshToken={refreshToken}
additionalButtons={addRoomButton}
/>
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
</div>;
};
@ -531,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -550,11 +535,12 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ _t("Skip for now") }
</AccessibleButton>
}
filterPlaceholder={_t("Search for rooms or spaces")}
onFinished={onFinished}
roomsRenderer={defaultRoomsRenderer}
spacesRenderer={defaultSpacesRenderer}
dmsRenderer={defaultDmsRenderer}
/>
<div className="mx_SpaceRoomView_buttons" />
<SpaceFeedbackPrompt />
</div>;
};
@ -574,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -603,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
</AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
<p>{ _t("We're working on this, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -728,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
}
private resetRecaptcha() {
if (this.captchaWidgetId !== null) {
global.grecaptcha.reset(this.captchaWidgetId);
if (this.captchaWidgetId) {
global?.grecaptcha?.reset(this.captchaWidgetId);
}
}

View file

@ -27,6 +27,8 @@ import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag";
// XXX: Keep this around for re-use in future Betas
interface IProps {
title?: string;
featureId: string;

View file

@ -49,6 +49,13 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.props.onFinished();
};
onKeyDown = (ev) => {
// Prevent Backspace and Delete keys from functioning in the entry field
if (ev.code === "Backspace" || ev.code === "Delete") {
ev.preventDefault();
}
};
onChange = (ev) => {
this.setState({ value: ev.target.value });
};
@ -64,6 +71,7 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
className="mx_DialPadContextMenu_dialled"
value={this.state.value}
autoFocus={true}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
/>
</div>

View file

@ -86,14 +86,18 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
<span className={classNames("mx_IconizedContextMenu_icon", {
mx_IconizedContextMenu_checked: active,
mx_IconizedContextMenu_unchecked: !active,
})} />
</MenuItemCheckbox>;
};
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, children, ...props }) => {
return <MenuItem {...props} label={label}>
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ children }
</MenuItem>;
};

View file

@ -0,0 +1,216 @@
/*
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, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import {
IProps as IContextMenuProps,
} from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { _t } from "../../../languageHandler";
import {
leaveSpace,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite,
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomViewStore from "../../../stores/RoomViewStore";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { BetaPill } from "../beta/BetaCard";
interface IProps extends IContextMenuProps {
space: Room;
}
const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
let inviteOption;
if (space.getJoinRule() === "public" || space.canInvite(userId)) {
const onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showSpaceInvite(space);
onFinished();
};
inviteOption = (
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite people")}
onClick={onInviteClick}
/>
);
}
let settingsOption;
let leaveSection;
if (shouldShowSpaceSettings(space)) {
const onSettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showSpaceSettings(space);
onFinished();
};
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("Settings")}
onClick={onSettingsClick}
/>
);
} else {
const onLeaveClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
leaveSpace(space);
onFinished();
};
leaveSection = <IconizedContextMenuOptionList red first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconLeave"
label={_t("Leave space")}
onClick={onLeaveClick}
/>
</IconizedContextMenuOptionList>;
}
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let newRoomSection;
if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
const onNewRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCreateNewRoom(space);
onFinished();
};
const onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(space);
onFinished();
};
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCreateNewSubspace(space);
onFinished();
};
newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Create new room")}
onClick={onNewRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={onAddExistingRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>;
}
const onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (!RoomViewStore.getRoomId()) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
}, true);
}
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList,
refireParams: { space: space },
});
onFinished();
};
const onExploreRoomsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
});
onFinished();
};
return <IconizedContextMenu
{...props}
onFinished={onFinished}
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ space.name }
</div>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
onClick={onMembersClick}
/>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={onExploreRoomsClick}
/>
</IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection }
</IconizedContextMenu>;
};
export default SpaceContextMenu;

View file

@ -0,0 +1,67 @@
/*
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 { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog";
interface IProps {
space: Room;
onCreateSubspaceClick(): void;
onFinished(added?: boolean): void;
}
const AddExistingSubspaceDialog: React.FC<IProps> = ({ space, onCreateSubspaceClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
return <BaseDialog
title={(
<SubspaceSelector
title={_t("Add existing space")}
space={space}
value={selectedSpace}
onChange={setSelectedSpace}
/>
)}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace"
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={space.client}>
<AddExistingToSpace
space={space}
onFinished={onFinished}
footerPrompt={<>
<div>{ _t("Want to add a new space instead?") }</div>
<AccessibleButton onClick={onCreateSubspaceClick} kind="link">
{ _t("Create a new space") }
</AccessibleButton>
</>}
filterPlaceholder={_t("Search for spaces")}
spacesRenderer={defaultSpacesRenderer}
/>
</MatrixClientContext.Provider>
</BaseDialog>;
};
export default AddExistingSubspaceDialog;

View file

@ -18,9 +18,9 @@ import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { sleep } from "matrix-js-sdk/src/utils";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
@ -35,19 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps {
interface IProps {
space: Room;
onCreateRoomClick(space: Room): void;
onCreateRoomClick(): void;
onAddSubspaceClick(): void;
onFinished(added?: boolean): void;
}
const Entry = ({ room, checked, onChange }) => {
export const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry">
{ room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
@ -65,14 +66,36 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps {
space: Room;
footerPrompt?: ReactNode;
filterPlaceholder: string;
emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
roomsRenderer?(
rooms: Room[],
selectedToAdd: Set<Room>,
onChange: undefined | ((checked: boolean, room: Room) => void),
truncateAt: number,
overflowTile: (overflowCount: number, totalCount: number) => JSX.Element,
): ReactNode;
spacesRenderer?(
spaces: Room[],
selectedToAdd: Set<Room>,
onChange?: (checked: boolean, room: Room) => void,
): ReactNode;
dmsRenderer?(
dms: Room[],
selectedToAdd: Set<Room>,
onChange?: (checked: boolean, room: Room) => void,
): ReactNode;
}
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space,
footerPrompt,
emptySelectionButton,
filterPlaceholder,
roomsRenderer,
dmsRenderer,
spacesRenderer,
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
@ -196,7 +219,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</>;
}
const onChange = !busy && !error ? (checked, room) => {
const onChange = !busy && !error ? (checked: boolean, room: Room) => {
if (checked) {
selectedToAdd.add(room);
} else {
@ -206,7 +229,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
} : null;
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount, totalCount) {
function overflowTile(overflowCount: number, totalCount: number): JSX.Element {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile
@ -222,73 +245,36 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
);
}
let noResults = true;
if ((roomsRenderer && rooms.length > 0) ||
(dmsRenderer && dms.length > 0) ||
(!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone
) {
noResults = false;
}
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Filter your rooms and spaces")}
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
<TruncatedList
truncateAt={truncateAt}
createOverflowElement={overflowTile}
getChildren={(start, end) => rooms.slice(start, end).map(room =>
<Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div>
{ rooms.length > 0 && roomsRenderer ? (
roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
{ spaces.length > 0 && spacesRenderer ? (
spacesRenderer(spaces, selectedToAdd, onChange)
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
{ dms.length > 0 && dmsRenderer ? (
dmsRenderer(dms, selectedToAdd, onChange)
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ noResults ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
@ -299,50 +285,126 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
rooms, selectedToAdd, onChange, truncateAt, overflowTile,
) => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
<TruncatedList
truncateAt={truncateAt}
createOverflowElement={overflowTile}
getChildren={(start, end) => rooms.slice(start, end).map(room =>
<Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked: boolean) => {
onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div>
);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section">
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
);
spaceOptionSection = (
export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked: boolean) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
);
interface ISubspaceSelectorProps {
title: string;
space: Room;
value: Room;
onChange(space: Room): void;
}
export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => {
const options = useMemo(() => {
return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => {
return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId);
})];
}, [space]);
let body;
if (options.length > 1) {
body = (
<Dropdown
id="mx_SpaceSelectDropdown"
className="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
onChange(options.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
value={value.roomId}
label={_t("Space selection")}
>
{ options }
{ options.map((space) => {
const classes = classNames({
mx_SubspaceSelector_dropdownOptionActive: space === value,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}) }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
body = (
<div className="mx_SubspaceSelector_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>
);
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
return <div className="mx_SubspaceSelector">
<RoomAvatar room={value} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
<h1>{ title }</h1>
{ body }
</div>
</React.Fragment>;
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
return <BaseDialog
title={title}
title={(
<SubspaceSelector
title={_t("Add existing rooms")}
space={space}
value={selectedSpace}
onChange={setSelectedSpace}
/>
)}
className="mx_AddExistingToSpaceDialog"
contentId="mx_AddExistingToSpace"
onFinished={onFinished}
@ -354,14 +416,35 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick,
onFinished={onFinished}
footerPrompt={<>
<div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(space)} kind="link">
<AccessibleButton
kind="link"
onClick={() => {
onCreateRoomClick();
onFinished();
}}
>
{ _t("Create a new room") }
</AccessibleButton>
</>}
filterPlaceholder={_t("Search for rooms")}
roomsRenderer={defaultRoomsRenderer}
spacesRenderer={() => (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Spaces") }</h3>
<AccessibleButton
kind="link"
onClick={() => {
onAddSubspaceClick();
onFinished();
}}
>
{ _t("Adding spaces has moved.") }
</AccessibleButton>
</div>
)}
dmsRenderer={defaultDmsRenderer}
/>
</MatrixClientContext.Provider>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};

View file

@ -14,22 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from "react";
import React from "react";
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas
interface IProps extends IDialogProps {
featureId: string;
@ -38,77 +34,28 @@ interface IProps extends IDialogProps {
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
o[k] = SettingsStore.getValue(k);
return o;
}, {});
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
title: _t("Beta feedback"),
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_BetaFeedbackDialog"
hasCancelButton={true}
return <GenericFeatureFeedbackDialog
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
description={<React.Fragment>
<div className="mx_BetaFeedbackDialog_subheading">
{ _t(info.feedbackSubheading) }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
<AccessibleButton
kind="link"
onClick={() => {
onFinished(false);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
}}
>
{ _t("To leave the beta, visit your settings.") }
</AccessibleButton>
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
subheading={_t(info.feedbackSubheading)}
onFinished={onFinished}
rageshakeLabel={info.feedbackLabel}
rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => {
return SettingsStore.getValue(k);
}))}
>
<AccessibleButton
kind="link"
onClick={() => {
onFinished(false);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
}}
>
{ _t("To leave the beta, visit your settings.") }
</AccessibleButton>
</GenericFeatureFeedbackDialog>;
};
export default BetaFeedbackDialog;

View file

@ -32,8 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import Dropdown from "../elements/Dropdown";
import SpaceStore from "../../../stores/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps {
defaultPublic?: boolean;
@ -250,7 +250,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Public) {
} else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) {
publicPrivateLabel = <p>
{ _t(
"Anyone will be able to find and join this room, not just members of <SpaceName/>.", {}, {
@ -260,6 +260,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Public) {
publicPrivateLabel = <p>
{ _t("Anyone will be able to find and join this room.") }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Invite) {
publicPrivateLabel = <p>
{ _t(
@ -316,21 +322,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
}
const options = [
<div key={JoinRule.Invite} className="mx_CreateRoomDialog_dropdown_invite">
{ _t("Private room (invite only)") }
</div>,
<div key={JoinRule.Public} className="mx_CreateRoomDialog_dropdown_public">
{ _t("Public room") }
</div>,
];
if (this.supportsRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_CreateRoomDialog_dropdown_restricted">
{ _t("Visible to space members") }
</div>);
}
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
@ -350,16 +341,14 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
className="mx_CreateRoomDialog_topic"
/>
<Dropdown
id="mx_CreateRoomDialog_typeDropdown"
className="mx_CreateRoomDialog_typeDropdown"
onOptionChange={this.onJoinRuleChange}
menuWidth={448}
value={this.state.joinRule}
<JoinRuleDropdown
label={_t("Room visibility")}
>
{ options }
</Dropdown>
labelInvite={_t("Private room (invite only)")}
labelPublic={_t("Public room")}
labelRestricted={this.supportsRestricted ? _t("Visible to space members") : undefined}
value={this.state.joinRule}
onChange={this.onJoinRuleChange}
/>
{ publicPrivateLabel }
{ e2eeSection }

View file

@ -0,0 +1,210 @@
/*
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, { useRef, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
import { RoomType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { BetaPill } from "../beta/BetaCard";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import SpaceStore from "../../../stores/SpaceStore";
import { SpaceCreateForm } from "../spaces/SpaceCreateMenu";
import createRoom from "../../../createRoom";
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
interface IProps {
space: Room;
onAddExistingSpaceClick(): void;
onFinished(added?: boolean): void;
}
const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick, onFinished }) => {
const [parentSpace, setParentSpace] = useState(space);
const [busy, setBusy] = useState<boolean>(false);
const [name, setName] = useState("");
const spaceNameField = useRef<Field>();
const [alias, setAlias] = useState("");
const spaceAliasField = useRef<RoomAliasField>();
const [avatar, setAvatar] = useState<File>(null);
const [topic, setTopic] = useState<string>("");
const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
const spaceJoinRule = space.getJoinRule();
let defaultJoinRule = JoinRule.Invite;
if (spaceJoinRule === JoinRule.Public) {
defaultJoinRule = JoinRule.Public;
} else if (supportsRestricted) {
defaultJoinRule = JoinRule.Restricted;
}
const [joinRule, setJoinRule] = useState<JoinRule>(defaultJoinRule);
const onCreateSubspaceClick = async (e) => {
e.preventDefault();
if (busy) return;
setBusy(true);
// require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true });
setBusy(false);
return;
}
// validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
setBusy(false);
return;
}
try {
await createRoom({
createOpts: {
preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
...joinRule === JoinRule.Public ? { invite: 0 } : {},
},
room_alias_name: joinRule === JoinRule.Public && alias
? alias.substr(1, alias.indexOf(":") - 1)
: undefined,
topic,
},
avatar,
roomType: RoomType.Space,
parentSpace,
spinner: false,
encryption: false,
andView: true,
inlineErrors: true,
});
onFinished(true);
} catch (e) {
console.error(e);
}
};
let joinRuleMicrocopy: JSX.Element;
if (joinRule === JoinRule.Restricted) {
joinRuleMicrocopy = <p>
{ _t(
"Anyone in <SpaceName/> will be able to find and join.", {}, {
SpaceName: () => <b>{ parentSpace.name }</b>,
},
) }
</p>;
} else if (joinRule === JoinRule.Public) {
joinRuleMicrocopy = <p>
{ _t(
"Anyone will be able to find and join this space, not just members of <SpaceName/>.", {}, {
SpaceName: () => <b>{ parentSpace.name }</b>,
},
) }
</p>;
} else if (joinRule === JoinRule.Invite) {
joinRuleMicrocopy = <p>
{ _t("Only people invited will be able to find and join this space.") }
</p>;
}
return <BaseDialog
title={(
<SubspaceSelector
title={_t("Create a space")}
space={space}
value={parentSpace}
onChange={setParentSpace}
/>
)}
className="mx_CreateSubspaceDialog"
contentId="mx_CreateSubspaceDialog"
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={space.client}>
<div className="mx_CreateSubspaceDialog_content">
<div className="mx_CreateSubspaceDialog_betaNotice">
<BetaPill />
{ _t("Add a space to a space you manage.") }
</div>
<SpaceCreateForm
busy={busy}
onSubmit={onCreateSubspaceClick}
setAvatar={setAvatar}
name={name}
setName={setName}
nameFieldRef={spaceNameField}
topic={topic}
setTopic={setTopic}
alias={alias}
setAlias={setAlias}
showAliasField={joinRule === JoinRule.Public}
aliasFieldRef={spaceAliasField}
>
<JoinRuleDropdown
label={_t("Space visibility")}
labelInvite={_t("Private space (invite only)")}
labelPublic={_t("Public space")}
labelRestricted={supportsRestricted ? _t("Visible to space members") : undefined}
width={478}
value={joinRule}
onChange={setJoinRule}
/>
{ joinRuleMicrocopy }
</SpaceCreateForm>
</div>
<div className="mx_CreateSubspaceDialog_footer">
<div className="mx_CreateSubspaceDialog_footer_prompt">
<div>{ _t("Want to add an existing space instead?") }</div>
<AccessibleButton
kind="link"
onClick={() => {
onAddExistingSpaceClick();
onFinished();
}}
>
{ _t("Add existing space") }
</AccessibleButton>
</div>
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished(false)}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSubspaceClick}>
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div>
</MatrixClientContext.Provider>
</BaseDialog>;
};
export default CreateSubspaceDialog;

View file

@ -490,9 +490,9 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
forceStateEvent={true}
onBack={this.onBack}
inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
}}
/>;
}

View file

@ -0,0 +1,101 @@
/*
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 QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
interface IProps extends IDialogProps {
title: string;
subheading: string;
rageshakeLabel: string;
rageshakeData?: Record<string, string>;
}
const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
title,
subheading,
children,
rageshakeLabel,
rageshakeData = {},
onFinished,
}) => {
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData);
onFinished(true);
Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, {
title,
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_GenericFeatureFeedbackDialog"
hasCancelButton={true}
title={title}
description={<React.Fragment>
<div className="mx_GenericFeatureFeedbackDialog_subheading">
{ subheading }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
{ children }
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onChange={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
};
export default GenericFeatureFeedbackDialog;

View file

@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default function KeySignatureUploadFailedDialog({
failures,
source,
continuation,
onFinished,
failures,
source,
continuation,
onFinished,
}) {
const RETRIES = 2;
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');

View file

@ -0,0 +1,197 @@
/*
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, { useEffect, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "./AddExistingToSpaceDialog";
import SearchBox from "../../structures/SearchBox";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import StyledRadioGroup from "../elements/StyledRadioGroup";
enum RoomsToLeave {
All = "All",
Specific = "Specific",
None = "None",
}
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const filteredRooms = useMemo(() => {
if (!lcQuery) {
return rooms;
}
const matcher = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
});
return matcher.match(lcQuery);
}, [rooms, lcQuery]);
return <div className="mx_LeaveSpaceDialog_section">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
{ filteredRooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
onChange(checked, room);
}}
/>;
}) }
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
const [state, setState] = useState<string>(RoomsToLeave.All);
useEffect(() => {
if (state === RoomsToLeave.All) {
setRoomsToLeave(spaceChildren);
} else {
setRoomsToLeave([]);
}
}, [setRoomsToLeave, state, spaceChildren]);
return <div className="mx_LeaveSpaceDialog_section">
<StyledRadioGroup
name="roomsToLeave"
value={state}
onChange={setState}
definitions={[
{
value: RoomsToLeave.All,
label: _t("Leave all rooms and spaces"),
}, {
value: RoomsToLeave.None,
label: _t("Don't leave any"),
}, {
value: RoomsToLeave.Specific,
label: _t("Leave specific rooms and spaces"),
},
]}
/>
{ state === RoomsToLeave.Specific && (
<SpaceChildPicker
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
rooms={spaceChildren}
selected={selected}
onChange={(selected: boolean, room: Room) => {
if (selected) {
setRoomsToLeave([room, ...roomsToLeave]);
} else {
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
}
}}
/>
) }
</div>;
};
interface IProps {
space: Room;
onFinished(leave: boolean, rooms?: Room[]): void;
}
const isOnlyAdmin = (room: Room): boolean => {
return !room.getJoinedMembers().some(member => {
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
});
};
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
let rejoinWarning;
if (space.getJoinRule() !== JoinRule.Public) {
rejoinWarning = _t("You won't be able to rejoin unless you are re-invited.");
}
let onlyAdminWarning;
if (isOnlyAdmin(space)) {
onlyAdminWarning = _t("You're the only admin of this space. " +
"Leaving it will mean no one has control over it.");
} else {
const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length;
if (numChildrenOnlyAdminIn > 0) {
onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " +
"Leaving them will leave them without any admins.");
}
}
return <BaseDialog
title={_t("Leave %(spaceName)s", { spaceName: space.name })}
className="mx_LeaveSpaceDialog"
contentId="mx_LeaveSpaceDialog"
onFinished={() => onFinished(false)}
fixedWidth={false}
>
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
<p>
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
spaceName: () => <b>{ space.name }</b>,
}) }
&nbsp;
{ rejoinWarning }
</p>
{ spaceChildren.length > 0 && <LeaveRoomsPicker
space={space}
spaceChildren={spaceChildren}
roomsToLeave={roomsToLeave}
setRoomsToLeave={setRoomsToLeave}
/> }
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
{ onlyAdminWarning }
</div> }
</div>
<DialogButtons
primaryButton={_t("Leave space")}
onPrimaryButtonClick={() => onFinished(true, roomsToLeave)}
hasCancel={true}
onCancel={onFinished}
/>
</BaseDialog>;
};
export default LeaveSpaceDialog;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
shouldLoadBackupStatus: boolean;
loading: boolean;
backupInfo: IKeyBackupInfo;
error?: string;
}
@replaceableComponent("views.dialogs.LogoutDialog")
export default class LogoutDialog extends React.Component {
defaultProps = {
export default class LogoutDialog extends React.Component<IProps, IState> {
static defaultProps = {
onFinished: function() {},
};
constructor() {
super();
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
this._onFinished = this._onFinished.bind(this);
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
constructor(props) {
super(props);
const cli = MatrixClientPeg.get();
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
};
if (shouldLoadBackupStatus) {
this._loadBackupStatus();
this.loadBackupStatus();
}
}
async _loadBackupStatus() {
private async loadBackupStatus() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
}
}
_onSettingsLinkClick() {
private onSettingsLinkClick = (): void => {
// close dialog
this.props.onFinished();
}
this.props.onFinished(true);
};
_onExportE2eKeysClicked() {
private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
}
};
_onFinished(confirmed) {
private onFinished = (confirmed: boolean): void => {
if (confirmed) {
dis.dispatch({ action: 'logout' });
}
// close dialog
this.props.onFinished();
}
this.props.onFinished(confirmed);
};
_onSetRecoveryMethodClick() {
private onSetRecoveryMethodClick = (): void => {
if (this.state.backupInfo) {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component {
}
// close dialog
this.props.onFinished();
}
this.props.onFinished(true);
};
_onLogoutConfirm() {
private onLogoutConfirm = (): void => {
dis.dispatch({ action: 'logout' });
// close dialog
this.props.onFinished();
}
this.props.onFinished(true);
};
render() {
if (this.state.shouldLoadBackupStatus) {
@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
</div>
<DialogButtons primaryButton={setupButtonCaption}
hasCancel={false}
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this._onLogoutConfirm}>
<button onClick={this.onLogoutConfirm}>
{ _t("I don't want my encrypted messages") }
</button>
</DialogButtons>
<details>
<summary>{ _t("Advanced") }</summary>
<p><button onClick={this._onExportE2eKeysClicked}>
<p><button onClick={this.onExportE2eKeysClicked}>
{ _t("Manually export keys") }
</button></p>
</details>
@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component {
title={_t("You'll lose access to your encrypted messages")}
contentId='mx_Dialog_content'
hasCancel={true}
onFinished={this._onFinished}
onFinished={this.onFinished}
>
{ dialogContent }
</BaseDialog>);
@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
"Are you sure you want to sign out?",
)}
button={_t("Sign out")}
onFinished={this._onFinished}
onFinished={this.onFinished}
/>);
}
}

View file

@ -17,9 +17,12 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import BaseDialog from "..//dialogs/BaseDialog";
import DialogButtons from "./DialogButtons";
import classNames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
export interface DesktopCapturerSource {
id: string;
@ -28,62 +31,70 @@ export interface DesktopCapturerSource {
}
export enum Tabs {
Screens = "screens",
Windows = "windows",
Screens = "screen",
Windows = "window",
}
export interface DesktopCapturerSourceIProps {
export interface ExistingSourceIProps {
source: DesktopCapturerSource;
onSelect(source: DesktopCapturerSource): void;
selected: boolean;
}
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
constructor(props) {
export class ExistingSource extends React.Component<ExistingSourceIProps> {
constructor(props: ExistingSourceIProps) {
super(props);
}
onClick = (ev) => {
private onClick = (): void => {
this.props.onSelect(this.props.source);
};
render() {
const thumbnailClasses = classNames({
mx_desktopCapturerSourcePicker_source_thumbnail: true,
mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected,
});
return (
<AccessibleButton
className="mx_desktopCapturerSourcePicker_stream_button"
className="mx_desktopCapturerSourcePicker_source"
title={this.props.source.name}
onClick={this.onClick}
>
<img
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
className={thumbnailClasses}
src={this.props.source.thumbnailURL}
/>
<span className="mx_desktopCapturerSourcePicker_stream_name">{ this.props.source.name }</span>
<span className="mx_desktopCapturerSourcePicker_source_name">{ this.props.source.name }</span>
</AccessibleButton>
);
}
}
export interface DesktopCapturerSourcePickerIState {
export interface PickerIState {
selectedTab: Tabs;
sources: Array<DesktopCapturerSource>;
selectedSource: DesktopCapturerSource | null;
}
export interface DesktopCapturerSourcePickerIProps {
export interface PickerIProps {
onFinished(source: DesktopCapturerSource): void;
}
@replaceableComponent("views.elements.DesktopCapturerSourcePicker")
export default class DesktopCapturerSourcePicker extends React.Component<
DesktopCapturerSourcePickerIProps,
DesktopCapturerSourcePickerIState
> {
interval;
PickerIProps,
PickerIState
> {
interval: number;
constructor(props) {
constructor(props: PickerIProps) {
super(props);
this.state = {
selectedTab: Tabs.Screens,
sources: [],
selectedSource: null,
};
}
@ -107,69 +118,61 @@ export default class DesktopCapturerSourcePicker extends React.Component<
clearInterval(this.interval);
}
onSelect = (source) => {
this.props.onFinished(source);
private onSelect = (source: DesktopCapturerSource): void => {
this.setState({ selectedSource: source });
};
onScreensClick = (ev) => {
this.setState({ selectedTab: Tabs.Screens });
private onShare = (): void => {
this.props.onFinished(this.state.selectedSource);
};
onWindowsClick = (ev) => {
this.setState({ selectedTab: Tabs.Windows });
private onTabChange = (): void => {
this.setState({ selectedSource: null });
};
onCloseClick = (ev) => {
private onCloseClick = (): void => {
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} />;
});
}
private getTab(type: "screen" | "window", label: string): Tab {
const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => {
return (
<ExistingSource
selected={this.state.selectedSource?.id === source.id}
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 new Tab(type, label, null, (
<div className="mx_desktopCapturerSourcePicker_tab">
{ sources }
</div>
));
}
render() {
const tabs = [
this.getTab("screen", _t("Share entire screen")),
this.getTab("window", _t("Application window")),
];
return (
<BaseDialog
className="mx_desktopCapturerSourcePicker"
onFinished={this.onCloseClick}
title={_t("Share your screen")}
title={_t("Share content")}
>
<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>
<TabbedView tabs={tabs} tabLocation={TabLocation.TOP} onChange={this.onTabChange} />
<DialogButtons
primaryButton={_t("Share")}
hasCancel={true}
onCancel={this.onCloseClick}
onPrimaryButtonClick={this.onShare}
primaryDisabled={!this.state.selectedSource}
/>
</BaseDialog>
);
}

View file

@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
import { Layout } from '../../../settings/Layout';
interface IProps {
// An array of member events to summarise
@ -38,6 +39,8 @@ interface IProps {
children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
// The layout currently used
layout?: Layout;
}
const EventListSummary: React.FC<IProps> = ({
@ -48,6 +51,7 @@ const EventListSummary: React.FC<IProps> = ({
startExpanded,
summaryMembers = [],
summaryText,
layout,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@ -63,7 +67,7 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}>
{ children }
</li>
);
@ -92,7 +96,7 @@ const EventListSummary: React.FC<IProps> = ({
}
return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""} data-layout={layout}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
@ -103,6 +107,7 @@ const EventListSummary: React.FC<IProps> = ({
EventListSummary.defaultProps = {
startExpanded: false,
layout: Layout.Group,
};
export default EventListSummary;

View file

@ -0,0 +1,68 @@
/*
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 { JoinRule } from 'matrix-js-sdk/src/@types/partials';
import Dropdown from "./Dropdown";
interface IProps {
value: JoinRule;
label: string;
width?: number;
labelInvite: string;
labelPublic: string;
labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
onChange(value: JoinRule): void;
}
const JoinRuleDropdown = ({
label,
labelInvite,
labelPublic,
labelRestricted,
value,
width = 448,
onChange,
}: IProps) => {
const options = [
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
{ labelInvite }
</div>,
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
{ labelPublic }
</div>,
];
if (labelRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{ labelRestricted }
</div>);
}
return <Dropdown
id="mx_JoinRuleDropdown"
className="mx_JoinRuleDropdown"
onOptionChange={onChange}
menuWidth={width}
value={value}
label={label}
>
{ options }
</Dropdown>;
};
export default JoinRuleDropdown;

View file

@ -25,12 +25,15 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Layout } from '../../../settings/Layout';
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
// The currently selected layout
layout: Layout;
}
interface IUserEvents {
@ -67,6 +70,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
layout: Layout.Group,
};
shouldComponentUpdate(nextProps) {
@ -453,6 +457,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
}
}

View file

@ -192,7 +192,8 @@ class Pill extends React.Component {
});
}
onUserPillClicked = () => {
onUserPillClicked = (e) => {
e.preventDefault();
dis.dispatch({
action: Action.ViewUser,
member: this.state.member,

View file

@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
interface IProps {
mxEvent: MatrixEvent;
@ -69,6 +70,18 @@ export default class CallEvent extends React.Component<IProps, IState> {
this.setState({ callState: newState });
};
private renderCallBackButton(text: string): JSX.Element {
return (
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { text } </span>
</AccessibleButton>
);
}
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({
@ -103,8 +116,18 @@ export default class CallEvent extends React.Component<IProps, IState> {
}
if (state === CallState.Ended) {
const hangupReason = this.props.callEventGrouper.hangupReason;
const gotRejected = this.props.callEventGrouper.gotRejected;
const rejectParty = this.props.callEventGrouper.rejectParty;
if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
if (gotRejected) {
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
return (
<div className="mx_CallEvent_content">
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
</div>
);
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
@ -116,6 +139,13 @@ export default class CallEvent extends React.Component<IProps, IState> {
{ _t("This call has ended") }
</div>
);
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_CallEvent_content">
{ _t("They didn't pick up") }
{ this.renderCallBackButton(_t("Call again")) }
</div>
);
}
let reason;
@ -133,8 +163,6 @@ export default class CallEvent extends React.Component<IProps, IState> {
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
reason = _t("An unknown error occurred");
} else if (hangupReason === CallErrorCode.InviteTimeout) {
reason = _t("No answer");
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("The user you called is busy.");
} else {
@ -163,13 +191,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
return (
<div className="mx_CallEvent_content">
{ _t("You missed this call") }
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { _t("Call back") } </span>
</AccessibleButton>
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
}
@ -186,11 +208,17 @@ export default class CallEvent extends React.Component<IProps, IState> {
const sender = event.sender ? event.sender.name : event.getSender();
const isVoice = this.props.callEventGrouper.isVoice;
const callType = isVoice ? _t("Voice call") : _t("Video call");
const content = this.renderContent(this.state.callState);
const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent(callState);
const className = classNames({
mx_CallEvent: true,
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
mx_CallEvent_missed: (
callState === CustomCallState.Missed ||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
),
});
return (

View file

@ -16,12 +16,13 @@ limitations under the License.
import { MatrixEvent } from "matrix-js-sdk/src";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import React, { createRef } from "react";
import React from "react";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Spinner from "../elements/Spinner";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { FileDownloader } from "../../../utils/FileDownloader";
interface IProps {
mxEvent: MatrixEvent;
@ -39,7 +40,7 @@ interface IState {
@replaceableComponent("views.messages.DownloadActionButton")
export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
private downloader = new FileDownloader();
public constructor(props: IProps) {
super(props);
@ -56,27 +57,21 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
if (this.state.blob) {
// Cheat and trigger a download, again.
return this.onFrameLoad();
return this.doDownload();
}
const blob = await this.props.mediaEventHelperGet().sourceBlob.value;
this.setState({ blob });
await this.doDownload();
};
private onFrameLoad = () => {
this.setState({ loading: false });
// we aren't showing the iframe, so we can send over the bare minimum styles and such.
this.iframe.current.contentWindow.postMessage({
imgSrc: "", // no image
imgStyle: null,
style: "",
private async doDownload() {
await this.downloader.download({
blob: this.state.blob,
download: this.props.mediaEventHelperGet().fileName,
textContent: "",
auto: true, // autodownload
}, '*');
};
name: this.props.mediaEventHelperGet().fileName,
});
this.setState({ loading: false });
}
public render() {
let spinner: JSX.Element;
@ -92,18 +87,11 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
return <RovingAccessibleTooltipButton
className={classes}
title={spinner ? _t("Downloading") : _t("Download")}
title={spinner ? _t("Decrypting") : _t("Download")}
onClick={this.onDownloadClick}
disabled={!!spinner}
>
{ spinner }
{ this.state.blob && <iframe
src="usercontent/" // XXX: Like MFileBody, this should come from the skin
ref={this.iframe}
onLoad={this.onFrameLoad}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
style={{ display: "none" }}
/> }
</RovingAccessibleTooltipButton>;
}
}

View file

@ -26,6 +26,8 @@ import { TileShape } from "../rooms/EventTile";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
@ -111,6 +113,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
private userDidClick = false;
private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);
public constructor(props: IProps) {
super(props);
@ -118,6 +121,32 @@ export default class MFileBody extends React.Component<IProps, IState> {
this.state = {};
}
private get content(): IMediaEventContent {
return this.props.mxEvent.getContent<IMediaEventContent>();
}
private get fileName(): string {
return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment");
}
private get linkText(): string {
return presentableTextForFile(this.content);
}
private downloadFile(fileName: string, text: string) {
this.fileDownloader.download({
blob: this.state.decryptedBlob,
name: fileName,
autoDownload: this.userDidClick,
opts: {
imgSrc: DOWNLOAD_ICON_URL,
imgStyle: null,
style: computedStyle(this.dummyLink.current),
textContent: _t("Download %(text)s", { text }),
},
});
}
private getContentUrl(): string {
const media = mediaFromContent(this.props.mxEvent.getContent());
return media.srcHttp;
@ -129,24 +158,56 @@ export default class MFileBody extends React.Component<IProps, IState> {
}
}
public render() {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const text = presentableTextForFile(content);
const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this.getContentUrl();
const fileSize = content.info ? content.info.size : null;
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
private decryptFile = async (): Promise<void> => {
if (this.state.decryptedBlob) {
return;
}
try {
this.userDidClick = true;
this.setState({
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
});
} catch (err) {
console.warn("Unable to decrypt attachment: ", err);
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
title: _t("Error"),
description: _t("Error decrypting attachment"),
});
}
};
let placeholder = null;
private onPlaceholderClick = async () => {
const mediaHelper = this.props.mediaEventHelper;
if (mediaHelper.media.isEncrypted) {
await this.decryptFile();
this.downloadFile(this.fileName, this.linkText);
} else {
// As a button we're missing the `download` attribute for styling reasons, so
// download with the file downloader.
this.fileDownloader.download({
blob: await mediaHelper.sourceBlob.value,
name: this.fileName,
});
}
};
public render() {
const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
const contentUrl = this.getContentUrl();
const fileSize = this.content.info ? this.content.info.size : null;
const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
let placeholder: React.ReactNode = null;
if (this.props.showGenericPlaceholder) {
placeholder = (
<div className="mx_MediaBody mx_MFileBody_info">
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
<span className="mx_MFileBody_info_icon" />
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(content, _t("Attachment"), false) }
</span>
</div>
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }
</span>
</TextWithTooltip>
</AccessibleButton>
);
}
@ -157,20 +218,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
// Need to decrypt the attachment
// Wait for the user to click on the link before downloading
// and decrypting the attachment.
const decrypt = async () => {
try {
this.userDidClick = true;
this.setState({
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
});
} catch (err) {
console.warn("Unable to decrypt attachment: ", err);
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
title: _t("Error"),
description: _t("Error decrypting attachment"),
});
}
};
// This button should actually Download because usercontent/ will try to click itself
// but it is not guaranteed between various browsers' settings.
@ -178,31 +225,14 @@ export default class MFileBody extends React.Component<IProps, IState> {
<span className="mx_MFileBody">
{ placeholder }
{ showDownloadLink && <div className="mx_MFileBody_download">
<AccessibleButton onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
<AccessibleButton onClick={this.decryptFile}>
{ _t("Decrypt %(text)s", { text: this.linkText }) }
</AccessibleButton>
</div> }
</span>
);
}
// When the iframe loads we tell it to render a download link
const onIframeLoad = (ev) => {
ev.target.contentWindow.postMessage({
imgSrc: DOWNLOAD_ICON_URL,
imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
style: computedStyle(this.dummyLink.current),
blob: this.state.decryptedBlob,
// Set a download attribute for encrypted files so that the file
// will have the correct name when the user tries to download it.
// We can't provide a Content-Disposition header like we would for HTTP.
download: fileName,
textContent: _t("Download %(text)s", { text: text }),
// only auto-download if a user triggered this iframe explicitly
auto: this.userDidClick,
}, "*");
};
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
// If the attachment is encrypted then put the link inside an iframe.
@ -218,9 +248,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
*/ }
<a ref={this.dummyLink} />
</div>
{ /*
TODO: Move iframe (and dummy link) into FileDownloader.
We currently have it set up this way because of styles applied to the iframe
itself which cannot be easily handled/overridden by the FileDownloader. In
future, the download link may disappear entirely at which point it could also
be suitable to just remove this bit of code.
*/ }
<iframe
src={url}
onLoad={onIframeLoad}
onLoad={() => this.downloadFile(this.fileName, this.linkText)}
ref={this.iframe}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
</div> }
@ -259,7 +296,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
// We have to create an anchor to download the file
const tempAnchor = document.createElement('a');
tempAnchor.download = fileName;
tempAnchor.download = this.fileName;
tempAnchor.href = blobUrl;
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
tempAnchor.click();
@ -268,7 +305,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
};
} else {
// Else we are hoping the browser will do the right thing
downloadProps["download"] = fileName;
downloadProps["download"] = this.fileName;
}
return (
@ -277,16 +314,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
{ showDownloadLink && <div className="mx_MFileBody_download">
<a {...downloadProps}>
<span className="mx_MFileBody_download_icon" />
{ _t("Download %(text)s", { text: text }) }
{ _t("Download %(text)s", { text: this.linkText }) }
</a>
{ this.props.tileShape === TileShape.FileGrid && <div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
{ this.content.info && this.content.info.size ? filesize(this.content.info.size) : "" }
</div> }
</div> }
</span>
);
} else {
const extra = text ? (': ' + text) : '';
const extra = this.linkText ? (': ' + this.linkText) : '';
return <span className="mx_MFileBody">
{ placeholder }
{ _t("Invalid file%(extra)s", { extra: extra }) }

View file

@ -366,7 +366,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }}>
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
{ showPlaceholder &&
<div
className="mx_MImageBody_thumbnail"

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import MAudioBody from "./MAudioBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import MVoiceMessageBody from "./MVoiceMessageBody";
import { IBodyProps } from "./IBodyProps";
@ -27,8 +26,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
if (isVoiceMessage && voiceMessagesEnabled) {
if (isVoiceMessage) {
return <MVoiceMessageBody {...this.props} />;
} else {
return <MAudioBody {...this.props} />;

View file

@ -15,18 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from 'matrix-js-sdk/src';
import classNames from 'classnames';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
expanded: boolean;
}
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
@ -35,7 +38,7 @@ export default class ViewSourceEvent extends React.PureComponent {
};
}
componentDidMount() {
public componentDidMount(): void {
const { mxEvent } = this.props;
const client = MatrixClientPeg.get();
@ -46,15 +49,15 @@ export default class ViewSourceEvent extends React.PureComponent {
}
}
onToggle = (ev) => {
private onToggle = (ev: React.MouseEvent) => {
ev.preventDefault();
const { expanded } = this.state;
this.setState({
expanded: !expanded,
});
}
};
render() {
public render(): React.ReactNode {
const { mxEvent } = this.props;
const { expanded } = this.state;

View file

@ -152,7 +152,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
<h2>{ _t("Nothing pinned, yet") }</h2>
{ _t("If you have permissions, open the menu on any message and select " +
"<b>Pin</b> to stick them here.", {}, {
b: sub => <b>{ sub }</b>,
b: sub => <b>{ sub }</b>,
}) }
</div>
</div>;

View file

@ -1381,8 +1381,8 @@ const BasicUserInfo: React.FC<{
className="mx_UserInfo_field"
onClick={() => {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}}
>

View file

@ -896,6 +896,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender,
});
// If the tile is in the Sending state, don't speak the message.
@ -1165,8 +1166,9 @@ export default class EventTile extends React.Component<IProps, IState> {
/>
{ keyRequestInfo }
{ actionBar }
{ this.props.layout === Layout.IRC && (reactionsRow) }
</div>
{ reactionsRow }
{ this.props.layout !== Layout.IRC && (reactionsRow) }
{ msgOption }
</>)
);

View file

@ -342,8 +342,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private onVoiceStoreUpdate = () => {
const recording = VoiceRecordingStore.instance.activeRecording;
this.setState({ haveRecording: !!recording });
if (recording) {
// Delay saying we have a recording until it is started, as we might not yet have A/V permissions
recording.on(RecordingState.Started, () => {
this.setState({ haveRecording: !!VoiceRecordingStore.instance.activeRecording });
});
// We show a little heads up that the recording is about to automatically end soon. The 3s
// display time is completely arbitrary. Note that we don't need to deregister the listener
// because the recording instance will clean that up for us.
@ -351,6 +354,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
this.setState({ recordingTimeLeftSeconds: secondsLeft });
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
});
} else {
this.setState({ haveRecording: false });
}
};
@ -389,12 +394,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (SettingsStore.getValue("feature_voice_messages")) {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
room={this.props.room} />);
}
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
room={this.props.room} />);
if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(

View file

@ -64,9 +64,9 @@ const NewRoomIntro = () => {
height={AVATAR_SIZE}
onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
});
}}
/>

View file

@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px",
left: toPx(oldInfo.left) });
left: toPx(oldInfo.left) });
}
startStyles.push({ top: startTopOffset+'px', left: '0' });
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{ userName: this.props.fallbackUserId,
dateTime: dateString },
dateTime: dateString },
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{ displayName: this.props.member.rawDisplayName,
userName: this.props.fallbackUserId,
dateTime: dateString },
userName: this.props.fallbackUserId,
dateTime: dateString },
);
}
}

View file

@ -67,15 +67,21 @@ export default class ReplyTile extends React.PureComponent<IProps> {
};
private onClick = (e: React.MouseEvent): void => {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
const clickTarget = e.target as HTMLElement;
// Following a link within a reply should not dispatch the `view_room` action
// so that the browser can direct the user to the correct location
// The exception being the link wrapping the reply
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
}
};
render() {

View file

@ -29,6 +29,8 @@ import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Modal from '../../../Modal';
import InfoDialog from "../dialogs/InfoDialog";
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
@ -87,6 +89,14 @@ export default class RoomHeader extends React.Component<IProps> {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
private displayInfoDialogAboutScreensharing() {
Modal.createDialog(InfoDialog, {
title: _t("Screen sharing is here!"),
description: _t("You can now share your screen by pressing the \"screen share\" " +
"button during a call. You can even do this in audio calls if both sides support it!"),
});
}
public render() {
let searchStatus = null;
@ -185,8 +195,8 @@ export default class RoomHeader extends React.Component<IProps> {
videoCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => this.props.onCallPlaced(
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
onClick={(ev) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
title={_t("Video call")} />;
}

View file

@ -26,6 +26,7 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton";
import SpaceStore from "../../../../../stores/SpaceStore";
interface IState {
autoLaunch: boolean;
@ -47,6 +48,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
'breadcrumbs',
];
static SPACES_SETTINGS = [
"Spaces.allRoomsInHome",
];
static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
@ -231,6 +236,11 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div>
{ SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
</div> }
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useContext, useRef, useState } from "react";
import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, useState } from "react";
import classNames from "classnames";
import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event";
import { RoomType } from "matrix-js-sdk/src/@types/event";
import FocusLock from "react-focus-lock";
import { _t } from "../../../languageHandler";
@ -24,18 +24,16 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { SpaceAvatar } from "./SpaceBasicSettings";
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton";
import { BetaPill } from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import RoomAliasField from "../elements/RoomAliasField";
import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
import SettingsStore from "../../../settings/SettingsStore";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@ -66,8 +64,111 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`;
};
const SpaceCreateMenu = ({ onFinished }) => {
// XXX: Temporary for the Spaces release only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a new feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, {
title: _t("Spaces feedback"),
subheading: _t("Thank you for trying Spaces. " +
"Your feedback will help inform the next versions."),
rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([
"feature_spaces.all_rooms",
"feature_spaces.space_member_dms",
"feature_spaces.space_dm_badges",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}
>
{ _t("Give feedback.") }
</AccessibleButton>
</div>;
};
type BProps = Pick<ComponentProps<typeof SpaceBasicSettings>, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
interface ISpaceCreateFormProps extends BProps {
busy: boolean;
alias: string;
nameFieldRef: RefObject<Field>;
aliasFieldRef: RefObject<RoomAliasField>;
showAliasField?: boolean;
onSubmit(e: SyntheticEvent): void;
setAlias(alias: string): void;
}
export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
busy,
onSubmit,
setAvatar,
name,
setName,
nameFieldRef,
alias,
aliasFieldRef,
setAlias,
showAliasField,
topic,
setTopic,
children,
}) => {
const cli = useContext(MatrixClientContext);
const domain = cli.getDomain();
return <form className="mx_SpaceBasicSettings" onSubmit={onSubmit}>
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
<Field
name="spaceName"
label={_t("Name")}
autoFocus={true}
value={name}
onChange={ev => {
const newName = ev.target.value;
if (!alias || alias === nameToAlias(name, domain)) {
setAlias(nameToAlias(newName, domain));
}
setName(newName);
}}
ref={nameFieldRef}
onValidate={spaceNameValidator}
disabled={busy}
/>
{ showAliasField
? <RoomAliasField
ref={aliasFieldRef}
onChange={setAlias}
domain={domain}
value={alias}
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
label={_t("Address")}
disabled={busy}
/>
: null
}
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={busy}
/>
{ children }
</form>;
};
const SpaceCreateMenu = ({ onFinished }) => {
const [visibility, setVisibility] = useState<Visibility>(null);
const [busy, setBusy] = useState<boolean>(false);
@ -98,42 +199,26 @@ const SpaceCreateMenu = ({ onFinished }) => {
return;
}
const initialState: ICreateRoomStateEvent[] = [
{
type: EventType.RoomHistoryVisibility,
content: {
"history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
},
},
];
if (avatar) {
const url = await cli.uploadContent(avatar);
initialState.push({
type: EventType.RoomAvatar,
content: { url },
});
}
try {
await createRoom({
createOpts: {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
creation_content: {
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: initialState,
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
...visibility === Visibility.Public ? { invite: 0 } : {},
},
room_alias_name: visibility === Visibility.Public && alias
? alias.substr(1, alias.indexOf(":") - 1)
: undefined,
topic,
},
avatar,
roomType: RoomType.Space,
historyVisibility: visibility === Visibility.Public
? HistoryVisibility.WorldReadable
: HistoryVisibility.Invited,
spinner: false,
encryption: false,
andView: true,
@ -171,7 +256,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
<SpaceFeedbackPrompt onClick={onFinished} />
</React.Fragment>;
} else {
const domain = cli.getDomain();
body = <React.Fragment>
<AccessibleTooltipButton
className="mx_SpaceCreateMenu_back"
@ -192,49 +276,20 @@ const SpaceCreateMenu = ({ onFinished }) => {
}
</p>
<form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}>
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
<Field
name="spaceName"
label={_t("Name")}
autoFocus={true}
value={name}
onChange={ev => {
const newName = ev.target.value;
if (!alias || alias === nameToAlias(name, domain)) {
setAlias(nameToAlias(newName, domain));
}
setName(newName);
}}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/>
{ visibility === Visibility.Public
? <RoomAliasField
ref={spaceAliasField}
onChange={setAlias}
domain={domain}
value={alias}
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
label={_t("Address")}
disabled={busy}
/>
: null
}
<Field
name="spaceTopic"
element="textarea"
label={_t("Description")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
rows={3}
disabled={busy}
/>
</form>
<SpaceCreateForm
busy={busy}
onSubmit={onSpaceCreateClick}
setAvatar={setAvatar}
name={name}
setName={setName}
nameFieldRef={spaceNameField}
topic={topic}
setTopic={setTopic}
alias={alias}
setAlias={setAlias}
showAliasField={visibility === Visibility.Public}
aliasFieldRef={spaceAliasField}
/>
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
{ busy ? _t("Creating...") : _t("Create") }
@ -252,13 +307,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
managed={false}
>
<FocusLock returnFocus={true}>
<BetaPill onClick={() => {
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
}} />
{ body }
</FocusLock>
</ContextMenu>;

View file

@ -14,115 +14,46 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
import { SpaceItem } from "./SpaceTreeLevel";
import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore, {
HOME_SPACE,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import NotificationBadge from "../rooms/NotificationBadge";
import {
RovingAccessibleButton,
RovingAccessibleTooltipButton,
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
interface IButtonProps {
space?: Room;
className?: string;
selected?: boolean;
tooltip?: string;
notificationState?: NotificationState;
isNarrow?: boolean;
onClick(): void;
}
const SpaceButton: React.FC<IButtonProps> = ({
space,
className,
selected,
onClick,
tooltip,
notificationState,
isNarrow,
children,
}) => {
const classes = classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected,
mx_SpaceButton_narrow: isNarrow,
});
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
if (space) {
avatar = <RoomAvatar width={32} height={32} room={space} />;
}
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space)}
forceCount={false}
notification={notificationState}
/>
</div>;
}
let button;
if (isNarrow) {
button = (
<RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
<div className="mx_SpaceButton_selectionWrapper">
{ avatar }
{ notifBadge }
{ children }
</div>
</RovingAccessibleTooltipButton>
);
} else {
button = (
<RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
<div className="mx_SpaceButton_selectionWrapper">
{ avatar }
<span className="mx_SpaceButton_name">{ tooltip }</span>
{ notifBadge }
{ children }
</div>
</RovingAccessibleButton>
);
}
return <li className={classNames({
"mx_SpaceItem": true,
"collapsed": isNarrow,
})}>
{ button }
</li>;
};
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
const useSpaces = (): [Room[], Room[], Room | null] => {
const [invites, setInvites] = useState<Room[]>(SpaceStore.instance.invitedSpaces);
useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
return SpaceStore.instance.invitedSpaces;
});
const spaces = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => {
return SpaceStore.instance.spacePanelSpaces;
});
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpace;
});
return [invites, spaces, activeSpace];
};
@ -132,23 +63,108 @@ interface IInnerSpacePanelProps {
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
}
const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof SpaceContextMenu>) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
return <IconizedContextMenu
{...props}
onFinished={onFinished}
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ _t("Home") }
</div>
<IconizedContextMenuOptionList first>
<IconizedContextMenuCheckbox
iconClassName="mx_SpacePanel_noIcon"
label={_t("Show all rooms")}
active={allRoomsInHome}
onClick={() => {
SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome);
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
interface IHomeButtonProps {
selected: boolean;
isPanelCollapsed: boolean;
}
const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
return <li className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed,
})}>
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={selected}
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
notificationState={allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(HOME_SPACE)}
isNarrow={isPanelCollapsed}
ContextMenuComponent={HomeButtonContextMenu}
contextMenuTooltip={_t("Options")}
/>
</li>;
};
const CreateSpaceButton = ({
isPanelCollapsed,
setPanelCollapsed,
}: Pick<IInnerSpacePanelProps, "isPanelCollapsed" | "setPanelCollapsed">) => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
useEffect(() => {
if (!isPanelCollapsed && menuDisplayed) {
closeMenu();
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
let contextMenu = null;
if (menuDisplayed) {
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
}
const onNewClick = menuDisplayed ? closeMenu : () => {
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
};
return <li className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed,
})}>
<SpaceButton
className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
})}
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={onNewClick}
isNarrow={isPanelCollapsed}
/>
{ contextMenu }
</li>;
};
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
return <div className="mx_SpaceTreeLevel">
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")}
notificationState={homeNotificationState}
isNarrow={isPanelCollapsed}
/>
<HomeButton selected={!activeSpace} isPanelCollapsed={isPanelCollapsed} />
{ invites.map(s => (
<SpaceItem
key={s.roomId}
@ -178,26 +194,13 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
</Draggable>
)) }
{ children }
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
</div>;
});
const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
useEffect(() => {
if (!isPanelCollapsed && menuDisplayed) {
closeMenu();
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
let contextMenu = null;
if (menuDisplayed) {
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
}
const onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
@ -259,11 +262,6 @@ const SpacePanel = () => {
}
};
const onNewClick = menuDisplayed ? closeMenu : () => {
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
};
return (
<DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list
@ -291,15 +289,6 @@ const SpacePanel = () => {
>
{ provided.placeholder }
</InnerSpacePanel>
<SpaceButton
className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
})}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={onNewClick}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
) }
</Droppable>
@ -308,7 +297,6 @@ const SpacePanel = () => {
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
{ contextMenu }
</ul>
) }
</RovingTabIndexProvider>

View file

@ -21,12 +21,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import SpaceBasicSettings from "./SpaceBasicSettings";
import { avatarUrlForRoom } from "../../../Avatar";
import { IDialogProps } from "../dialogs/IDialogProps";
import { getTopic } from "../elements/RoomTopic";
import { defaultDispatcher } from "../../../dispatcher/dispatcher";
import { leaveSpace } from "../../../utils/space";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -96,8 +95,6 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt />
<div className="mx_SettingsTab_section">
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
@ -128,10 +125,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
<AccessibleButton
kind="danger"
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
leaveSpace(space);
}}
>
{ _t("Leave Space") }

View file

@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
import React, {
createRef,
MouseEvent,
InputHTMLAttributes,
LegacyRef,
ComponentProps,
ComponentType,
} from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
@ -23,31 +30,104 @@ import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf } from "../../structures/ContextMenu";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showSpaceInvite,
showSpaceSettings,
} from "../../../utils/space";
import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { EventType } from "matrix-js-sdk/src/@types/event";
import AccessibleButton from "../elements/AccessibleButton";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
interface IButtonProps extends Omit<ComponentProps<typeof RovingAccessibleTooltipButton>, "title"> {
space?: Room;
className?: string;
selected?: boolean;
label: string;
contextMenuTooltip?: string;
notificationState?: NotificationState;
isNarrow?: boolean;
avatarSize?: number;
ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>;
onClick(ev: MouseEvent): void;
}
export const SpaceButton: React.FC<IButtonProps> = ({
space,
className,
selected,
onClick,
label,
contextMenuTooltip,
notificationState,
avatarSize,
isNarrow,
children,
ContextMenuComponent,
...props
}) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>();
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
if (space) {
avatar = <RoomAvatar width={avatarSize} height={avatarSize} room={space} />;
}
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
forceCount={false}
notification={notificationState}
/>
</div>;
}
let contextMenu: JSX.Element;
if (menuDisplayed && ContextMenuComponent) {
contextMenu = <ContextMenuComponent
{...toRightOf(handle.current?.getBoundingClientRect(), 0)}
space={space}
onFinished={closeMenu}
/>;
}
return (
<RovingAccessibleTooltipButton
{...props}
className={classNames("mx_SpaceButton", className, {
mx_SpaceButton_active: selected,
mx_SpaceButton_hasMenuOpen: menuDisplayed,
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
onClick={onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
role="treeitem"
inputRef={handle}
>
{ children }
<div className="mx_SpaceButton_selectionWrapper">
{ avatar }
{ !isNarrow && <span className="mx_SpaceButton_name">{ label }</span> }
{ notifBadge }
{ ContextMenuComponent && <ContextMenuTooltipButton
className="mx_SpaceButton_menuButton"
onClick={openMenu}
title={contextMenuTooltip}
isExpanded={menuDisplayed}
/> }
{ contextMenu }
</div>
</RovingAccessibleTooltipButton>
);
};
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space?: Room;
@ -61,7 +141,6 @@ interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
interface IItemState {
collapsed: boolean;
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
childSpaces: Room[];
}
@ -81,7 +160,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.state = {
collapsed: collapsed,
contextMenuPosition: null,
childSpaces: this.childSpaces,
};
@ -124,19 +202,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
evt.stopPropagation();
};
private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return;
ev.preventDefault();
ev.stopPropagation();
this.setState({
contextMenuPosition: {
right: ev.clientX,
top: ev.clientY,
height: 0,
},
});
};
private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
const action = getKeyBindingsManager().getRoomListAction(ev);
@ -180,188 +245,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
SpaceStore.instance.setActiveSpace(this.props.space);
};
private onMenuOpenClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};
private onMenuClose = () => {
this.setState({ contextMenuPosition: null });
};
private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showSpaceInvite(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showSpaceSettings(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onLeaveClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "leave_room",
room_id: this.props.space.roomId,
});
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onNewRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCreateNewRoom(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (!RoomViewStore.getRoomId()) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
}, true);
}
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList,
refireParams: { space: this.props.space },
});
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onExploreRoomsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
});
this.setState({ contextMenuPosition: null }); // also close the menu
};
private renderContextMenu(): React.ReactElement {
if (this.props.space.getMyMembership() !== "join") return null;
let contextMenu = null;
if (this.state.contextMenuPosition) {
const userId = this.context.getUserId();
let inviteOption;
if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
inviteOption = (
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite people")}
onClick={this.onInviteClick}
/>
);
}
let settingsOption;
let leaveSection;
if (shouldShowSpaceSettings(this.props.space)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("Settings")}
onClick={this.onSettingsClick}
/>
);
} else {
leaveSection = <IconizedContextMenuOptionList red first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconLeave"
label={_t("Leave space")}
onClick={this.onLeaveClick}
/>
</IconizedContextMenuOptionList>;
}
const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let newRoomSection;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Create new room")}
onClick={this.onNewRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
</IconizedContextMenuOptionList>;
}
contextMenu = <IconizedContextMenu
{...toRightOf(this.state.contextMenuPosition, 0)}
onFinished={this.onMenuClose}
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ this.props.space.name }
</div>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
onClick={this.onMembersClick}
/>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={this.onExploreRoomsClick}
/>
</IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection }
</IconizedContextMenu>;
}
return (
<React.Fragment>
<ContextMenuTooltipButton
className="mx_SpaceButton_menuButton"
onClick={this.onMenuOpenClick}
title={_t("Space options")}
isExpanded={!!this.state.contextMenuPosition}
/>
{ contextMenu }
</React.Fragment>
);
}
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
@ -369,7 +252,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const collapsed = this.isCollapsed;
const isActive = activeSpaces.includes(space);
const itemClasses = classNames(this.props.className, {
"mx_SpaceItem": true,
"mx_SpaceItem_narrow": isPanelCollapsed,
@ -378,12 +260,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
});
const isInvite = space.getMyMembership() === "invite";
const classes = classNames("mx_SpaceButton", {
mx_SpaceButton_active: isActive,
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
mx_SpaceButton_narrow: isPanelCollapsed,
mx_SpaceButton_invite: isInvite,
});
const notificationState = isInvite
? StaticNotificationState.forSymbol("!", NotificationColor.Red)
: SpaceStore.instance.getNotificationState(space.roomId);
@ -398,19 +275,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
/>;
}
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space)}
forceCount={false}
notification={notificationState}
/>
</div>;
}
const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = this.state.childSpaces?.length ?
<AccessibleButton
className="mx_SpaceButton_toggleCollapse"
@ -421,25 +285,23 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
return (
<li {...otherProps} className={itemClasses} ref={innerRef}>
<RovingAccessibleTooltipButton
className={classes}
title={space.name}
<SpaceButton
space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined}
selected={activeSpaces.includes(space)}
label={space.name}
contextMenuTooltip={_t("Space options")}
notificationState={notificationState}
isNarrow={isPanelCollapsed}
avatarSize={isNested ? 24 : 32}
onClick={this.onClick}
onContextMenu={this.onContextMenu}
forceHide={!isPanelCollapsed || !!this.state.contextMenuPosition}
role="treeitem"
aria-expanded={!collapsed}
inputRef={this.buttonRef}
onKeyDown={this.onKeyDown}
aria-expanded={!collapsed}
ContextMenuComponent={this.props.space.getMyMembership() === "join"
? SpaceContextMenu : undefined}
>
{ toggleCollapseButton }
<div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
{ !isPanelCollapsed && <span className="mx_SpaceButton_name">{ space.name }</span> }
{ notifBadge }
{ this.renderContextMenu() }
</div>
</RovingAccessibleTooltipButton>
</SpaceButton>
{ childItems }
</li>

View file

@ -23,9 +23,21 @@ interface IProps {
feed: CallFeed;
}
export default class AudioFeed extends React.Component<IProps> {
interface IState {
audioMuted: boolean;
}
export default class AudioFeed extends React.Component<IProps, IState> {
private element = createRef<HTMLAudioElement>();
constructor(props: IProps) {
super(props);
this.state = {
audioMuted: this.props.feed.isAudioMuted(),
};
}
componentDidMount() {
MediaDeviceHandler.instance.addListener(
MediaDeviceHandlerEvent.AudioOutputChanged,
@ -62,6 +74,7 @@ export default class AudioFeed extends React.Component<IProps> {
private playMedia() {
const element = this.element.current;
if (!element) return;
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
element.muted = false;
element.srcObject = this.props.feed.stream;
@ -85,6 +98,7 @@ export default class AudioFeed extends React.Component<IProps> {
private stopMedia() {
const element = this.element.current;
if (!element) return;
element.pause();
element.src = null;
@ -96,10 +110,16 @@ export default class AudioFeed extends React.Component<IProps> {
}
private onNewStream = () => {
this.setState({
audioMuted: this.props.feed.isAudioMuted(),
});
this.playMedia();
};
render() {
// Do not render the audio element if there is no audio track
if (this.state.audioMuted) return null;
return (
<audio ref={this.element} />
);

View file

@ -146,7 +146,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
window.addEventListener("resize", this.snap);
window.addEventListener("resize", this.onResize);
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
}
@ -156,7 +156,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
document.removeEventListener("mousemove", this.onMoving);
document.removeEventListener("mouseup", this.onEndMoving);
window.removeEventListener("resize", this.snap);
window.removeEventListener("resize", this.onResize);
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
@ -164,6 +164,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
SettingsStore.unwatchSetting(this.settingsWatcherRef);
}
private onResize = (): void => {
this.snap(false);
};
private animationCallback = () => {
// If the PiP isn't being dragged and there is only a tiny difference in
// the desiredTranslation and translation, quit the animationCallback
@ -207,7 +211,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
}
}
private snap = () => {
private snap(animate?: boolean): void {
const translationX = this.desiredTranslationX;
const translationY = this.desiredTranslationY;
// We subtract the PiP size from the window size in order to calculate
@ -236,10 +240,17 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.desiredTranslationY = PADDING.top;
}
// We start animating here because we want the PiP to move when we're
// resizing the window
this.scheduledUpdate.mark();
};
if (animate) {
// We start animating here because we want the PiP to move when we're
// resizing the window
this.scheduledUpdate.mark();
} else {
this.setState({
translationX: this.desiredTranslationX,
translationY: this.desiredTranslationY,
});
}
}
private onRoomViewStoreUpdate = () => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
@ -310,7 +321,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
private onEndMoving = () => {
this.moving = false;
this.snap();
this.snap(true);
};
public render() {
@ -333,6 +344,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
secondaryCall={this.state.secondaryCall}
pipMode={true}
onMouseDownOnHeader={this.onStartMoving}
onResize={this.onResize}
/>
</div>
);

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
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.
@ -32,6 +33,10 @@ import { avatarUrlForMember } from '../../../Avatar';
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
import Modal from '../../../Modal';
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
import CallViewSidebar from './CallViewSidebar';
interface IProps {
// The call for us to display
@ -59,11 +64,15 @@ interface IState {
isRemoteOnHold: boolean;
micMuted: boolean;
vidMuted: boolean;
screensharing: boolean;
callState: CallState;
controlsVisible: boolean;
hoveringControls: boolean;
showMoreMenu: boolean;
showDialpad: boolean;
feeds: CallFeed[];
primaryFeed: CallFeed;
secondaryFeeds: Array<CallFeed>;
sidebarShown: boolean;
}
function getFullScreenElement() {
@ -94,7 +103,7 @@ function exitFullscreen() {
if (exitMethod) exitMethod.call(document);
}
const CONTROLS_HIDE_DELAY = 1000;
const CONTROLS_HIDE_DELAY = 2000;
// Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
@ -106,20 +115,27 @@ export default class CallView extends React.Component<IProps, IState> {
private controlsHideTimer: number = null;
private dialpadButton = createRef<HTMLDivElement>();
private contextMenuButton = createRef<HTMLDivElement>();
private contextMenu = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
const { primary, secondary } = this.getOrderedFeeds(this.props.call.getFeeds());
this.state = {
isLocalOnHold: this.props.call.isLocalOnHold(),
isRemoteOnHold: this.props.call.isRemoteOnHold(),
micMuted: this.props.call.isMicrophoneMuted(),
vidMuted: this.props.call.isLocalVideoMuted(),
screensharing: this.props.call.isScreensharing(),
callState: this.props.call.state,
controlsVisible: true,
hoveringControls: false,
showMoreMenu: false,
showDialpad: false,
feeds: this.props.call.getFeeds(),
primaryFeed: primary,
secondaryFeeds: secondary,
sidebarShown: true,
};
this.updateCallListeners(null, this.props.call);
@ -194,7 +210,11 @@ export default class CallView extends React.Component<IProps, IState> {
};
private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
this.setState({ feeds: newFeeds });
const { primary, secondary } = this.getOrderedFeeds(newFeeds);
this.setState({
primaryFeed: primary,
secondaryFeeds: secondary,
});
};
private onCallLocalHoldUnhold = () => {
@ -227,6 +247,7 @@ export default class CallView extends React.Component<IProps, IState> {
};
private onControlsHideTimer = () => {
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
this.controlsHideTimer = null;
this.setState({
controlsVisible: false,
@ -237,7 +258,30 @@ export default class CallView extends React.Component<IProps, IState> {
this.showControls();
};
private showControls() {
private getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } {
let primary;
// Try to use a screensharing as primary, a remote one if possible
const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
primary = screensharingFeeds.find((feed) => !feed.isLocal()) || screensharingFeeds[0];
// If we didn't find remote screen-sharing stream, try to find any remote stream
if (!primary) {
primary = feeds.find((feed) => !feed.isLocal());
}
const secondary = [...feeds];
// Remove the primary feed from the array
if (primary) secondary.splice(secondary.indexOf(primary), 1);
secondary.sort((a, b) => {
if (a.isLocal() && !b.isLocal()) return -1;
if (!a.isLocal() && b.isLocal()) return 1;
return 0;
});
return { primary, secondary };
}
private showControls(): void {
if (this.state.showMoreMenu || this.state.showDialpad) return;
if (!this.state.controlsVisible) {
@ -251,73 +295,62 @@ export default class CallView extends React.Component<IProps, IState> {
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
private onDialpadClick = () => {
private onDialpadClick = (): void => {
if (!this.state.showDialpad) {
if (this.controlsHideTimer) {
clearTimeout(this.controlsHideTimer);
this.controlsHideTimer = null;
}
this.setState({
showDialpad: true,
controlsVisible: true,
});
this.setState({ showDialpad: true });
this.showControls();
} else {
if (this.controlsHideTimer !== null) {
clearTimeout(this.controlsHideTimer);
}
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
this.setState({
showDialpad: false,
});
this.setState({ showDialpad: false });
}
};
private onMicMuteClick = () => {
private onMicMuteClick = (): void => {
const newVal = !this.state.micMuted;
this.props.call.setMicrophoneMuted(newVal);
this.setState({ micMuted: newVal });
};
private onVidMuteClick = () => {
private onVidMuteClick = (): void => {
const newVal = !this.state.vidMuted;
this.props.call.setLocalVideoMuted(newVal);
this.setState({ vidMuted: newVal });
};
private onMoreClick = () => {
if (this.controlsHideTimer) {
clearTimeout(this.controlsHideTimer);
this.controlsHideTimer = null;
}
private onScreenshareClick = async (): Promise<void> => {
const isScreensharing = await this.props.call.setScreensharingEnabled(
!this.state.screensharing,
async (): Promise<DesktopCapturerSource> => {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
},
);
this.setState({
showMoreMenu: true,
controlsVisible: true,
sidebarShown: true,
screensharing: isScreensharing,
});
};
private closeDialpad = () => {
this.setState({
showDialpad: false,
});
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
private onMoreClick = (): void => {
this.setState({ showMoreMenu: true });
this.showControls();
};
private closeContextMenu = () => {
this.setState({
showMoreMenu: false,
});
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
private closeDialpad = (): void => {
this.setState({ showDialpad: false });
};
private closeContextMenu = (): void => {
this.setState({ showMoreMenu: false });
};
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
// Note that this assumes we always have a CallView on screen at any given time
// CallHandler would probably be a better place for this
private onNativeKeyDown = ev => {
private onNativeKeyDown = (ev): void => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
@ -347,7 +380,16 @@ export default class CallView extends React.Component<IProps, IState> {
}
};
private onRoomAvatarClick = () => {
private onCallControlsMouseEnter = (): void => {
this.setState({ hoveringControls: true });
this.showControls();
};
private onCallControlsMouseLeave = (): void => {
this.setState({ hoveringControls: false });
};
private onRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
dis.dispatch({
action: 'view_room',
@ -355,7 +397,7 @@ export default class CallView extends React.Component<IProps, IState> {
});
};
private onSecondaryRoomAvatarClick = () => {
private onSecondaryRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
dis.dispatch({
@ -364,50 +406,30 @@ export default class CallView extends React.Component<IProps, IState> {
});
};
private onCallResumeClick = () => {
private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
};
private onTransferClick = () => {
private onTransferClick = (): void => {
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
this.props.call.transferToCall(transfereeCall);
};
public render() {
const client = MatrixClientPeg.get();
const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
const callRoom = client.getRoom(callRoomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
private onHangupClick = (): void => {
dis.dispatch({
action: 'hangup',
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
});
};
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
{...alwaysAboveLeftOf(
this.contextMenuButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
onFinished={this.closeContextMenu}
call={this.props.call}
/>;
}
private onToggleSidebar = (): void => {
this.setState({
sidebarShown: !this.state.sidebarShown,
});
};
private renderCallControls(): JSX.Element {
const micClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: !this.state.micMuted,
@ -420,6 +442,18 @@ export default class CallView extends React.Component<IProps, IState> {
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
});
const screensharingClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_screensharingOn: this.state.screensharing,
mx_CallView_callControls_button_screensharingOff: !this.state.screensharing,
});
const sidebarButtonClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown,
mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown,
});
// Put the other states of the mic/video icons in the document to make sure they're cached
// (otherwise the icon disappears briefly when toggled)
const micCacheClasses = classNames({
@ -441,59 +475,151 @@ export default class CallView extends React.Component<IProps, IState> {
mx_CallView_callControls_hidden: !this.state.controlsVisible,
});
const vidMuteButton = this.props.call.type === CallType.Video ? <AccessibleButton
className={vidClasses}
onClick={this.onVidMuteClick}
/> : null;
// We don't support call upgrades (yet) so hide the video mute button in voice calls
let vidMuteButton;
if (this.props.call.type === CallType.Video) {
vidMuteButton = (
<AccessibleButton
className={vidClasses}
onClick={this.onVidMuteClick}
/>
);
}
// Screensharing is possible, if we can send a second stream and
// identify it using SDPStreamMetadata or if we can replace the already
// existing usermedia track by a screensharing track. We also need to be
// connected to know the state of the other side
let screensharingButton;
if (
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
this.props.call.state === CallState.Connected
) {
screensharingButton = (
<AccessibleButton
className={screensharingClasses}
onClick={this.onScreenshareClick}
/>
);
}
// To show the sidebar we need secondary feeds, if we don't have them,
// we can hide this button. If we are in PiP, sidebar is also hidden, so
// we can hide the button too
let sidebarButton;
if (
!this.props.pipMode &&
(
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
this.props.call.isScreensharing()
)
) {
sidebarButton = (
<AccessibleButton
className={sidebarButtonClasses}
onClick={this.onToggleSidebar}
/>
);
}
// 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" />;
let contextMenuButton;
if (this.state.callState === CallState.Connected) {
contextMenuButton = (
<ContextMenuButton
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
onClick={this.onMoreClick}
inputRef={this.contextMenuButton}
isExpanded={this.state.showMoreMenu}
/>
);
}
let dialpadButton;
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
dialpadButton = (
<ContextMenuButton
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
inputRef={this.dialpadButton}
onClick={this.onDialpadClick}
isExpanded={this.state.showDialpad}
/>
);
}
const contextMenuButton = this.state.callState === CallState.Connected ? <ContextMenuButton
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
onClick={this.onMoreClick}
inputRef={this.contextMenuButton}
isExpanded={this.state.showMoreMenu}
/> : <div className="mx_CallView_callControls_button mx_CallView_callControls_button_more_hidden" />;
let dialPad;
if (this.state.showDialpad) {
dialPad = <DialpadContextMenu
{...alwaysAboveRightOf(
this.dialpadButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
mountAsChild={true}
onFinished={this.closeDialpad}
call={this.props.call}
/>;
}
// 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}>
{ dialpadButton }
<AccessibleButton
className={micClasses}
onClick={this.onMicMuteClick}
/>
<AccessibleButton
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
onClick={() => {
dis.dispatch({
action: 'hangup',
room_id: callRoomId,
});
}}
/>
{ vidMuteButton }
<div className={micCacheClasses} />
<div className={vidCacheClasses} />
{ contextMenuButton }
</div>;
let contextMenu;
if (this.state.showMoreMenu) {
contextMenu = <CallContextMenu
{...alwaysAboveLeftOf(
this.contextMenuButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
mountAsChild={true}
onFinished={this.closeContextMenu}
call={this.props.call}
/>;
}
return (
<div
className={callControlsClasses}
onMouseEnter={this.onCallControlsMouseEnter}
onMouseLeave={this.onCallControlsMouseLeave}
>
{ dialPad }
{ contextMenu }
{ dialpadButton }
<AccessibleButton
className={micClasses}
onClick={this.onMicMuteClick}
/>
{ vidMuteButton }
<div className={micCacheClasses} />
<div className={vidCacheClasses} />
{ screensharingButton }
{ sidebarButton }
{ contextMenuButton }
<AccessibleButton
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
onClick={this.onHangupClick}
/>
</div>
);
}
public render() {
const client = MatrixClientPeg.get();
const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
const callRoom = client.getRoom(callRoomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const avatarSize = this.props.pipMode ? 76 : 160;
// The 'content' for the call, ie. the videos for a video call and profile picture
// for voice calls (fills the bg)
let contentView: React.ReactNode;
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
const isScreensharing = this.props.call.isScreensharing();
const sidebarShown = this.state.sidebarShown;
const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
});
const isVideoCall = this.props.call.type === CallType.Video;
let contentView: React.ReactNode;
let holdTransferContent;
if (transfereeCall) {
const transferTargetRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(this.props.call),
@ -539,9 +665,25 @@ export default class CallView extends React.Component<IProps, IState> {
</div>;
}
let sidebar;
if (
!isOnHold &&
!transfereeCall &&
sidebarShown &&
(isVideoCall || someoneIsScreensharing)
) {
sidebar = (
<CallViewSidebar
feeds={this.state.secondaryFeeds}
call={this.props.call}
pipMode={this.props.pipMode}
/>
);
}
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
if (isOnHold || transfereeCall) {
if (this.props.call.type === CallType.Video) {
if (isVideoCall) {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
@ -560,7 +702,7 @@ export default class CallView extends React.Component<IProps, IState> {
<div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
{ onHoldBackground }
{ holdTransferContent }
{ callControls }
{ this.renderCallControls() }
</div>
);
} else {
@ -585,7 +727,7 @@ export default class CallView extends React.Component<IProps, IState> {
</div>
</div>
{ holdTransferContent }
{ callControls }
{ this.renderCallControls() }
</div>
);
}
@ -599,77 +741,91 @@ export default class CallView extends React.Component<IProps, IState> {
mx_CallView_voice: true,
});
const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
// Here we check to hide local audio feeds to achieve the same UI/UX
// as before. But once again this might be subject to change
if (feed.isVideoMuted()) return;
return (
<VideoFeed
key={i}
feed={feed}
call={this.props.call}
pipMode={this.props.pipMode}
onResize={this.props.onResize}
/>
);
});
// Saying "Connecting" here isn't really true, but the best thing
// I can come up with, but this might be subject to change as well
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
{ feeds }
<div className="mx_CallView_voice_avatarsContainer">
<div className="mx_CallView_voice_avatarContainer" style={{ width: avatarSize, height: avatarSize }}>
<RoomAvatar
room={callRoom}
height={avatarSize}
width={avatarSize}
/>
contentView = (
<div
className={classes}
onMouseMove={this.onMouseMove}
>
{ sidebar }
<div className="mx_CallView_voice_avatarsContainer">
<div
className="mx_CallView_voice_avatarContainer"
style={{ width: avatarSize, height: avatarSize }}
>
<RoomAvatar
room={callRoom}
height={avatarSize}
width={avatarSize}
/>
</div>
</div>
<div className="mx_CallView_holdTransferContent">{ _t("Connecting") }</div>
{ this.renderCallControls() }
</div>
<div className="mx_CallView_holdTransferContent">{ _t("Connecting") }</div>
{ callControls }
</div>;
);
} else {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
});
// TODO: Later the CallView should probably be reworked to support
// any number of feeds but now we can always expect there to be two
// feeds. This is because the js-sdk ignores any new incoming streams
const feeds = this.state.feeds.map((feed, i) => {
// Here we check to hide local audio feeds to achieve the same UI/UX
// as before. But once again this might be subject to change
if (feed.isVideoMuted() && feed.isLocal()) return;
return (
let toast;
if (someoneIsScreensharing) {
const presentingClasses = classNames({
mx_CallView_presenting: true,
mx_CallView_presenting_hidden: !this.state.controlsVisible,
});
const sharerName = this.state.primaryFeed.getMember().name;
let text = isScreensharing
? _t("You are presenting")
: _t('%(sharerName)s is presenting', { sharerName });
if (!this.state.sidebarShown && isVideoCall) {
text += " • " + (this.props.call.isLocalVideoMuted()
? _t("Your camera is turned off")
: _t("Your camera is still enabled"));
}
toast = (
<div className={presentingClasses}>
{ text }
</div>
);
}
contentView = (
<div
className={containerClasses}
ref={this.contentRef}
onMouseMove={this.onMouseMove}
>
{ toast }
{ sidebar }
<VideoFeed
key={i}
feed={feed}
feed={this.state.primaryFeed}
call={this.props.call}
pipMode={this.props.pipMode}
onResize={this.props.onResize}
primary={true}
/>
);
});
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
{ feeds }
{ callControls }
</div>;
{ this.renderCallControls() }
</div>
);
}
const callTypeText = this.props.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
let myClassName;
let fullScreenButton;
if (this.props.call.type === CallType.Video && !this.props.pipMode) {
fullScreenButton = <div
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
onClick={this.onFullscreenClick}
title={_t("Fill Screen")}
/>;
if (!this.props.pipMode) {
fullScreenButton = (
<div
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
onClick={this.onFullscreenClick}
title={_t("Fill Screen")}
/>
);
}
let expandButton;
@ -686,10 +842,15 @@ export default class CallView extends React.Component<IProps, IState> {
{ expandButton }
</div>;
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
"mx_CallView_header_callTypeIcon_video": isVideoCall,
});
let header: React.ReactNode;
if (!this.props.pipMode) {
header = <div className="mx_CallView_header">
<div className="mx_CallView_header_phoneIcon" />
<div className={callTypeIconClassName} />
<span className="mx_CallView_header_callType">{ callTypeText }</span>
{ headerControls }
</div>;
@ -731,8 +892,6 @@ export default class CallView extends React.Component<IProps, IState> {
return <div className={"mx_CallView " + myClassName}>
{ header }
{ contentView }
{ dialPad }
{ contextMenu }
</div>;
}
}

View file

@ -0,0 +1,53 @@
/*
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 { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import VideoFeed from "./VideoFeed";
import classNames from "classnames";
interface IProps {
feeds: Array<CallFeed>;
call: MatrixCall;
pipMode: boolean;
}
export default class CallViewSidebar extends React.Component<IProps> {
render() {
const feeds = this.props.feeds.map((feed) => {
return (
<VideoFeed
key={feed.stream.id}
feed={feed}
call={this.props.call}
primary={false}
pipMode={this.props.pipMode}
/>
);
});
const className = classNames("mx_CallViewSidebar", {
mx_CallViewSidebar_pipMode: this.props.pipMode,
});
return (
<div className={className}>
{ feeds }
</div>
);
}
}

View file

@ -16,12 +16,13 @@ limitations under the License.
import classnames from 'classnames';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import React, { createRef } from 'react';
import React from 'react';
import SettingsStore from "../../../settings/SettingsStore";
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger';
import MemberAvatar from "../avatars/MemberAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
interface IProps {
call: MatrixCall;
@ -37,6 +38,8 @@ interface IProps {
// a callback which is called when the video element is resized
// due to a change in video metadata
onResize?: (e: Event) => void;
primary: boolean;
}
interface IState {
@ -45,8 +48,8 @@ interface IState {
}
@replaceableComponent("views.voip.VideoFeed")
export default class VideoFeed extends React.Component<IProps, IState> {
private element = createRef<HTMLVideoElement>();
export default class VideoFeed extends React.PureComponent<IProps, IState> {
private element: HTMLVideoElement;
constructor(props: IProps) {
super(props);
@ -58,18 +61,59 @@ export default class VideoFeed extends React.Component<IProps, IState> {
}
componentDidMount() {
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.updateFeed(null, this.props.feed);
this.playMedia();
}
componentWillUnmount() {
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
this.element.current?.removeEventListener('resize', this.onResize);
this.stopMedia();
this.updateFeed(this.props.feed, null);
}
componentDidUpdate(prevProps: IProps, prevState: IState) {
this.updateFeed(prevProps.feed, this.props.feed);
// If the mutes state has changed, we try to playMedia()
if (
prevState.videoMuted !== this.state.videoMuted ||
prevProps.feed.stream !== this.props.feed.stream
) {
this.playMedia();
}
}
static getDerivedStateFromProps(props: IProps) {
return {
audioMuted: props.feed.isAudioMuted(),
videoMuted: props.feed.isVideoMuted(),
};
}
private setElementRef = (element: HTMLVideoElement): void => {
if (!element) {
this.element?.removeEventListener('resize', this.onResize);
return;
}
this.element = element;
element.addEventListener('resize', this.onResize);
};
private updateFeed(oldFeed: CallFeed, newFeed: CallFeed) {
if (oldFeed === newFeed) return;
if (oldFeed) {
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
this.props.feed.removeListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
this.stopMedia();
}
if (newFeed) {
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.props.feed.addListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged);
this.playMedia();
}
}
private playMedia() {
const element = this.element.current;
const element = this.element;
if (!element) return;
// We play audio in AudioFeed, not here
element.muted = true;
@ -92,7 +136,7 @@ export default class VideoFeed extends React.Component<IProps, IState> {
}
private stopMedia() {
const element = this.element.current;
const element = this.element;
if (!element) return;
element.pause();
@ -109,7 +153,13 @@ export default class VideoFeed extends React.Component<IProps, IState> {
audioMuted: this.props.feed.isAudioMuted(),
videoMuted: this.props.feed.isVideoMuted(),
});
this.playMedia();
};
private onMuteStateChanged = () => {
this.setState({
audioMuted: this.props.feed.isAudioMuted(),
videoMuted: this.props.feed.isVideoMuted(),
});
};
private onResize = (e) => {
@ -119,35 +169,58 @@ export default class VideoFeed extends React.Component<IProps, IState> {
};
render() {
const videoClasses = {
mx_VideoFeed: true,
mx_VideoFeed_local: this.props.feed.isLocal(),
mx_VideoFeed_remote: !this.props.feed.isLocal(),
const { pipMode, primary, feed } = this.props;
const wrapperClasses = classnames("mx_VideoFeed", {
mx_VideoFeed_voice: this.state.videoMuted,
mx_VideoFeed_video: !this.state.videoMuted,
mx_VideoFeed_mirror: (
this.props.feed.isLocal() &&
SettingsStore.getValue('VideoView.flipVideoHorizontally')
),
};
});
const micIconClasses = classnames("mx_VideoFeed_mic", {
mx_VideoFeed_mic_muted: this.state.audioMuted,
mx_VideoFeed_mic_unmuted: !this.state.audioMuted,
});
if (this.state.videoMuted) {
const member = this.props.feed.getMember();
const avatarSize = this.props.pipMode ? 76 : 160;
return (
<div className={classnames(videoClasses)}>
<MemberAvatar
member={member}
height={avatarSize}
width={avatarSize}
/>
</div>
);
} else {
return (
<video className={classnames(videoClasses)} ref={this.element} />
let micIcon;
if (feed.purpose !== SDPStreamMetadataPurpose.Screenshare && !pipMode) {
micIcon = (
<div className={micIconClasses} />
);
}
let content;
if (this.state.videoMuted) {
const member = this.props.feed.getMember();
let avatarSize;
if (pipMode && primary) avatarSize = 76;
else if (pipMode && !primary) avatarSize = 16;
else if (!pipMode && primary) avatarSize = 160;
else; // TBD
content =(
<MemberAvatar
member={member}
height={avatarSize}
width={avatarSize}
/>
);
} else {
const videoClasses = classnames("mx_VideoFeed_video", {
mx_VideoFeed_video_mirror: (
this.props.feed.isLocal() &&
SettingsStore.getValue('VideoView.flipVideoHorizontally')
),
});
content= (
<video className={videoClasses} ref={this.setElementRef} />
);
}
return (
<div className={wrapperClasses}>
{ micIcon }
{ content }
</div>
);
}
}

View file

@ -18,9 +18,15 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
import { JoinRule, Preset, RestrictedAllowType, Visibility } from "matrix-js-sdk/src/@types/partials";
import {
HistoryVisibility,
JoinRule,
Preset,
RestrictedAllowType,
Visibility,
} from "matrix-js-sdk/src/@types/partials";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
@ -52,6 +58,9 @@ export interface IOpts {
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
avatar?: File | string; // will upload if given file, else mxcUrl is needed
roomType?: RoomType | string;
historyVisibility?: HistoryVisibility;
parentSpace?: Room;
joinRule?: JoinRule;
}
@ -112,6 +121,13 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
createOpts.is_direct = true;
}
if (opts.roomType) {
createOpts.creation_content = {
...createOpts.creation_content,
[RoomCreateTypeField]: opts.roomType,
};
}
// By default, view the room after creating it
if (opts.andView === undefined) {
opts.andView = true;
@ -144,12 +160,11 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
if (opts.parentSpace) {
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
createOpts.initial_state.push({
type: EventType.RoomHistoryVisibility,
content: {
"history_visibility": createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
},
});
if (!opts.historyVisibility) {
opts.historyVisibility = createOpts.preset === Preset.PublicChat
? HistoryVisibility.WorldReadable
: HistoryVisibility.Invited;
}
if (opts.joinRule === JoinRule.Restricted) {
if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) {
@ -169,13 +184,35 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
}
}
if (opts.joinRule !== JoinRule.Restricted) {
// we handle the restricted join rule in the parentSpace handling block above
if (opts.joinRule && opts.joinRule !== JoinRule.Restricted) {
createOpts.initial_state.push({
type: EventType.RoomJoinRules,
content: { join_rule: opts.joinRule },
});
}
if (opts.avatar) {
let url = opts.avatar;
if (opts.avatar instanceof File) {
url = await client.uploadContent(opts.avatar);
}
createOpts.initial_state.push({
type: EventType.RoomAvatar,
content: { url },
});
}
if (opts.historyVisibility) {
createOpts.initial_state.push({
type: EventType.RoomHistoryVisibility,
content: {
"history_visibility": opts.historyVisibility,
},
});
}
let modal;
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');

View file

@ -0,0 +1,53 @@
/*
* 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.
*/
// Populate this class with the details of your customisations when copying it.
import { ITemplateParams } from "matrix-widget-api";
/**
* Provides a partial set of the variables needed to render any widget. If
* variables are missing or not provided then they will be filled with the
* application-determined defaults.
*
* This will not be called until after isReady() resolves.
* @returns {Partial<Omit<ITemplateParams, "widgetRoomId">>} The variables.
*/
function provideVariables(): Partial<Omit<ITemplateParams, "widgetRoomId">> {
return {};
}
/**
* Resolves to whether or not the customisation point is ready for variables
* to be provided. This will block widgets being rendered.
* @returns {Promise<boolean>} Resolves when ready.
*/
async function isReady(): Promise<void> {
return; // default no waiting
}
// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IWidgetVariablesCustomisations {
provideVariables?: typeof provideVariables;
// If not provided, the app will assume that the customisation is always ready.
isReady?: typeof isReady;
}
// A real customisation module will define and export one or more of the
// customisation points that make up the interface above.
export const WidgetVariableCustomisations: IWidgetVariablesCustomisations = {};

View file

@ -193,4 +193,16 @@ export enum Action {
* Switches space. Should be used with SwitchSpacePayload.
*/
SwitchSpace = "switch_space",
/**
* Signals to the visible space hierarchy that a change has occurred an that it should refresh.
*/
UpdateSpaceHierarchy = "update_space_hierarchy",
/**
* Fires when a monitored setting is updated,
* see SettingsStore::monitorSetting for more details.
* Should be used with SettingUpdatedPayload.
*/
SettingUpdated = "setting_updated",
}

View file

@ -0,0 +1,29 @@
/*
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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { SettingLevel } from "../../settings/SettingLevel";
export interface SettingUpdatedPayload extends ActionPayload {
action: Action.SettingUpdated;
settingName: string;
roomId: string;
level: SettingLevel;
newValueAtLevel: SettingLevel;
newValue: any;
}

View file

@ -121,6 +121,12 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
return partCreator.plain(`\`${n.textContent}\``);
case "DEL":
return partCreator.plain(`<del>${n.textContent}</del>`);
case "SUB":
return partCreator.plain(`<sub>${n.textContent}</sub>`);
case "SUP":
return partCreator.plain(`<sup>${n.textContent}</sup>`);
case "U":
return partCreator.plain(`<u>${n.textContent}</u>`);
case "LI": {
const indent = " ".repeat(state.listDepth - 1);
if (n.parentElement.nodeName === "OL") {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useRef, useEffect } from "react";
import { useRef, useEffect, useState, useCallback } from "react";
import type { EventEmitter } from "events";
type Handler = (...args: any[]) => void;
@ -48,3 +48,14 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo
[eventName, emitter], // Re-run if eventName or emitter changes
);
};
type Mapper<T> = (...args: any[]) => T;
export const useEventEmitterState = <T>(emitter: EventEmitter, eventName: string | symbol, fn: Mapper<T>): T => {
const [value, setValue] = useState<T>(fn());
const handler = useCallback((...args: any[]) => {
setValue(fn(...args));
}, [fn]);
useEventEmitter(emitter, eventName, handler);
return value;
};

View file

@ -1552,5 +1552,15 @@
"Too Many Calls": "مكالمات كثيرة جدا",
"Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك قم بالتأكد.",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.",
"Explore rooms": "استكشِف الغرف"
"Explore rooms": "استكشِف الغرف",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات <helpIcon /> مع %(widgetDomain)s ومدير التكامل الخاص بك.",
"Identity server is": "خادم الهوية هو",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
"Identity server": "خادم الهوية",
"Identity server (%(server)s)": "خادمة الهوية (%(server)s)",
"Could not connect to identity server": "تعذر الاتصال بخادم هوية",
"Not a valid identity server (status code %(code)s)": "خادم هوية مردود (رقم الحال %(code)s)",
"Identity server URL must be HTTPS": "يجب أن يكون رابط (URL) خادم الهوية HTTPS"
}

View file

@ -383,5 +383,7 @@
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.",
"Create Account": "Hesab Aç",
"Explore rooms": "Otaqları kəşf edin",
"Sign In": "Daxil ol"
"Sign In": "Daxil ol",
"Identity server is": "Eyniləşdirmənin serveri bu",
"Identity server": "Eyniləşdirmənin serveri"
}

View file

@ -2897,5 +2897,17 @@
"Already in call": "Вече в разговор",
"You're already in a call with this person.": "Вече сте в разговор в този човек.",
"Too Many Calls": "Твърде много повиквания",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно."
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно.",
"Integration manager": "Мениджър на интеграции",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Вашият %(brand)s не позволява да използвате мениджъра на интеграции за да направите това. Свържете се с администратор.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Използването на това приспособление може да сподели данни <helpIcon /> с %(widgetDomain)s и с мениджъра на интеграции.",
"Identity server is": "Сървър за самоличност:",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Мениджърът на интеграции получава конфигурационни данни, може да модифицира приспособления, да изпраща покани за стаи и да настройва нива на достъп от ваше име.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции за управление на ботове, приспособления и стикери.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции <b>%(serverName)s</b> за управление на ботове, приспособления и стикери.",
"Identity server": "Сървър за самоличност",
"Identity server (%(server)s)": "Сървър за самоличност (%(server)s)",
"Could not connect to identity server": "Неуспешна връзка със сървъра за самоличност",
"Not a valid identity server (status code %(code)s)": "Невалиден сървър за самоличност (статус код %(code)s)",
"Identity server URL must be HTTPS": "Адресът на сървъра за самоличност трябва да бъде HTTPS"
}

View file

@ -1 +1,4 @@
{}
{
"Integration manager": "ইন্টিগ্রেশন ম্যানেজার",
"Identity server": "পরিচয় সার্ভার"
}

View file

@ -1 +1,4 @@
{}
{
"Integration manager": "ইন্টিগ্রেশন ম্যানেজার",
"Identity server": "পরিচয় সার্ভার"
}

View file

@ -2,5 +2,6 @@
"Dismiss": "Odbaci",
"Create Account": "Otvori račun",
"Sign In": "Prijavite se",
"Explore rooms": "Istražite sobe"
"Explore rooms": "Istražite sobe",
"Identity server": "Identifikacioni Server"
}

View file

@ -953,5 +953,10 @@
"Unable to access microphone": "No s'ha pogut accedir al micròfon",
"Explore rooms": "Explora sales",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops"
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops",
"Integration manager": "Gestor d'integracions",
"Identity server is": "El servidor d'identitat és",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Els gestors d'integracions reben dades de configuració i poden modificar ginys, enviar invitacions a sales i establir nivells d'autoritat en nom teu.",
"Identity server": "Servidor d'identitat",
"Could not connect to identity server": "No s'ha pogut connectar amb el servidor d'identitat"
}

View file

@ -857,7 +857,7 @@
"Failed to upgrade room": "Nepovedlo se upgradeovat místnost",
"The room upgrade could not be completed": "Upgrade místnosti se nepovedlo dokončit",
"Upgrade this room to version %(version)s": "Upgradování místnosti na verzi %(version)s",
"Security & Privacy": "Zabezpečení",
"Security & Privacy": "Zabezpečení a soukromí",
"Encryption": "Šifrování",
"Once enabled, encryption cannot be disabled.": "Po zapnutí, už nepůjde šifrování vypnout.",
"Encrypted": "Šifrováno",
@ -1061,7 +1061,7 @@
"Anchor": "Kotva",
"Headphones": "Sluchátka",
"Folder": "Desky",
"Pin": "Připínáček",
"Pin": "Připnout",
"Yes": "Ano",
"No": "Ne",
"Never lose encrypted messages": "Nikdy nepřijdete o šifrované zprávy",
@ -2250,7 +2250,7 @@
"Send feedback": "Odeslat zpětnou vazbu",
"Feedback": "Zpětná vazba",
"Feedback sent": "Zpětná vazba byla odeslána",
"Security & privacy": "Zabezpečení",
"Security & privacy": "Zabezpečení a soukromí",
"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 novou konverzaci",
@ -3322,7 +3322,7 @@
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Kliknutím na avatar na panelu filtrů můžete kdykoli zobrazit pouze místnosti a lidi spojené s danou komunitou.",
"Move down": "Posun dolů",
"Move up": "Posun nahoru",
"Report": "Zpráva",
"Report": "Nahlásit",
"Collapse reply thread": "Sbalit vlákno odpovědi",
"Show preview": "Zobrazit náhled",
"View source": "Zobrazit zdroj",
@ -3400,5 +3400,107 @@
"Some invites couldn't be sent": "Některé pozvánky nebylo možné odeslat",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Poslali jsme ostatním, ale níže uvedení lidé nemohli být pozváni do <RoomName/>",
"Visibility": "Viditelnost",
"Address": "Adresa"
"Address": "Adresa",
"To view all keyboard shortcuts, click here.": "Pro zobrazení všech klávesových zkratek, klikněte zde.",
"Unnamed audio": "Nepojmenovaný audio soubor",
"Error processing audio message": "Došlo k chybě při zpracovávání hlasové zprávy",
"Images, GIFs and videos": "Obrázky, GIFy a videa",
"Code blocks": "Bloky kódu",
"Displaying time": "Zobrazování času",
"Keyboard shortcuts": "Klávesové zkratky",
"Use Ctrl + F to search timeline": "Stiskněte Ctrl + F k vyhledávání v časové ose",
"Use Command + F to search timeline": "Stiskněte Command + F k vyhledávání v časové ose",
"Integration manager": "Správce integrací",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Váš %(brand)s neumožňuje použít správce integrací. Kontaktujte prosím správce.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Použití tohoto widgetu může sdílet data <helpIcon /> s %(widgetDomain)s a vaším správcem integrací.",
"Identity server is": "Server identity je",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Správci integrace přijímají konfigurační data a mohou vaším jménem upravovat widgety, odesílat pozvánky do místností a nastavovat úrovně oprávnění.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Použít správce integrací na správu botů, widgetů a samolepek.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Použít správce integrací <b>(%(serverName)s)</b> na správu botů, widgetů a samolepek.",
"Identity server": "Server identit",
"Identity server (%(server)s)": "Server identit (%(server)s)",
"Could not connect to identity server": "Nepodařilo se připojit k serveru identit",
"Not a valid identity server (status code %(code)s)": "Toto není platný server identit (stavový kód %(code)s)",
"Identity server URL must be HTTPS": "Adresa serveru identit musí být na HTTPS",
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Upozorňujeme, že aktualizací vznikne nová verze místnosti</b>. Všechny aktuální zprávy zůstanou v této archivované místnosti.",
"Automatically invite members from this room to the new one": "Automaticky pozve členy této místnosti do nové místnosti",
"These are likely ones other room admins are a part of.": "Pravděpodobně se jedná o ty, kterých se účastní i ostatní správci místností.",
"Other spaces or rooms you might not know": "Další prostory nebo místnosti, které možná neznáte",
"Spaces you know that contain this room": "Prostory, které znáte a které obsahují tuto místnost",
"Search spaces": "Hledat prostory",
"Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Rozhodněte, které prostory mají přístup do této místnosti. Pokud je vybrán prostor, mohou jeho členové najít <RoomName/> a připojit se k němu.",
"Select spaces": "Vybrané prostory",
"You're removing all spaces. Access will default to invite only": "Odstraňujete všechny prostory. Přístup bude ve výchozím nastavení pouze na pozvánky",
"User Directory": "Adresář uživatelů",
"Connected": "Připojeno",
"& %(count)s more|other": "a %(count)s dalších",
"Only invited people can join.": "Připojit se mohou pouze pozvané osoby.",
"Private (invite only)": "Soukromé (pouze pro pozvané)",
"This upgrade will allow members of selected spaces access to this room without an invite.": "Tato změna umožní členům vybraných prostorů přístup do této místnosti bez pozvánky.",
"There was an error loading your notification settings.": "Došlo k chybě při načítání nastavení oznámení.",
"Global": "Globální",
"Enable email notifications for %(email)s": "Povolení e-mailových oznámení pro %(email)s",
"Enable for this account": "Povolit pro tento účet",
"An error occurred whilst saving your notification preferences.": "Při ukládání předvoleb oznámení došlo k chybě.",
"Error saving notification preferences": "Chyba při ukládání předvoleb oznámení",
"Messages containing keywords": "Zprávy obsahující klíčová slova",
"This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Díky tomuto mohou místnosti zůstat soukromé a zároveň je mohou lidé v prostoru najít a připojit se k nim. Všechny nové místnosti v prostoru budou mít tuto možnost k dispozici.",
"To help space members find and join a private room, go to that room's Security & Privacy settings.": "Chcete-li členům prostoru pomoci najít soukromou místnost a připojit se k ní, přejděte do nastavení Zabezpečení a soukromí dané místnosti.",
"Error downloading audio": "Chyba při stahování audia",
"Unknown failure: %(reason)s)": "Neznámá chyba: %(reason)s",
"No answer": "Žádná odpověď",
"An unknown error occurred": "Došlo k neznámé chybě",
"Their device couldn't start the camera or microphone": "Jejich zařízení nemohlo spustit kameru nebo mikrofon",
"Connection failed": "Spojení se nezdařilo",
"Could not connect media": "Nepodařilo se připojit média",
"This call has ended": "Tento hovor byl ukončen",
"Unable to copy a link to the room to the clipboard.": "Nelze zkopírovat odkaz na místnost do schránky.",
"Unable to copy room link": "Nelze zkopírovat odkaz na místnost",
"This call has failed": "Toto volání se nezdařilo",
"Anyone can find and join.": "Kdokoliv může najít a připojit se.",
"Room visibility": "Viditelnost místnosti",
"Visible to space members": "Viditelné pro členy prostoru",
"Public room": "Veřejná místnost",
"Private room (invite only)": "Soukromá místnost (pouze pro pozvané)",
"Create a room": "Vytvořit místnost",
"Only people invited will be able to find and join this room.": "Tuto místnost budou moci najít a připojit se k ní pouze pozvaní lidé.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Tuto místnost bude moci najít a připojit se k ní kdokoli, nejen členové <SpaceName/>.",
"You can change this at any time from room settings.": "Tuto hodnotu můžete kdykoli změnit v nastavení místnosti.",
"Everyone in <SpaceName/> will be able to find and join this room.": "Všichni v <SpaceName/> budou moci tuto místnost najít a připojit se k ní.",
"Image": "Obrázek",
"Sticker": "Nálepka",
"Downloading": "Stahování",
"The call is in an unknown state!": "Hovor je v neznámém stavu!",
"Call back": "Zavolat zpět",
"You missed this call": "Zmeškali jste tento hovor",
"The voice message failed to upload.": "Hlasovou zprávu se nepodařilo nahrát.",
"Copy Room Link": "Kopírovat odkaz na místnost",
"Show %(count)s other previews|one": "Zobrazit %(count)s další náhled",
"Show %(count)s other previews|other": "Zobrazit %(count)s dalších náhledů",
"Access": "Přístup",
"People with supported clients will be able to join the room without having a registered account.": "Lidé s podporovanými klienty se budou moci do místnosti připojit, aniž by měli registrovaný účet.",
"Decide who can join %(roomName)s.": "Rozhodněte, kdo se může připojit k %(roomName)s.",
"Space members": "Členové prostoru",
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Každý, kdo se nachází v prostoru %(spaceName)s, ho může najít a připojit se k němu. Můžete vybrat i jiné prostory.",
"Anyone in a space can find and join. You can select multiple spaces.": "Každý, kdo se nachází v prostoru, ho může najít a připojit se k němu. Můžete vybrat více prostorů.",
"Spaces with access": "Prostory s přístupem",
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Každý, kdo se nachází v prostoru, ho může najít a připojit se k němu. <a>Zde upravte, ke kterým prostorům lze přistupovat.</a>",
"Currently, %(count)s spaces have access|other": "V současné době má %(count)s prostorů přístup k",
"Upgrade required": "Vyžadována aktualizace",
"Mentions & keywords": "Zmínky a klíčová slova",
"Message bubbles": "Bubliny zpráv",
"IRC": "IRC",
"New keyword": "Nové klíčové slovo",
"Keyword": "Klíčové slovo",
"New layout switcher (with message bubbles)": "Nový přepínač rozložení (s bublinami zpráv)",
"Help space members find private rooms": "Pomoci členům prostorů najít soukromé místnosti",
"Help people in spaces to find and join private rooms": "Pomoci lidem v prostorech najít soukromé místnosti a připojit se k nim",
"New in the Spaces beta": "Nové v betaverzi Spaces",
"User %(userId)s is already invited to the room": "Uživatel %(userId)s je již pozván do místnosti",
"Transfer Failed": "Přepojení se nezdařilo",
"Unable to transfer call": "Nelze přepojit hovor",
"They didn't pick up": "Nezvedli to",
"Call again": "Volat znova",
"They declined this call": "Odmítli tento hovor",
"You declined this call": "Odmítli jste tento hovor"
}

View file

@ -11,5 +11,6 @@
"Sign In": "Mewngofnodi",
"Create Account": "Creu Cyfrif",
"Dismiss": "Wfftio",
"Explore rooms": "Archwilio Ystafelloedd"
"Explore rooms": "Archwilio Ystafelloedd",
"Identity server": "Gweinydd Adnabod"
}

View file

@ -15,7 +15,7 @@
"Bans user with given id": "Verbannt den Benutzer mit der angegebenen ID",
"Deops user with given id": "Setzt das Berechtigungslevel beim Benutzer mit der angegebenen ID zurück",
"Invites user with given id to current room": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein",
"Kicks user with given id": "Benutzer mit der angegebenen ID kicken",
"Kicks user with given id": "Benutzer mit der angegebenen ID entfernen",
"Changes your display nickname": "Ändert deinen Anzeigenamen",
"Change Password": "Passwort ändern",
"Searches DuckDuckGo for results": "Verwendet DuckDuckGo zum Suchen",
@ -204,14 +204,14 @@
"Failed to ban user": "Verbannen des Benutzers fehlgeschlagen",
"Failed to change power level": "Ändern der Berechtigungsstufe fehlgeschlagen",
"Failed to join room": "Betreten des Raumes ist fehlgeschlagen",
"Failed to kick": "Rauswurf fehlgeschlagen",
"Failed to kick": "Entfernen fehlgeschlagen",
"Failed to mute user": "Stummschalten des Nutzers fehlgeschlagen",
"Failed to reject invite": "Ablehnen der Einladung ist fehlgeschlagen",
"Failed to set display name": "Anzeigename konnte nicht geändert werden",
"Fill screen": "Fülle Bildschirm",
"Incorrect verification code": "Falscher Verifizierungscode",
"Join Room": "Raum beitreten",
"Kick": "Rausschmeißen",
"Kick": "Entfernen",
"not specified": "nicht angegeben",
"No more results": "Keine weiteren Ergebnisse",
"No results": "Keine Ergebnisse",
@ -539,10 +539,10 @@
"were banned %(count)s times|one": "wurden verbannt",
"was banned %(count)s times|other": "wurde %(count)s-mal verbannt",
"was banned %(count)s times|one": "wurde verbannt",
"were kicked %(count)s times|other": "wurden %(count)s-mal rausgeworfen",
"were kicked %(count)s times|one": "wurden rausgeworfen",
"was kicked %(count)s times|other": "wurde %(count)s-mal rausgeworfen",
"was kicked %(count)s times|one": "wurde rausgeworfen",
"were kicked %(count)s times|other": "wurden %(count)s-mal entfernt",
"were kicked %(count)s times|one": "wurden entfernt",
"was kicked %(count)s times|other": "wurde %(count)s-mal entfernt",
"was kicked %(count)s times|one": "wurde entfernt",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)shaben %(count)s-mal ihren Namen geändert",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)shaben ihren Namen geändert",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)shat %(count)s-mal den Namen geändert",
@ -551,7 +551,7 @@
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shat das Profilbild %(count)s-mal geändert",
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)shat das Profilbild geändert",
"Disinvite this user?": "Einladung für diesen Benutzer zurückziehen?",
"Kick this user?": "Diesen Benutzer rausschmeißen?",
"Kick this user?": "Diesen Benutzer entfernen?",
"Unban this user?": "Verbannung für diesen Benutzer aufheben?",
"Ban this user?": "Diesen Benutzer verbannen?",
"Members only (since the point in time of selecting this option)": "Mitglieder",
@ -979,7 +979,7 @@
"Render simple counters in room header": "Einfache Zähler in Raumkopfzeile anzeigen",
"Enable Emoji suggestions while typing": "Emojivorschläge während Eingabe",
"Show a placeholder for removed messages": "Platzhalter für gelöschte Nachrichten",
"Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Rauswürfe/Banne)",
"Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Entfernen/Banne)",
"Show avatar changes": "Avataränderungen",
"Show display name changes": "Änderungen von Anzeigenamen",
"Send typing notifications": "Tippbenachrichtigungen senden",
@ -1211,7 +1211,7 @@
"Send messages": "Nachrichten senden",
"Invite users": "Benutzer einladen",
"Change settings": "Einstellungen ändern",
"Kick users": "Benutzer kicken",
"Kick users": "Benutzer entfernen",
"Ban users": "Benutzer verbannen",
"Remove messages": "Nachrichten löschen",
"Notify everyone": "Jeden benachrichtigen",
@ -1644,7 +1644,7 @@
"Failed to set topic": "Das Festlegen des Themas ist fehlgeschlagen",
"Command failed": "Befehl fehlgeschlagen",
"Could not find user in room": "Benutzer konnte nicht im Raum gefunden werden",
"Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
"Click the button below to confirm adding this email address.": "Klicke unten auf den Knopf, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
"Confirm adding phone number": "Hinzugefügte Telefonnummer bestätigen",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschlussregel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s erneuert eine Ausschlussregel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
@ -3371,8 +3371,8 @@
"See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden",
"Teammates might not be able to view or join any private rooms you make.": "Mitglieder werden private Räume möglicherweise weder sehen noch betreten können.",
"Error - Mixed content": "Fehler - Uneinheitlicher Inhalt",
"Kick, ban, or invite people to your active room, and make you leave": "Den aktiven Raum verlassen, Leute einladen, kicken oder bannen",
"Kick, ban, or invite people to this room, and make you leave": "Diesen Raum verlassen, Leute einladen, kicken oder bannen",
"Kick, ban, or invite people to your active room, and make you leave": "Den aktiven Raum verlassen, Leute einladen, entfernen oder bannen",
"Kick, ban, or invite people to this room, and make you leave": "Diesen Raum verlassen, Leute einladen, entfernen oder bannen",
"View source": "Rohdaten anzeigen",
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "Die Person verbreitet Falschinformation.\nDies wird an die Raummoderation gemeldet.",
"[number]": "[Nummer]",
@ -3427,8 +3427,8 @@
"Show all rooms in Home": "Alle Räume auf der Startseite zeigen",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Inhalte an Mods melden. In Räumen, die Moderation unterstützen, kannst du so unerwünschte Inhalte direkt der Raummoderation melden",
"%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s hat die <a>angehefteten Nachrichten</a> geändert.",
"%(senderName)s kicked %(targetName)s": "%(senderName)s hat %(targetName)s gekickt",
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s gekickt: %(reason)s",
"%(senderName)s kicked %(targetName)s": "%(senderName)s hat %(targetName)s entfernt",
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s entfernt: %(reason)s",
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen",
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen: %(reason)s",
"%(senderName)s unbanned %(targetName)s": "%(senderName)s hat %(targetName)s entbannt",
@ -3442,7 +3442,7 @@
"%(senderName)s removed their profile picture": "%(senderName)s hat das Profilbild entfernt",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s hat den alten Nicknamen %(oldDisplayName)s entfernt",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s hat den Nicknamen zu %(displayName)s geändert",
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu%(displayName)s geändert",
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu %(displayName)s geändert",
"%(senderName)s banned %(targetName)s": "%(senderName)s hat %(targetName)s gebannt",
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s gebannt: %(reason)s",
"%(targetName)s accepted an invitation": "%(targetName)s hat die Einladung akzeptiert",
@ -3452,5 +3452,63 @@
"Message search initialisation failed, check <a>your settings</a> for more information": "Initialisierung der Nachrichtensuche fehlgeschlagen. Öffne <a>die Einstellungen</a> für mehr Information.",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden.",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden. Diese können jedoch die verschlüsselten Nachrichten nicht lesen.",
"This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann."
"This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann.",
"Unnamed audio": "Unbenannte Audiodatei",
"Show %(count)s other previews|one": "%(count)s andere Vorschau zeigen",
"Show %(count)s other previews|other": "%(count)s andere Vorschauen zeigen",
"Images, GIFs and videos": "Mediendateien",
"To view all keyboard shortcuts, click here.": "Alle Tastenkombinationen anzeigen",
"Keyboard shortcuts": "Tastenkombinationen",
"User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen",
"Unable to copy a link to the room to the clipboard.": "Der Link zum Raum konnte nicht kopiert werden.",
"Unable to copy room link": "Raumlink konnte nicht kopiert werden",
"Integration manager": "Integrationsverwaltung",
"User Directory": "Benutzerverzeichnis",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Dein %(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.",
"Copy Link": "Link kopieren",
"Transfer Failed": "Übertragen fehlgeschlagen",
"Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Wenn du dieses Widget verwendest, können Daten <helpIcon /> zu %(widgetDomain)s und deinem Integrationsserver übertragen werden.",
"Identity server is": "Der Identitätsserver ist",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsverwalter erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsverwalter, um Bots, Widgets und Stickerpakete zu verwalten.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsverwalter <b>(%(serverName)s)</b>, um Bots, Widgets und Stickerpakete zu verwalten.",
"Identity server": "Identitätsserver",
"Identity server (%(server)s)": "Identitätsserver (%(server)s)",
"Could not connect to identity server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden",
"Not a valid identity server (status code %(code)s)": "Ungültiger Identitätsserver (Fehlercode %(code)s)",
"Identity server URL must be HTTPS": "Der Identitätsserver muss über HTTPS erreichbar sein",
"Error processing audio message": "Fehler beim Verarbeiten der Audionachricht",
"Copy Room Link": "Raumlink kopieren",
"Code blocks": "Codeblöcke",
"There was an error loading your notification settings.": "Fehler beim Laden der Benachrichtigungseinstellungen.",
"Mentions & keywords": "Erwähnungen und Schlüsselwörter",
"Global": "Global",
"New keyword": "Neues Schlüsselwort",
"Keyword": "Schlüsselwort",
"Enable email notifications for %(email)s": "E-Mail-Benachrichtigungen für %(email)s aktivieren",
"Enable for this account": "Für dieses Konto aktivieren",
"An error occurred whilst saving your notification preferences.": "Beim Speichern der Benachrichtigungseinstellungen ist ein Fehler aufgetreten.",
"Error saving notification preferences": "Fehler beim Speichern der Benachrichtigungseinstellungen",
"Messages containing keywords": "Nachrichten mit Schlüsselwörtern",
"Show notification badges for People in Spaces": "Benachrichtigungssymbol für Personen in Spaces zeigen",
"Use Ctrl + F to search timeline": "Nutze STRG + F, um den Verlauf zu durchsuchen",
"Downloading": "Herunterladen",
"The call is in an unknown state!": "Dieser Anruf ist in einem unbekannten Zustand!",
"Call back": "Zurückrufen",
"You missed this call": "Du hast einen Anruf verpasst",
"This call has failed": "Anruf fehlgeschlagen",
"Unknown failure: %(reason)s)": "Unbekannter Fehler: %(reason)s",
"Connection failed": "Verbindung fehlgeschlagen",
"This call has ended": "Anruf beendet",
"Connected": "Verbunden",
"IRC": "IRC",
"Silence call": "Anruf stummschalten",
"Error downloading audio": "Fehler beim Herunterladen der Audiodatei",
"Image": "Bild",
"Sticker": "Sticker",
"An unknown error occurred": "Ein unbekannter Fehler ist aufgetreten",
"Message bubbles": "Nachrichtenblasen",
"New layout switcher (with message bubbles)": "Layout ändern erlauben (mit Nachrichtenblasen)",
"New in the Spaces beta": "Neues in der Spaces Beta"
}

View file

@ -925,5 +925,6 @@
"Done": "Τέλος",
"Not Trusted": "Μη Έμπιστο",
"You're already in a call with this person.": "Είστε ήδη σε κλήση με αυτόν τον χρήστη.",
"Already in call": "Ήδη σε κλήση"
"Already in call": "Ήδη σε κλήση",
"Identity server is": "Ο διακομιστής ταυτοποίησης είναι"
}

View file

@ -35,11 +35,8 @@
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Call Failed": "Call Failed",
"Call Declined": "Call Declined",
"The other party declined the call.": "The other party declined the call.",
"User Busy": "User Busy",
"The user you called is busy.": "The user you called is busy.",
"The remote side failed to pick up": "The remote side failed to pick up",
"The call could not be established": "The call could not be established",
"Answered Elsewhere": "Answered Elsewhere",
"The call was answered on another device.": "The call was answered on another device.",
@ -55,7 +52,6 @@
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
"No other application is using the webcam": "No other application is using the webcam",
"Unable to capture screen": "Unable to capture screen",
"VoIP is unsupported": "VoIP is unsupported",
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
"Too Many Calls": "Too Many Calls",
@ -491,6 +487,11 @@
"Converts the room to a DM": "Converts the room to a DM",
"Converts the DM to a room": "Converts the DM to a room",
"Displays action": "Displays action",
"Someone": "Someone",
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s placed a video call. (not supported by this browser)",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
"%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
@ -540,7 +541,6 @@
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.",
"Someone": "Someone",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.",
@ -807,12 +807,7 @@
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
"Show all rooms in Home": "Show all rooms in Home",
"Show people in spaces": "Show people in spaces",
"If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
"Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"Message Pinning": "Message Pinning",
@ -881,6 +876,8 @@
"Manually verify all remote sessions": "Manually verify all remote sessions",
"IRC display name width": "IRC display name width",
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
"Show all rooms in Home": "Show all rooms in Home",
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading logs": "Uploading logs",
@ -913,6 +910,10 @@
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
"%(peerName)s held the call": "%(peerName)s held the call",
"Connecting": "Connecting",
"You are presenting": "You are presenting",
"%(sharerName)s is presenting": "%(sharerName)s is presenting",
"Your camera is turned off": "Your camera is turned off",
"Your camera is still enabled": "Your camera is still enabled",
"Video Call": "Video Call",
"Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
@ -1007,6 +1008,12 @@
"Name": "Name",
"Description": "Description",
"Please enter a name for the space": "Please enter a name for the space",
"Spaces are a new feature.": "Spaces are a new feature.",
"Spaces feedback": "Spaces feedback",
"Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.",
"Give feedback.": "Give feedback.",
"e.g. my-space": "e.g. my-space",
"Address": "Address",
"Create a space": "Create a space",
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.",
"Public": "Public",
@ -1019,12 +1026,12 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
"e.g. my-space": "e.g. my-space",
"Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
"All rooms": "All rooms",
"Home": "Home",
"Show all rooms": "Show all rooms",
"All rooms": "All rooms",
"Options": "Options",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
"Click to copy": "Click to copy",
@ -1054,16 +1061,9 @@
"Preview Space": "Preview Space",
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
"Recommended for public spaces.": "Recommended for public spaces.",
"Settings": "Settings",
"Leave space": "Leave space",
"Create new room": "Create new room",
"Add existing room": "Add existing room",
"Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
"Space options": "Space options",
"Expand": "Expand",
"Collapse": "Collapse",
"Space options": "Space options",
"Remove": "Remove",
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
@ -1572,6 +1572,8 @@
"Unnamed room": "Unnamed room",
"World readable": "World readable",
"Guests can join": "Guests can join",
"Screen sharing is here!": "Screen sharing is here!",
"You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@ -1585,8 +1587,11 @@
"Start chat": "Start chat",
"Rooms": "Rooms",
"Add room": "Add room",
"Create new room": "Create new room",
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
"Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
"Explore rooms": "Explore rooms",
"Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms",
"Low priority": "Low priority",
@ -1664,6 +1669,7 @@
"Low Priority": "Low Priority",
"Invite People": "Invite People",
"Copy Room Link": "Copy Room Link",
"Settings": "Settings",
"Leave Room": "Leave Room",
"Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@ -1757,13 +1763,13 @@
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection",
"Yours, or the other users session": "Yours, or the other users session",
"Members": "Members",
"Nothing pinned, yet": "Nothing pinned, yet",
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages",
"Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
"Options": "Options",
"Set my room layout for everyone": "Set my room layout for everyone",
"Widgets": "Widgets",
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
@ -1859,16 +1865,19 @@
"Verification cancelled": "Verification cancelled",
"Compare emoji": "Compare emoji",
"Connected": "Connected",
"You declined this call": "You declined this call",
"They declined this call": "They declined this call",
"Call back": "Call back",
"Call again": "Call again",
"This call has ended": "This call has ended",
"They didn't pick up": "They didn't pick up",
"Could not connect media": "Could not connect media",
"Connection failed": "Connection failed",
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
"An unknown error occurred": "An unknown error occurred",
"No answer": "No answer",
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
"This call has failed": "This call has failed",
"You missed this call": "You missed this call",
"Call back": "Call back",
"The call is in an unknown state!": "The call is in an unknown state!",
"Sunday": "Sunday",
"Monday": "Monday",
@ -1879,7 +1888,7 @@
"Saturday": "Saturday",
"Today": "Today",
"Yesterday": "Yesterday",
"Downloading": "Downloading",
"Decrypting": "Decrypting",
"Download": "Download",
"View Source": "View Source",
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
@ -1894,9 +1903,9 @@
"Retry": "Retry",
"Reply": "Reply",
"Message Actions": "Message Actions",
"Download %(text)s": "Download %(text)s",
"Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s",
"Download %(text)s": "Download %(text)s",
"Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image",
"Show image": "Show image",
@ -1995,9 +2004,9 @@
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
"This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages",
"Share your screen": "Share your screen",
"Screens": "Screens",
"Windows": "Windows",
"Share entire screen": "Share entire screen",
"Application window": "Application window",
"Share content": "Share content",
"Join": "Join",
"No results": "No results",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
@ -2105,17 +2114,20 @@
"Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms",
"Add existing space": "Add existing space",
"Want to add a new space instead?": "Want to add a new space instead?",
"Create a new space": "Create a new space",
"Search for spaces": "Search for spaces",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Filter your rooms and spaces": "Filter your rooms and spaces",
"Feeling experimental?": "Feeling experimental?",
"You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
"Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room",
"Search for rooms": "Search for rooms",
"Adding spaces has moved.": "Adding spaces has moved.",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
@ -2129,15 +2141,8 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway",
"Close dialog": "Close dialog",
"Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
"Feedback": "Feedback",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent",
@ -2192,6 +2197,7 @@
"Everyone in <SpaceName/> will be able to find and join this room.": "Everyone in <SpaceName/> will be able to find and join this room.",
"You can change this at any time from room settings.": "You can change this at any time from room settings.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Anyone will be able to find and join this room, not just members of <SpaceName/>.",
"Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.",
"Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
"You cant disable this later. Bridges & most bots wont work yet.": "You cant disable this later. Bridges & most bots wont work yet.",
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",
@ -2202,13 +2208,22 @@
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
"Topic (optional)": "Topic (optional)",
"Room visibility": "Room visibility",
"Private room (invite only)": "Private room (invite only)",
"Public room": "Public room",
"Visible to space members": "Visible to space members",
"Topic (optional)": "Topic (optional)",
"Room visibility": "Room visibility",
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
"Create Room": "Create Room",
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
"Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
"Add a space to a space you manage.": "Add a space to a space you manage.",
"Space visibility": "Space visibility",
"Private space (invite only)": "Private space (invite only)",
"Public space": "Public space",
"Want to add an existing space instead?": "Want to add an existing space instead?",
"Adding...": "Adding...",
"Sign out": "Sign out",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this",
"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.": "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.",
@ -2274,8 +2289,10 @@
"Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback",
"Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"Send feedback": "Send feedback",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
@ -2283,6 +2300,10 @@
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort",
@ -2354,6 +2375,16 @@
"Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s",
"Leave all rooms and spaces": "Leave all rooms and spaces",
"Don't leave any": "Don't leave any",
"Leave specific rooms and spaces": "Leave specific rooms and spaces",
"Search %(spaceName)s": "Search %(spaceName)s",
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
"You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
"You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
"Leave %(spaceName)s": "Leave %(spaceName)s",
"Are you sure you want to leave <spaceName/>?": "Are you sure you want to leave <spaceName/>?",
"Leave space": "Leave space",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Start using Key Backup": "Start using Key Backup",
"I don't want my encrypted messages": "I don't want my encrypted messages",
@ -2562,6 +2593,8 @@
"Source URL": "Source URL",
"Collapse reply thread": "Collapse reply thread",
"Report": "Report",
"Add space": "Add space",
"Manage & explore rooms": "Manage & explore rooms",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
@ -2794,8 +2827,6 @@
"Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Spaces are a beta feature.": "Spaces are a beta feature.",
"Public space": "Public space",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
@ -2810,6 +2841,7 @@
"Creating rooms...": "Creating rooms...",
"What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
"Search for rooms or spaces": "Search for rooms or spaces",
"Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room",
@ -2821,7 +2853,7 @@
"Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
"We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",

View file

@ -3326,5 +3326,16 @@
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.",
"No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»",
"The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.",
"User Busy": "Uzanto estas okupata"
"User Busy": "Uzanto estas okupata",
"Integration manager": "Kunigilo",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Via %(brand)so ne permesas al vi uzi kunigilon por tio. Bonvolu kontakti administranton.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Uzo de tiu ĉi fenestraĵo eble havigos datumojn <helpIcon /> kun %(widgetDomain)s kaj via kunigilo.",
"Identity server is": "Identiga servilo estas",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Kunigiloj ricevas agordajn datumojn, kaj povas modifi fenestraĵojn, sendi invitojn al ĉambroj, kaj vianome agordi povnivelojn.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Uzu kunigilon por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Uzu kunigilon <b>(%(serverName)s)</b> por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.",
"Identity server": "Identiga servilo",
"Identity server (%(server)s)": "Identiga servilo (%(server)s)",
"Could not connect to identity server": "Ne povis konektiĝi al identiga servilo",
"Not a valid identity server (status code %(code)s)": "Nevalida identiga servilo (statkodo %(code)s)"
}

View file

@ -1706,7 +1706,7 @@
"Encrypted by an unverified session": "Cifrado por una sesión no verificada",
"Unencrypted": "Sin cifrar",
"Encrypted by a deleted session": "Cifrado por una sesión eliminada",
"Invite only": "Sólamente por invitación",
"Invite only": "Solo por invitación",
"Scroll to most recent messages": "Ir a los mensajes más recientes",
"Close preview": "Cerrar vista previa",
"No recent messages by %(user)s found": "No se han encontrado mensajes recientes de %(user)s",
@ -3254,7 +3254,7 @@
"Enter your Security Phrase a second time to confirm it.": "Escribe tu frase de seguridad de nuevo para confirmarla.",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elige salas o conversaciones para añadirlas. Este espacio es solo para ti, no informaremos a nadie. Puedes añadir más más tarde.",
"What do you want to organise?": "¿Qué quieres organizar?",
"Filter all spaces": "Filtrar todos los espacios",
"Filter all spaces": "Filtrar espacios",
"%(count)s results in all spaces|one": "%(count)s resultado en todos los espacios",
"%(count)s results in all spaces|other": "%(count)s resultados en todos los espacios",
"You have no ignored users.": "No has ignorado a nadie.",
@ -3420,5 +3420,62 @@
"%(targetName)s accepted an invitation": "%(targetName)s ha aceptado una invitación",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s ha aceptado la invitación a %(displayName)s",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Hemos enviado el resto, pero no hemos podido invitar las siguientes personas a la sala <RoomName/>",
"Some invites couldn't be sent": "No se han podido enviar algunas invitaciones"
"Some invites couldn't be sent": "No se han podido enviar algunas invitaciones",
"Integration manager": "Gestor de integración",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s no utilizar un \"gestor de integración\" para hacer esto. Por favor, contacta con un administrador.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Usar este widget puede resultar en que se compartan datos <helpIcon /> con %(widgetDomain)s y su administrador de integración.",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Los administradores de integración reciben datos de configuración, y pueden modificar widgets, enviar invitaciones de sala, y establecer niveles de poder en tu nombre.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Utiliza un administrador de integración para gestionar los bots, los widgets y los paquetes de pegatinas.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Usar un gestor de integraciones <b>(%(serverName)s)</b> para manejar los bots, widgets y paquetes de pegatinas.",
"Identity server": "Servidor de identidad",
"Identity server (%(server)s)": "Servidor de identidad %(server)s",
"Could not connect to identity server": "No se ha podido conectar al servidor de identidad",
"Not a valid identity server (status code %(code)s)": "No es un servidor de identidad válido (código de estado %(code)s)",
"Identity server URL must be HTTPS": "La URL del servidor de identidad debe ser tipo HTTPS",
"Unable to copy a link to the room to the clipboard.": "No se ha podido copiar el enlace a la sala.",
"Unable to copy room link": "No se ha podido copiar el enlace a la sala",
"Unnamed audio": "Audio sin título",
"User Directory": "Lista de usuarios",
"Error processing audio message": "Error al procesar el mensaje de audio",
"Copy Link": "Copiar enlace",
"Show %(count)s other previews|one": "Ver otras %(count)s vistas previas",
"Show %(count)s other previews|other": "Ver %(count)s otra vista previa",
"Images, GIFs and videos": "Imágenes, GIFs y vídeos",
"Code blocks": "Bloques de código",
"To view all keyboard shortcuts, click here.": "Para ver todos los atajos de teclado, haz clic aquí.",
"Keyboard shortcuts": "Atajos de teclado",
"Identity server is": "El servidor de identidad es",
"There was an error loading your notification settings.": "Ha ocurrido un error al cargar tus ajustes de notificaciones",
"Mentions & keywords": "Menciones y palabras clave",
"Global": "Global",
"New keyword": "Nueva palabra clave",
"Keyword": "Palabra clave",
"Enable email notifications for %(email)s": "Activar notificaciones por correo electrónico para %(email)s",
"Enable for this account": "Activar para esta cuenta",
"An error occurred whilst saving your notification preferences.": "Ha ocurrido un error al guardar las tus preferencias de notificaciones.",
"Error saving notification preferences": "Error al guardar las preferencias de notificaciones",
"Messages containing keywords": "Mensajes que contengan",
"Use Command + F to search timeline": "Usa Control + F para buscar",
"Transfer Failed": "La transferencia ha fallado",
"Unable to transfer call": "No se ha podido transferir la llamada",
"This call has ended": "La llamada ha terminado",
"Could not connect media": "No se ha podido conectar con los dispositivos multimedia",
"Their device couldn't start the camera or microphone": "El dispositivo de la otra persona no ha podido iniciar la cámara o micrófono",
"Error downloading audio": "Error al descargar el audio",
"Image": "Imagen",
"Sticker": "Pegatina",
"Downloading": "Descargando",
"The call is in an unknown state!": "La llamada está en un estado desconocido",
"Call back": "Devolver",
"You missed this call": "No has cogido esta llamada",
"This call has failed": "Esta llamada ha fallado",
"Unknown failure: %(reason)s)": "Fallo desconocido: %(reason)s)",
"No answer": "Sin respuesta",
"An unknown error occurred": "Ha ocurrido un error desconocido",
"Connection failed": "Ha fallado la conexión",
"Connected": "Conectado",
"Copy Room Link": "Copiar enlace a la sala",
"Displaying time": "Mostrando la hora",
"IRC": "IRC",
"Use Ctrl + F to search timeline": "Usa Control + F para buscar dentro de la conversación"
}

View file

@ -1489,7 +1489,7 @@
"Update any local room aliases to point to the new room": "uuendame kõik jututoa aliased nii, et nad viitaks uuele jututoale",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "ei võimalda kasutajatel enam vanas jututoas suhelda ning avaldame seal teate, mis soovitab kõigil kolida uude jututuppa",
"Put a link back to the old room at the start of the new room so people can see old messages": "selleks et saaks vanu sõnumeid lugeda, paneme uue jututoa algusesse viite vanale jututoale",
"Automatically invite users": "Kutsu kasutajad automaatselt",
"Automatically invite users": "Kutsu automaatselt kasutajaid",
"Upgrade private room": "Uuenda omavaheline jututuba",
"Upgrade public room": "Uuenda avalik jututuba",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Jututoa uuendamine on keerukas toiming ning tavaliselt soovitatakse seda teha vaid siis, kui jututuba on vigade tõttu halvasti kasutatav, sealt on puudu vajalikke funktsionaalsusi või seal ilmneb turvavigu.",
@ -2829,7 +2829,7 @@
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Sõnumid siin jututoas on läbivalt krüptitud. Klõpsides tunnuspilti saad kontrollida kasutaja %(displayName)s profiili.",
"%(creator)s created this DM.": "%(creator)s alustas seda otsesuhtlust.",
"This is the start of <roomName/>.": "See on <roomName/> jututoa algus.",
"Add a photo, so people can easily spot your room.": "Selle, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.",
"Add a photo, so people can easily spot your room.": "Selleks, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.",
"%(displayName)s created this room.": "%(displayName)s lõi selle jututoa.",
"You created this room.": "Sa lõid selle jututoa.",
"<a>Add a topic</a> to help people know what it is about.": "Selleks, et teised teaks millega on tegemist, palun <a>lisa teema</a>.",
@ -3450,5 +3450,76 @@
"This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "See kasutaja spämmib jututuba reklaamidega, reklaamlinkidega või propagandaga.\nJututoa moderaatorid saavad selle kohta teate.",
"This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "Selle kasutaja tegevus on äärmiselt ebasobilik, milleks võib olla teiste jututoas osalejate solvamine, peresõbralikku jututuppa täiskasvanutele mõeldud sisu lisamine või muul viisil jututoa reeglite rikkumine.\nJututoa moderaatorid saavad selle kohta teate.",
"Please provide an address": "Palun sisesta aadress",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta"
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta",
"Unnamed audio": "Nimetu helifail",
"Code blocks": "Lähtekoodi lõigud",
"Images, GIFs and videos": "Pildid, gif'id ja videod",
"Show %(count)s other previews|other": "Näita %(count)s muud eelvaadet",
"Show %(count)s other previews|one": "Näita veel %(count)s eelvaadet",
"Error processing audio message": "Viga häälsõnumi töötlemisel",
"Integration manager": "Lõiminguhaldur",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Sinu %(brand)s ei võimalda selle tegevuse jaoks kasutada lõiminguhaldurit. Palun küsi lisateavet serveri haldajalt.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Selle vidina kasutamisel võidakse jagada andmeid <helpIcon /> %(widgetDomain)s saitidega ning sinu lõiminguhalduriga.",
"Identity server is": "Isikutuvastusserver on",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Lõiminguhalduritel on laiad volitused - nad võivad sinu nimel lugeda seadistusi, kohandada vidinaid, saata jututubade kutseid ning määrata õigusi.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide seadistamiseks kasuta lõiminguhaldurit.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide jaoks kasuta lõiminguhaldurit <b>(%(serverName)s)</b>.",
"Identity server": "Isikutuvastusserver",
"Identity server (%(server)s)": "Isikutuvastusserver %(server)s",
"Could not connect to identity server": "Ei saanud ühendust isikutuvastusserveriga",
"Not a valid identity server (status code %(code)s)": "See ei ole sobilik isikutuvastusserver (staatuskood %(code)s)",
"Identity server URL must be HTTPS": "Isikutuvastusserveri URL peab kasutama HTTPS-protokolli",
"User %(userId)s is already invited to the room": "Kasutaja %(userId)s sai juba kutse sellesse jututuppa",
"Use Command + F to search timeline": "Ajajoonelt otsimiseks kasuta Command+F klahve",
"Use Ctrl + F to search timeline": "Ajajoonelt otsimiseks kasuta Ctrl+F klahve",
"Keyboard shortcuts": "Kiirklahvid",
"To view all keyboard shortcuts, click here.": "Vaata siit kõiki kiirklahve.",
"Copy Link": "Kopeeri link",
"User Directory": "Kasutajate kataloog",
"Unable to copy room link": "Jututoa lingi kopeerimine ei õnnestu",
"Unable to copy a link to the room to the clipboard.": "Jututoa lingi kopeerimine lõikelauale ei õnnestunud.",
"Messages containing keywords": "Sõnumid, mis sisaldavad märksõnu",
"Error saving notification preferences": "Viga teavistuste eelistuste salvestamisel",
"An error occurred whilst saving your notification preferences.": "Sinu teavituste eelistuste salvestamisel tekkis viga.",
"Enable for this account": "Võta sellel kontol kasutusele",
"Enable email notifications for %(email)s": "Saada teavitusi %(email)s e-posti aadressile",
"Keyword": "Märksõnad",
"Mentions & keywords": "Mainimised ja märksõnad",
"New keyword": "Uus märksõna",
"Global": "Üldised",
"There was an error loading your notification settings.": "Sinu teavituste seadistuste laadimisel tekkis viga.",
"Transfer Failed": "Edasisuunamine ei õnnestunud",
"Unable to transfer call": "Kõne edasisuunamine ei õnnestunud",
"Downloading": "Laadin alla",
"The call is in an unknown state!": "Selle kõne oleks on teadmata!",
"Call back": "Helista tagasi",
"This call has failed": "Kõne ühendamine ei õnnestunud",
"You missed this call": "Sa ei võtnud kõnet vastu",
"Unknown failure: %(reason)s)": "Tundmatu viga: %(reason)s",
"No answer": "Keegi ei vasta kõnele",
"You're removing all spaces. Access will default to invite only": "Sa oled eemaldamas kõiki kogukonnakeskuseid. Edaspidine ligipääs eeldab kutse olemasolu",
"Select spaces": "Vali kogukonnakeskused",
"Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Vali missugustel kogukonnakeskustel on sellele jututoale ligipääs. Kui kogukonnakeskus on valitud, siis selle liikmed saavad <RoomName/> jututuba leida ja temaga liituda.",
"Search spaces": "Otsi kogukonnakeskusi",
"Spaces you know that contain this room": "Sulle teadaolevad kogukonnakeskused, millesse kuulub see jututuba",
"Other spaces or rooms you might not know": "Sellised muud jututoad ja kogukonnakeskused, mida sa ei pruugi teada",
"Automatically invite members from this room to the new one": "Kutsu jututoa senised liikmed automaatselt uude jututuppa",
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Palun arvesta, et uuendusega tehakse jututoast uus variant</b>. Kõik senised sõnumid jäävad sellesse jututuppa arhiveeritud olekus.",
"Only people invited will be able to find and join this room.": "See jututuba on leitav vaid kutse olemasolul ning liitumine on võimalik vaid kutse alusel.",
"Create a room": "Loo jututuba",
"Private room (invite only)": "Privaatne jututuba (kutse alusel)",
"Public room": "Avalik jututuba",
"Visible to space members": "Nähtav kogukonnakeskuse liikmetele",
"Room visibility": "Jututoa nähtavus",
"Spaces with access": "Ligipääsuga kogukonnakeskused",
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Kõik %(spaceName)s kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida muid kogukonnakeskuseid.",
"Anyone in a space can find and join. You can select multiple spaces.": "Kõik kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida ka mitu kogukonnakeskust.",
"Space members": "Kogukonnakeskuse liikmed",
"Decide who can join %(roomName)s.": "Vali, kes saavad liituda %(roomName)s jututoaga.",
"People with supported clients will be able to join the room without having a registered account.": "Kõik kes kasutavad sobilikke klientrakendusi, saavad jututoaga liituda ilma kasutajakonto registreerimiseta.",
"Access": "Ligipääs",
"The voice message failed to upload.": "Häälsõnumi üleslaadimine ei õnnestunud.",
"Everyone in <SpaceName/> will be able to find and join this room.": "Kõik <SpaceName/> kogukonna liikmed saavad seda jututuba leida ning võivad temaga liituda.",
"You can change this at any time from room settings.": "Sa saad seda alati jututoa seadistustest muuta.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Mitte ainult <SpaceName/> kogukonna liikmed, vaid kõik saavad seda jututuba leida ja võivad temaga liituda."
}

View file

@ -2293,5 +2293,17 @@
"Wrong file type": "Okerreko fitxategi-mota",
"Looks good!": "Itxura ona du!",
"Search rooms": "Bilatu gelak",
"User menu": "Erabiltzailea-menua"
"User menu": "Erabiltzailea-menua",
"Integration manager": "Integrazio-kudeatzailea",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Zure %(brand)s aplikazioak ez dizu hau egiteko integrazio kudeatzaile bat erabiltzen uzten. Kontaktatu administratzaileren batekin.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Trepeta hau erabiltzean <helpIcon /> %(widgetDomain)s domeinuarekin eta zure integrazio kudeatzailearekin datuak partekatu daitezke.",
"Identity server is": "Identitate zerbitzaria",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrazio kudeatzaileek konfigurazio datuak jasotzen dituzte, eta trepetak aldatu ditzakete, gelara gonbidapenak bidali, eta botere mailak zure izenean ezarri.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Erabili integrazio kudeatzaile bat botak, trepetak eta eranskailu multzoak kudeatzeko.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Erabili <b>(%(serverName)s)</b> integrazio kudeatzailea botak, trepetak eta eranskailu multzoak kudeatzeko.",
"Identity server": "Identitate zerbitzaria",
"Identity server (%(server)s)": "Identitate-zerbitzaria (%(server)s)",
"Could not connect to identity server": "Ezin izan da identitate-zerbitzarira konektatu",
"Not a valid identity server (status code %(code)s)": "Ez da identitate zerbitzari baliogarria (egoera-mezua %(code)s)",
"Identity server URL must be HTTPS": "Identitate zerbitzariaren URL-a HTTPS motakoa izan behar du"
}

View file

@ -3007,5 +3007,19 @@
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "این کار آنها را به %(communityName)s دعوت نمی‌کند. برای دعوت افراد به %(communityName)s،<a>اینجا</a> کلیک کنید",
"Start a conversation with someone using their name or username (like <userId/>).": "با استفاده از نام یا نام کاربری (مانند <userId/>)، گفتگوی جدیدی را با دیگران شروع کنید.",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "با استفاده از نام، آدرس ایمیل و یا نام کاربری (مانند <userId/>)، یک گفتگوی جدید را شروع کنید.",
"May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود"
"May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود",
"Integration manager": "مدیر یکپارچگی",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s شما اجازهٔ استفاده از یک مدیر یکپارچگی را برای این کار نمی دهد. لطفاً با مدیری تماس بگیرید.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "استفاده از این ابزارک ممکن است داده‌هایی <helpIcon /> را با %(widgetDomain)s و مدیر یکپارچگیتان هم رسانی کند.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "برای مدیریت بات‌ها، ابزارک‌ها و بسته‌های برچسب، از یک مدیر پکپارچه‌سازی استفاده کنید.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "برای مدیریت بات‌ها، ابزارک‌ها و بسته‌های برچسب، از یک مدیر پکپارچه‌سازی <b>(%(serverName)s)</b> استفاده کنید.",
"Identity server": "کارساز هویت",
"Identity server (%(server)s)": "کارساز هویت (%(server)s)",
"Could not connect to identity server": "نتوانست به کارساز هویت وصل شود",
"Not a valid identity server (status code %(code)s)": "کارساز هویت معتبر نیست (کد وضعیت %(code)s)",
"Identity server URL must be HTTPS": "نشانی کارساز هویت باید HTTPS باشد",
"Transfer Failed": "انتقال شکست خورد",
"Unable to transfer call": "ناتوان در انتقال تماس",
"The user you called is busy.": "کاربر موردنظر مشغول است.",
"User Busy": "کاربر مشغول"
}

View file

@ -3003,5 +3003,17 @@
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Salli vertaisyhteydet 1:1-puheluille (jos otat tämän käyttöön, toinen osapuoli saattaa nähdä IP-osoitteesi)",
"Send and receive voice messages": "Lähetä ja vastaanota ääniviestejä",
"Show options to enable 'Do not disturb' mode": "Näytä asetukset Älä häiritse -tilan ottamiseksi käyttöön",
"%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s"
"%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s",
"Integration manager": "Integraatioiden lähde",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-instanssisi ei salli sinun käyttävän integraatioiden lähdettä tämän tekemiseen. Ota yhteys ylläpitäjääsi.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Tämän sovelman käyttäminen saattaa jakaa tietoa <helpIcon /> osoitteille %(widgetDomain)s ja käyttämällesi integraatioiden lähteelle.",
"Identity server is": "Identiteettipalvelin on",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integraatioiden lähteet vastaanottavat asetusdataa ja voivat muokata sovelmia, lähettää kutsuja huoneeseen ja asettaa oikeustasoja puolestasi.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä bottien, sovelmien ja tarrapakettien hallintaan.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä <b>(%(serverName)s)</b> bottien, sovelmien ja tarrapakettien hallintaan.",
"Identity server": "Identiteettipalvelin",
"Identity server (%(server)s)": "Identiteettipalvelin (%(server)s)",
"Could not connect to identity server": "Identiteettipalvelimeen ei saatu yhteyttä",
"Not a valid identity server (status code %(code)s)": "Ei kelvollinen identiteettipalvelin (tilakoodi %(code)s)",
"Identity server URL must be HTTPS": "Identiteettipalvelimen URL-osoitteen täytyy olla HTTPS-alkuinen"
}

View file

@ -427,7 +427,7 @@
"You are no longer ignoring %(userId)s": "Vous nignorez plus %(userId)s",
"Invite to Community": "Inviter dans la communauté",
"Communities": "Communautés",
"Message Pinning": "Épingler un message",
"Message Pinning": "Messages épinglés",
"Mention": "Mentionner",
"Unignore": "Ne plus ignorer",
"Ignore": "Ignorer",
@ -3456,5 +3456,42 @@
"%(targetName)s accepted an invitation": "%(targetName)s a accepté une invitation",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s a accepté linvitation pour %(displayName)s",
"Some invites couldn't be sent": "Certaines invitations nont pas pu être envoyées",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Nous avons envoyé les invitations, mais les personnes ci-dessous nont pas pu être invitées à rejoindre <RoomName/>"
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Nous avons envoyé les invitations, mais les personnes ci-dessous nont pas pu être invitées à rejoindre <RoomName/>",
"Integration manager": "Gestionnaire dintégration",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Votre %(brand)s ne vous autorise pas à utiliser un gestionnaire dintégrations pour faire ça. Contactez un administrateur.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Lutilisation de ce widget pourrait partager des données <helpIcon /> avec %(widgetDomain)s et votre gestionnaire dintégrations.",
"Identity server is": "Le serveur d'identité est",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires dintégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire dintégrations pour gérer les robots, les widgets et les jeux dautocollants.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire dintégrations <b>(%(serverName)s)</b> pour gérer les robots, les widgets et les jeux dautocollants.",
"Identity server": "Serveur didentité",
"Identity server (%(server)s)": "Serveur didentité (%(server)s)",
"Could not connect to identity server": "Impossible de se connecter au serveur didentité",
"Not a valid identity server (status code %(code)s)": "Serveur didentité non valide (code de statut %(code)s)",
"Identity server URL must be HTTPS": "LURL du serveur didentité doit être en HTTPS",
"User Directory": "Répertoire utilisateur",
"Error processing audio message": "Erreur lors du traitement du message audio",
"Copy Link": "Copier le lien",
"Show %(count)s other previews|one": "Afficher %(count)s autre aperçu",
"Show %(count)s other previews|other": "Afficher %(count)s autres aperçus",
"Images, GIFs and videos": "Images, GIF et vidéos",
"Code blocks": "Blocs de code",
"Displaying time": "Affichage de lheure",
"To view all keyboard shortcuts, click here.": "Pour afficher tous les raccourcis clavier, cliquez ici.",
"Keyboard shortcuts": "Raccourcis clavier",
"There was an error loading your notification settings.": "Une erreur est survenue lors du chargement de vos paramètres de notification.",
"Mentions & keywords": "Mentions et mots-clés",
"Global": "Global",
"New keyword": "Nouveau mot-clé",
"Keyword": "Mot-clé",
"Enable email notifications for %(email)s": "Activer les notifications par e-mail pour %(email)s",
"Enable for this account": "Activer pour ce compte",
"An error occurred whilst saving your notification preferences.": "Une erreur est survenue lors de la sauvegarde de vos préférences de notification.",
"Error saving notification preferences": "Erreur lors de la sauvegarde des préférences de notification",
"Messages containing keywords": "Message contenant les mots-clés",
"Use Ctrl + F to search timeline": "Utilisez Ctrl + F pour rechercher dans le fil de discussion",
"Use Command + F to search timeline": "Utilisez Commande + F pour rechercher dans le fil de discussion",
"User %(userId)s is already invited to the room": "Lutilisateur %(userId)s est déjà invité dans le salon",
"Transfer Failed": "Échec du transfert",
"Unable to transfer call": "Impossible de transférer lappel"
}

View file

@ -3398,5 +3398,147 @@
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "Se tes permisos, abre o menú en calquera mensaxe e elixe <b>Fixar</b> para pegalos aquí.",
"Nothing pinned, yet": "Nada fixado, por agora",
"End-to-end encryption isn't enabled": "Non está activado o cifrado de extremo-a-extremo",
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. <a>Activa o cifrado nos axustes.</a>"
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. <a>Activa o cifrado nos axustes.</a>",
"Integration manager": "Xestor de Integracións",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "O teu %(brand)s non permite que uses o Xestor de Integracións, contacta coa administración.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Ao utilizar este widget poderías compartir datos <helpIcon /> con %(widgetDomain)s e o teu Xestor de integracións.",
"Identity server is": "O servidor de identidade é",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Os xestores de integracións reciben datos de configuración, e poden modificar os widgets, enviar convites das salas, e establecer roles no teu nome.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integracións para xestionar bots, widgets e paquetes de adhesivos.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integración <b>(%(serverName)s)</b> para xestionar bots, widgets e paquetes de adhesivos.",
"Identity server": "Servidor de identidade",
"Identity server (%(server)s)": "Servidor de Identidade (%(server)s)",
"Could not connect to identity server": "Non hai conexión co Servidor de Identidade",
"Not a valid identity server (status code %(code)s)": "Servidor de Identidade non válido (código de estado %(code)s)",
"Identity server URL must be HTTPS": "O URL do servidor de identidade debe comezar HTTPS",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "Esta sala está dedicada a contido ilegal ou tóxico ou a moderación non modera os contidos tóxicos ou ilegais.\nEsto vaise denunciar ante a administración de %(homeserver)s. As administradoras NON poderán ler o contido cifrado desta sala.",
"This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "Esta usuaria está facendo spam na sala con anuncios, ligazóns a anuncios ou propaganda.\nEsto vai ser denunciado ante a moderación da sala.",
"This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Esta usuaria está a comportarse dun xeito ilegal, por exemplo ameazando a persoas ou exhibindo violencia.\nEsto vaise denunciar ante a moderación da sala que podería presentar o caso ante as autoridades legais.",
"This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "Esta usuaria ten un comportamento tóxico, por exemplo insultar a outras usuarias o compartir contido adulto nunha sala de contido familiar ou faltando doutro xeito ás regras desta sala.\nVai ser denunciada ante a moderación da sala.",
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "O que escribe esta usuaria non é correcto.\nSerá denunciado á moderación da sala.",
"User Directory": "Directorio de Usuarias",
"Please provide an address": "Proporciona un enderezo",
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)s cambiou ACLs do servidor",
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)s cambiou o ACLs do servidor %(count)s veces",
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)s cambiaron o ACLs do servidor",
"%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)s cambiaron ACLs do servidor %(count)s veces",
"Message search initialisation failed, check <a>your settings</a> for more information": "Fallou a inicialización da busca de mensaxes, comproba <a>os axustes</a> para máis información",
"Error processing audio message": "Erro ao procesar a mensaxe de audio",
"Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Establecer enderezos para este espazo para que as usuarias poidan atopar o espazo no servidor (%(localDomain)s)",
"To publish an address, it needs to be set as a local address first.": "Para publicar un enderezo, primeiro debe establecerse como enderezo local.",
"Published addresses can be used by anyone on any server to join your room.": "Os enderezos publicados poden ser utilizados por calquera en calquera servidor para unirse á túa sala.",
"Published addresses can be used by anyone on any server to join your space.": "Os enderezos publicados podense usar por calquera en calquera servidor para unirse ao teu espazo.",
"This space has no local addresses": "Este espazo non ten enderezos locais",
"Copy Link": "Copiar Ligazón",
"Show %(count)s other previews|one": "Mostrar %(count)s outra vista previa",
"Show %(count)s other previews|other": "Mostrar outras %(count)s vistas previas",
"Space information": "Información do Espazo",
"Images, GIFs and videos": "Imaxes, GIFs e vídeos",
"Code blocks": "Bloques de código",
"Displaying time": "Mostrar hora",
"To view all keyboard shortcuts, click here.": "Para ver os atallos do teclado preme aquí.",
"Keyboard shortcuts": "Atallos de teclado",
"There was an error loading your notification settings.": "Houbo un erro ao cargar os axustes de notificación.",
"Mentions & keywords": "Mencións e palabras chave",
"Global": "Global",
"New keyword": "Nova palabra chave",
"Keyword": "Palabra chave",
"Enable email notifications for %(email)s": "Activar notificacións de email para %(email)s",
"Enable for this account": "Activar para esta conta",
"An error occurred whilst saving your notification preferences.": "Algo fallou ao gardar as túas preferencias de notificación.",
"Error saving notification preferences": "Erro ao gardar os axustes de notificación",
"Messages containing keywords": "Mensaxes coas palabras chave",
"Collapse": "Pechar",
"Expand": "Despregar",
"Recommended for public spaces.": "Recomendado para espazos públicos.",
"Allow people to preview your space before they join.": "Permitir que sexa visible o espazo antes de unirte a el.",
"Preview Space": "Vista previa do Espazo",
"only invited people can view and join": "só poden ver e unirse persoas que foron convidadas",
"anyone with the link can view and join": "calquera coa ligazón pode ver e unirse",
"Decide who can view and join %(spaceName)s.": "Decidir quen pode ver e unirse a %(spaceName)s.",
"Visibility": "Visibilidade",
"This may be useful for public spaces.": "Esto podería ser útil para espazos públicos.",
"Guests can join a space without having an account.": "As convidadas poden unirse ao espazo sen ter unha conta.",
"Enable guest access": "Activar acceso de convidadas",
"Failed to update the history visibility of this space": "Fallou a actualización da visibilidade do historial do espazo",
"Failed to update the guest access of this space": "Fallou a actualización do acceso de convidadas ao espazo",
"Failed to update the visibility of this space": "Fallou a actualización da visibilidade do espazo",
"Address": "Enderezo",
"e.g. my-space": "ex. o-meu-espazo",
"Silence call": "Acalar chamada",
"Sound on": "Son activado",
"Use Ctrl + F to search timeline": "Usar Ctrl + F para buscar na cronoloxía",
"Use Command + F to search timeline": "Usar Command + F para buscar na cronoloxía",
"Show notification badges for People in Spaces": "Mostra insignia de notificación para Persoas en Espazos",
"If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "Se está desactivado tamén poderás engadir as Mensaxes Directas aos Espazos personais. Se activado, verás automáticamente quen é membro do Espazo.",
"Show people in spaces": "Mostrar persoas nos Espazos",
"Show all rooms in Home": "Mostrar tódalas salas no Inicio",
"User %(userId)s is already invited to the room": "A usuaria %(userId)s xa ten un convite para a sala",
"%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s cambiou a <a>mensaxe fixada</a> da sala.",
"%(senderName)s kicked %(targetName)s": "%(senderName)s expulsou a %(targetName)s",
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s expulsou a %(targetName)s: %(reason)s",
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s retirou o convite para %(targetName)s",
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s retirou o convite para %(targetName)s: %(reason)s",
"%(senderName)s unbanned %(targetName)s": "%(senderName)s retiroulle o veto a %(targetName)s",
"%(targetName)s left the room": "%(targetName)s saíu da sala",
"%(targetName)s left the room: %(reason)s": "%(targetName)s saíu da sala: %(reason)s",
"%(targetName)s rejected the invitation": "%(targetName)s rexeitou o convite",
"%(targetName)s joined the room": "%(targetName)s uniuse á sala",
"%(senderName)s made no change": "%(senderName)s non fixo cambios",
"%(senderName)s set a profile picture": "%(senderName)s estableceu a foto de perfil",
"%(senderName)s changed their profile picture": "%(senderName)s cambiou a súa foto de perfil",
"%(senderName)s removed their profile picture": "%(senderName)s eliminou a súa foto de perfil",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s eliminou o seu nome público (%(oldDisplayName)s)",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s estableceu o seu nome público como %(displayName)s",
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s cambiou o seu nome público a %(displayName)s",
"%(senderName)s banned %(targetName)s": "%(senderName)s vetou %(targetName)s",
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s vetou %(targetName)s: %(reason)s",
"%(targetName)s accepted an invitation": "%(targetName)s aceptou o convite",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s aceptou o convite a %(displayName)s",
"Some invites couldn't be sent": "Non se puideron enviar algúns convites",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Convidamos as outras, pero as persoas de aquí embaixo non foron convidadas a <RoomName/>",
"Transfer Failed": "Fallou a transferencia",
"Unable to transfer call": "Non se puido transferir a chamada",
"[number]": "[número]",
"To view %(spaceName)s, you need an invite": "Para ver %(spaceName)s precisas un convite",
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Podes premer en calquera momento nun avatar no panel de filtros para ver só salas e persoas asociadas con esa comunidade.",
"Unable to copy a link to the room to the clipboard.": "Non se copiou a ligazón da sala ao portapapeis.",
"Unable to copy room link": "Non se puido copiar ligazón da sala",
"Unnamed audio": "Audio sen nome",
"Move down": "Ir abaixo",
"Move up": "Ir arriba",
"Report": "Denunciar",
"Collapse reply thread": "Contraer fío de resposta",
"Show preview": "Ver vista previa",
"View source": "Ver fonte",
"Forward": "Reenviar",
"Settings - %(spaceName)s": "Axustes - %(spaceName)s",
"Report the entire room": "Denunciar a toda a sala",
"Spam or propaganda": "Spam ou propaganda",
"Illegal Content": "Contido ilegal",
"Toxic Behaviour": "Comportamento tóxico",
"Disagree": "En desacordo",
"Please pick a nature and describe what makes this message abusive.": "Escolle unha opción e describe a razón pola que esta é unha mensaxe abusiva.",
"Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Outra razón. Por favor, describe o problema.\nInformaremos disto á moderación da sala.",
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Esta sala está dedicada a contido tóxico ou ilegal ou a moderación non é quen de moderar contido ilegal ou tóxico.\nImos informar disto á administración de %(homeserver)s.",
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Modelo de denuncia ante a moderación. Nas salas que teñen moderación, o botón `denuncia`permíteche denunciar un abuso á moderación da sala",
"Copy Room Link": "Copiar Ligazón da sala",
"Downloading": "Descargando",
"The call is in an unknown state!": "Esta chamada ten un estado descoñecido!",
"Call back": "Devolver a chamada",
"You missed this call": "Perdeches esta chamada",
"This call has failed": "A chamada fallou",
"Unknown failure: %(reason)s)": "Fallo descoñecido: %(reason)s",
"No answer": "Sen resposta",
"An unknown error occurred": "Aconteceu un fallo descoñecido",
"Their device couldn't start the camera or microphone": "O seu dispositivo non puido acender a cámara ou micrófono",
"Connection failed": "Fallou a conexión",
"Could not connect media": "Non se puido conectar o multimedia",
"This call has ended": "A chamada rematou",
"Connected": "Conectado",
"Message bubbles": "Burbullas con mensaxes",
"IRC": "IRC",
"New layout switcher (with message bubbles)": "Nova disposición do control (con burbullas con mensaxes)",
"Image": "Imaxe",
"Sticker": "Adhesivo"
}

View file

@ -2097,7 +2097,7 @@
"You do not have permission to create rooms in this community.": "אין לך הרשאה ליצור חדרים בקהילה זו.",
"Cannot create rooms in this community": "לא ניתן ליצור חדרים בקהילה זו",
"Failed to reject invitation": "דחיית ההזמנה נכשלה",
"Explore rooms": "שיטוט בחדרים",
"Explore rooms": "גלה חדרים",
"Create a Group Chat": "צור צ'אט קבוצתי",
"Explore Public Rooms": "חקור חדרים ציבוריים",
"Send a Direct Message": "שלח הודעה ישירה",
@ -2785,5 +2785,20 @@
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "לא ניתן היה להגיע לשרת הבית שלך ולא היה ניתן להתחבר. נסה שוב. אם זה נמשך, אנא פנה למנהל שרת הבית שלך.",
"Try again": "נסה שוב",
"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.": "ביקשנו מהדפדפן לזכור באיזה שרת בית אתה משתמש כדי לאפשר לך להיכנס, אך למרבה הצער הדפדפן שלך שכח אותו. עבור לדף הכניסה ונסה שוב.",
"We couldn't log you in": "לא הצלחנו להתחבר אליך"
"We couldn't log you in": "לא הצלחנו להתחבר אליך",
"Integration manager": "מנהל אינטגרציה",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s שלכם אינו מאפשר לך להשתמש במנהל שילוב לשם כך. אנא צרו קשר עם מנהל מערכת.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "שימוש ביישומון זה עשוי לשתף נתונים <helpIcon /> עם %(widgetDomain)s ומנהל האינטגרציה שלך.",
"Identity server is": "שרת ההזדהות הינו",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "מנהלי שילוב מקבלים נתוני תצורה ויכולים לשנות ווידג'טים, לשלוח הזמנות לחדר ולהגדיר רמות הספק מטעמכם.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב לניהול בוטים, ווידג'טים וחבילות מדבקות.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב <b> (%(serverName)s) </b> לניהול בוטים, ווידג'טים וחבילות מדבקות.",
"Identity server": "שרת הזדהות",
"Identity server (%(server)s)": "שרת הזדהות (%(server)s)",
"Could not connect to identity server": "לא ניתן להתחבר אל שרת הזיהוי",
"Not a valid identity server (status code %(code)s)": "שרת זיהוי לא מאושר(קוד סטטוס %(code)s)",
"Identity server URL must be HTTPS": "הזיהוי של כתובת השרת חייבת להיות מאובטחת ב- HTTPS",
"Enter Security Phrase": "הזן ביטוי אבטחה",
"Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "לא ניתן לפענח גיבוי עם ביטוי אבטחה זה: אנא ודא שהזנת את ביטוי האבטחה הנכון.",
"Incorrect Security Phrase": "ביטוי אבטחה שגוי"
}

View file

@ -588,5 +588,6 @@
"The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।",
"Explore rooms": "रूम का अन्वेषण करें",
"Sign In": "साइन करना",
"Create Account": "खाता बनाएं"
"Create Account": "खाता बनाएं",
"Identity server is": "आइडेंटिटी सर्वर हैं"
}

View file

@ -205,5 +205,8 @@
"Add Email Address": "Dodaj email adresu",
"Confirm": "Potvrdi",
"Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.",
"Confirm adding email": "Potvrdite dodavanje email adrese"
"Confirm adding email": "Potvrdite dodavanje email adrese",
"Integration manager": "Upravitelj integracijama",
"Identity server": "Poslužitelj identiteta",
"Could not connect to identity server": "Nije moguće spojiti se na poslužitelja identiteta"
}

View file

@ -3476,5 +3476,29 @@
"Address": "Cím",
"e.g. my-space": "pl. én-terem",
"Silence call": "Némít",
"Sound on": "Hang be"
"Sound on": "Hang be",
"Use Command + F to search timeline": "Command + F az idővonalon való kereséshez",
"Unnamed audio": "Névtelen hang",
"Error processing audio message": "Hiba a hangüzenet feldolgozásánál",
"Show %(count)s other previews|one": "%(count)s további előnézet megjelenítése",
"Show %(count)s other previews|other": "%(count)s további előnézet megjelenítése",
"Images, GIFs and videos": "Képek, GIFek és videók",
"Code blocks": "Kód blokkok",
"Displaying time": "Idő megjelenítése",
"To view all keyboard shortcuts, click here.": "A billentyűzet kombinációk megjelenítéséhez kattintson ide.",
"Keyboard shortcuts": "Billentyűzet kombinációk",
"Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez",
"User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába",
"Integration manager": "Integrációs Menedzser",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)sod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.",
"Identity server is": "Azonosítási szerver",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert <b>(%(serverName)s)</b> a botok, kisalkalmazások és matrica csomagok kezeléséhez.",
"Identity server": "Azonosító szerver",
"Identity server (%(server)s)": "Azonosítási kiszolgáló (%(server)s)",
"Could not connect to identity server": "Az Azonosítási Szerverhez nem lehet csatlakozni",
"Not a valid identity server (status code %(code)s)": "Az Azonosítási Szerver nem érvényes (státusz kód: %(code)s)",
"Identity server URL must be HTTPS": "Az Azonosítási Szerver URL-jének HTTPS-nek kell lennie"
}

View file

@ -279,5 +279,9 @@
"A call is currently being placed!": "Sedang melakukan panggilan sekarang!",
"A call is already in progress!": "Masih ada panggilan berlangsung!",
"Permission Required": "Permisi Dibutuhkan",
"You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini"
"You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini",
"Explore rooms": "Jelajahi ruang",
"Sign In": "Masuk",
"Create Account": "Buat Akun",
"Identity server": "Server Identitas"
}

View file

@ -728,5 +728,7 @@
"Explore all public rooms": "Kanna öll almenningsherbergi",
"Liberate your communication": "Frelsaðu samskipti þín",
"Welcome to <name/>": "Velkomin til <name/>",
"Welcome to %(appName)s": "Velkomin til %(appName)s"
"Welcome to %(appName)s": "Velkomin til %(appName)s",
"Identity server is": "Auðkennisþjónn er",
"Identity server": "Auðkennisþjónn"
}

View file

@ -564,7 +564,7 @@
"Click to mute audio": "Clicca per silenziare l'audio",
"Clear filter": "Annulla filtro",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non hai l'autorizzazione per vedere il messaggio in questione.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non si è trovato.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non è stato trovato.",
"Failed to load timeline position": "Caricamento posizione cronologica fallito",
"Uploading %(filename)s and %(count)s others|other": "Invio di %(filename)s e altri %(count)s",
"Uploading %(filename)s and %(count)s others|zero": "Invio di %(filename)s",
@ -1266,7 +1266,7 @@
"Sends the given message coloured as a rainbow": "Invia il messaggio dato colorato come un arcobaleno",
"Sends the given emote coloured as a rainbow": "Invia l'emoticon dato colorato come un arcobaleno",
"The user's homeserver does not support the version of the room.": "L'homeserver dell'utente non supporta la versione della stanza.",
"Show hidden events in timeline": "Mostra eventi nascosti nella timeline",
"Show hidden events in timeline": "Mostra eventi nascosti nella linea temporale",
"When rooms are upgraded": "Quando le stanze vengono aggiornate",
"this room": "questa stanza",
"View older messages in %(roomName)s.": "Vedi messaggi più vecchi in %(roomName)s.",
@ -1485,7 +1485,7 @@
"Error changing power level requirement": "Errore nella modifica del livello dei permessi",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "C'é stato un errore nel cambio di libelli dei permessi. Assicurati di avere i permessi necessari e riprova.",
"No recent messages by %(user)s found": "Non sono stati trovati messaggi recenti dell'utente %(user)s",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Prova a scorrere la timeline per vedere se ce ne sono di precedenti.",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Prova a scorrere la linea temporale per vedere se ce ne sono di precedenti.",
"Remove recent messages by %(user)s": "Rimuovi gli ultimi messaggi di %(user)s",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Stai per rimuovere %(count)s messaggi di %(user)s. L'azione é irreversibile. Vuoi continuare?",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Se i messaggi sono tanti può volerci un po' di tempo. Nel frattempo, per favore, non fare alcun refresh.",
@ -2043,7 +2043,7 @@
"Collapse room list section": "Riduci sezione elenco stanze",
"Expand room list section": "Espandi sezione elenco stanze",
"Clear room list filter field": "Svuota campo filtri elenco stanze",
"Scroll up/down in the timeline": "Scorri su/giù nella cronologia",
"Scroll up/down in the timeline": "Scorri su/giù nella linea temporale",
"Toggle the top left menu": "Attiva/disattiva menu in alto a sinistra",
"Close dialog or context menu": "Chiudi finestra o menu contestuale",
"Activate selected button": "Attiva pulsante selezionato",
@ -3481,5 +3481,107 @@
"%(senderName)s changed their profile picture": "%(senderName)s ha cambiato la propria immagine del profilo",
"%(senderName)s removed their profile picture": "%(senderName)s ha rimosso la propria immagine del profilo",
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s ha rimosso il proprio nome (%(oldDisplayName)s)",
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s"
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s",
"Integration manager": "Gestore di integrazioni",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Il tuo %(brand)s non ti permette di usare il gestore di integrazioni per questa azione. Contatta un amministratore.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Usando questo widget i dati possono essere condivisi <helpIcon /> con %(widgetDomain)s e il tuo gestore di integrazioni.",
"Identity server is": "Il server di identità è",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni <b>(%(serverName)s)</b> per gestire bot, widget e pacchetti di adesivi.",
"Identity server": "Server di identità",
"Identity server (%(server)s)": "Server di identità (%(server)s)",
"Could not connect to identity server": "Impossibile connettersi al server di identità",
"Not a valid identity server (status code %(code)s)": "Non è un server di identità valido (codice di stato %(code)s)",
"Identity server URL must be HTTPS": "L'URL del server di identità deve essere HTTPS",
"Unable to copy a link to the room to the clipboard.": "Impossibile copiare un collegamento alla stanza negli appunti.",
"Unable to copy room link": "Impossibile copiare il link della stanza",
"Unnamed audio": "Audio senza nome",
"Error processing audio message": "Errore elaborazione messaggio audio",
"Copy Link": "Copia collegamento",
"Show %(count)s other previews|one": "Mostra %(count)s altra anteprima",
"Show %(count)s other previews|other": "Mostra altre %(count)s anteprime",
"Images, GIFs and videos": "Immagini, GIF e video",
"Code blocks": "Blocchi di codice",
"To view all keyboard shortcuts, click here.": "Per vedere tutte le scorciatoie, clicca qui.",
"Keyboard shortcuts": "Scorciatoie da tastiera",
"There was an error loading your notification settings.": "Si è verificato un errore caricando le tue impostazioni di notifica.",
"Mentions & keywords": "Menzioni e parole chiave",
"Global": "Globale",
"New keyword": "Nuova parola chiave",
"Keyword": "Parola chiave",
"Enable email notifications for %(email)s": "Attive le notifiche email per %(email)s",
"Enable for this account": "Attiva per questo account",
"An error occurred whilst saving your notification preferences.": "Si è verificato un errore durante il salvataggio delle tue preferenze di notifica.",
"Error saving notification preferences": "Errore nel salvataggio delle preferenze di notifica",
"Messages containing keywords": "Messaggi contenenti parole chiave",
"Use Ctrl + F to search timeline": "Usa Ctrl + F per cercare nella linea temporale",
"Use Command + F to search timeline": "Usa Command + F per cercare nella linea temporale",
"User %(userId)s is already invited to the room": "L'utente %(userId)s è già stato invitato nella stanza",
"Transfer Failed": "Trasferimento fallito",
"Unable to transfer call": "Impossibile trasferire la chiamata",
"New layout switcher (with message bubbles)": "Nuovo commutatore disposizione (con nuvolette dei messaggi)",
"Error downloading audio": "Errore di scaricamento dell'audio",
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Nota che aggiornare creerà una nuova versione della stanza</b>. Tutti i messaggi attuali resteranno in questa stanza archiviata.",
"Automatically invite members from this room to the new one": "Invita automaticamente i membri da questa stanza a quella nuova",
"These are likely ones other room admins are a part of.": "Questi sono probabilmente quelli di cui fanno parte gli altri amministratori delle stanze.",
"Other spaces or rooms you might not know": "Altri spazi o stanze che potresti non conoscere",
"Spaces you know that contain this room": "Spazi di cui sai che contengono questa stanza",
"Search spaces": "Cerca spazi",
"Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Decidi quali spazi possono accedere a questa stanza. Se uno spazio è selezionato, i suoi membri possono trovare ed entrare in <RoomName/>.",
"Select spaces": "Seleziona spazi",
"You're removing all spaces. Access will default to invite only": "Stai rimuovendo tutti gli spazi. L'accesso tornerà solo su invito",
"User Directory": "Elenco utenti",
"Room visibility": "Visibilità stanza",
"Visible to space members": "Visibile ai membri dello spazio",
"Public room": "Stanza pubblica",
"Private room (invite only)": "Stanza privata (solo a invito)",
"Create a room": "Crea una stanza",
"Only people invited will be able to find and join this room.": "Solo le persone invitate potranno trovare ed entrare in questa stanza.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Chiunque potrà trovare ed entrare in questa stanza, non solo i membri di <SpaceName/>.",
"You can change this at any time from room settings.": "Puoi cambiarlo in qualsiasi momento dalle impostazioni della stanza.",
"Everyone in <SpaceName/> will be able to find and join this room.": "Chiunque in <SpaceName/> potrà trovare ed entrare in questa stanza.",
"Image": "Immagine",
"Sticker": "Sticker",
"Downloading": "Scaricamento",
"The call is in an unknown state!": "La chiamata è in uno stato sconosciuto!",
"Call back": "Richiama",
"You missed this call": "Hai perso questa chiamata",
"This call has failed": "Questa chiamata è fallita",
"Unknown failure: %(reason)s)": "Errore sconosciuto: %(reason)s)",
"No answer": "Nessuna risposta",
"An unknown error occurred": "Si è verificato un errore sconosciuto",
"Their device couldn't start the camera or microphone": "Il suo dispositivo non ha potuto avviare la fotocamera o il microfono",
"Connection failed": "Connessione fallita",
"Could not connect media": "Connessione del media fallita",
"This call has ended": "Questa chiamata è terminata",
"Connected": "Connesso",
"The voice message failed to upload.": "Invio del messaggio vocale fallito.",
"Copy Room Link": "Copia collegamento stanza",
"Access": "Accesso",
"People with supported clients will be able to join the room without having a registered account.": "Le persone con client supportati potranno entrare nella stanza senza avere un account registrato.",
"Decide who can join %(roomName)s.": "Decidi chi può entrare in %(roomName)s.",
"Space members": "Membri dello spazio",
"Anyone in a space can find and join. You can select multiple spaces.": "Chiunque in uno spazio può trovare ed entrare. Puoi selezionare più spazi.",
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Chiunque in %(spaceName)s può trovare ed entrare. Puoi selezionare anche altri spazi.",
"Spaces with access": "Spazi con accesso",
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Chiunque in uno spazio può trovare ed entrare. <a>Modifica quali spazi possono accedere qui.</a>",
"Currently, %(count)s spaces have access|other": "Attualmente, %(count)s spazi hanno accesso",
"& %(count)s more|other": "e altri %(count)s",
"Upgrade required": "Aggiornamento necessario",
"Anyone can find and join.": "Chiunque può trovare ed entrare.",
"Only invited people can join.": "Solo le persone invitate possono entrare.",
"Private (invite only)": "Privato (solo a invito)",
"This upgrade will allow members of selected spaces access to this room without an invite.": "Questo aggiornamento permetterà ai membri di spazi selezionati di accedere alla stanza senza invito.",
"Message bubbles": "Messaggi",
"IRC": "IRC",
"This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Ciò rende facile mantenere private le stanze in uno spazio, mentre le persone potranno trovarle ed unirsi. Tutte le stanze nuove in uno spazio avranno questa opzione disponibile.",
"To help space members find and join a private room, go to that room's Security & Privacy settings.": "Per aiutare i membri dello spazio a trovare ed entrare in una stanza privata, vai nelle impostazioni \"Sicurezza e privacy\" di quella stanza.",
"Help space members find private rooms": "Aiuta i membri dello spazio a trovare stanze private",
"Help people in spaces to find and join private rooms": "Aiuta le persone negli spazi a trovare ed entrare nelle stanze private",
"New in the Spaces beta": "Novità nella beta degli spazi",
"They didn't pick up": "Non ha risposto",
"Call again": "Richiama",
"They declined this call": "Ha rifiutato questa chiamata",
"You declined this call": "Hai rifiutato questa chiamata"
}

View file

@ -2504,5 +2504,15 @@
"You can change these anytime.": "ここで入力した情報はいつでも編集できます。",
"Add some details to help people recognise it.": "情報を入力してください。",
"View dev tools": "開発者ツールを表示",
"To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です"
"To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です",
"Integration manager": "インテグレーションマネージャ",
"Identity server is": "アイデンティティ・サーバー",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "インテグレーションマネージャは設定データを受け取り、ユーザーの代わりにウィジェットの変更、部屋への招待の送信、権限レベルの設定を行うことができます。",
"Use an integration manager to manage bots, widgets, and sticker packs.": "インテグレーションマネージャを使用して、ボット、ウィジェット、ステッカーパックを管理します。",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "インテグレーションマネージャ <b>(%(serverName)s)</b> を使用して、ボット、ウィジェット、ステッカーパックを管理します。",
"Identity server": "認証サーバ",
"Identity server (%(server)s)": "identity サーバー (%(server)s)",
"Could not connect to identity server": "identity サーバーに接続できませんでした",
"Not a valid identity server (status code %(code)s)": "有効な identity サーバーではありません (ステータスコード %(code)s)",
"Identity server URL must be HTTPS": "identityサーバーのURLは HTTPS スキーマである必要があります"
}

View file

@ -2754,5 +2754,17 @@
"(an error occurred)": "(tella-d tuccḍa)",
"(connection failed)": "(tuqqna ur teddi ara)",
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Iqeddcen akk ttwagedlen seg uttekki! Taxxamt-a dayen ur tettuseqdac ara.",
"Try again": "Ɛreḍ tikkelt-nniḍen"
"Try again": "Ɛreḍ tikkelt-nniḍen",
"Integration manager": "Amsefrak n umsidef",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-ik·im ur ak·am yefki ara tisirag i useqdec n umsefrak n umsidef i wakken ad tgeḍ aya. Ttxil-k·m nermes anedbal.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Aseqdec n uwiǧit-a yezmer ad yebḍu isefka <helpIcon/> d %(widgetDomain)s & amsefrak-inek·inem n umsidef.",
"Identity server is": "Aqeddac n timagit d",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Imsefrak n yimsidaf remmsen-d isefka n uswel, syen ad uɣalen zemren ad beddlen iwiǧiten, ad aznen tinubgiwin ɣer texxamin, ad yesbadu daɣen tazmert n yiswiren s yiswiren deg ubdil-ik·im.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef <b>(%(serverName)s)</b> i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.",
"Identity server": "Aqeddac n timagit",
"Identity server (%(server)s)": "Aqeddac n timagit (%(server)s)",
"Could not connect to identity server": "Ur izmir ara ad yeqqen ɣer uqeddac n timagit",
"Not a valid identity server (status code %(code)s)": "Aqeddac n timagit mačči d ameɣtu (status code %(code)s)",
"Identity server URL must be HTTPS": "URL n uqeddac n timagit ilaq ad yili d HTTPS"
}

View file

@ -1667,5 +1667,13 @@
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "사용자 %(userId)s의 세션 %(deviceId)s에서 받은 서명 키와 당신이 제공한 서명 키가 일치합니다. 세션이 검증되었습니다.",
"Show more": "더 보기",
"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.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다.",
"Create Account": "계정 만들기"
"Create Account": "계정 만들기",
"Integration manager": "통합 관리자",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "이 위젯을 사용하면 <helpcon /> %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.",
"Identity server is": "ID 서버:",
"Identity server": "ID 서버",
"Identity server (%(server)s)": "ID 서버 (%(server)s)",
"Could not connect to identity server": "ID 서버에 연결할 수 없음",
"Not a valid identity server (status code %(code)s)": "올바르지 않은 ID 서버 (상태 코드 %(code)s)",
"Identity server URL must be HTTPS": "ID 서버 URL은 HTTPS이어야 함"
}

View file

@ -2421,5 +2421,17 @@
"New Zealand": "Naujoji Zelandija",
"New Caledonia": "Naujoji Kaledonija",
"Netherlands": "Nyderlandai",
"Cayman Islands": "Kaimanų Salos"
"Cayman Islands": "Kaimanų Salos",
"Integration manager": "Integracijų tvarkytuvas",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Jūsų %(brand)s neleidžia jums naudoti integracijų tvarkytuvo tam atlikti. Susisiekite su administratoriumi.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Naudojimasis šiuo valdikliu gali pasidalinti duomenimis <helpIcon /> su %(widgetDomain)s ir jūsų integracijų tvarkytuvu.",
"Identity server is": "Tapatybės serveris yra",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų Tvarkytuvai gauna konfigūracijos duomenis ir jūsų vardu gali keisti valdiklius, siųsti kambario pakvietimus ir nustatyti galios lygius.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą botų, valdiklių ir lipdukų pakuočių tvarkymui.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą <b>(%(serverName)s)</b> botų, valdiklių ir lipdukų pakuočių tvarkymui.",
"Identity server": "Tapatybės serveris",
"Identity server (%(server)s)": "Tapatybės serveris (%(server)s)",
"Could not connect to identity server": "Nepavyko prisijungti prie tapatybės serverio",
"Not a valid identity server (status code %(code)s)": "Netinkamas tapatybės serveris (statuso kodas %(code)s)",
"Identity server URL must be HTTPS": "Tapatybės Serverio URL privalo būti HTTPS"
}

View file

@ -1582,5 +1582,9 @@
"Upload files": "Failu augšupielāde",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Šie faili <b>pārsniedz</b> augšupielādes izmēra limitu %(limit)s.",
"Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)",
"Check your devices": "Pārskatiet savas ierīces"
"Check your devices": "Pārskatiet savas ierīces",
"Integration manager": "Integrācija pārvaldnieks",
"Identity server is": "Indentifikācijas serveris ir",
"Identity server": "Identitāšu serveris",
"Could not connect to identity server": "Neizdevās pieslēgties identitāšu serverim"
}

View file

@ -130,5 +130,7 @@
"Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു...",
"Explore rooms": "മുറികൾ കണ്ടെത്തുക",
"Sign In": "പ്രവേശിക്കുക",
"Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക"
"Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക",
"Integration manager": "സംയോജക മാനേജർ",
"Identity server": "തിരിച്ചറിയൽ സെർവർ"
}

View file

@ -1981,5 +1981,13 @@
"Costa Rica": "Costa Rica",
"Cook Islands": "Cook-øyene",
"All keys backed up": "Alle nøkler er sikkerhetskopiert",
"Secret storage:": "Hemmelig lagring:"
"Secret storage:": "Hemmelig lagring:",
"Integration manager": "Integreringsbehandler",
"Identity server is": "Identitetstjeneren er",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integreringsbehandlere mottar oppsettsdata, og kan endre på moduler, sende rominvitasjoner, og bestemme styrkenivåer på dine vegne.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler til å behandle botter, moduler, og klistremerkepakker.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler <b>(%(serverName)s)</b> til å behandle botter, moduler, og klistremerkepakker.",
"Identity server": "Identitetstjener",
"Identity server (%(server)s)": "Identitetstjener (%(server)s)",
"Could not connect to identity server": "Kunne ikke koble til identitetsserveren"
}

View file

@ -171,7 +171,7 @@
"Fill screen": "Scherm vullen",
"Filter room members": "Gespreksleden filteren",
"Forget room": "Gesprek vergeten",
"For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Gelieve opnieuw inloggen.",
"For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Log opnieuw in.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s van %(fromPowerLevel)s naar %(toPowerLevel)s",
"Guests cannot join this room even if explicitly invited.": "Gasten - zelfs speficiek uitgenodigde - kunnen niet aan dit gesprek deelnemen.",
"Hangup": "Ophangen",
@ -1034,12 +1034,12 @@
"Legal": "Juridisch",
"Credits": "Met dank aan",
"For help with using %(brand)s, click <a>here</a>.": "Klik <a>hier</a> voor hulp bij het gebruiken van %(brand)s.",
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Klik <a>hier</a> voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.",
"For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Klik <a>hier</a> voor hulp bij het gebruiken van %(brand)s of begin een gesprek met onze robot met de knop hieronder.",
"Help & About": "Hulp & info",
"Bug reporting": "Bug meldingen",
"FAQ": "FAQ",
"Versions": "Versies",
"Preferences": "Instellingen",
"Preferences": "Voorkeuren",
"Composer": "Opsteller",
"Timeline": "Tijdslijn",
"Room list": "Gesprekslijst",
@ -1199,7 +1199,7 @@
"Invalid homeserver discovery response": "Ongeldig homeserver-vindbaarheids-antwoord",
"Invalid identity server discovery response": "Ongeldig identiteitsserver-vindbaarheidsantwoord",
"General failure": "Algemene fout",
"This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.",
"This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met een e-mailadres.",
"Please <a>contact your service administrator</a> to continue using this service.": "Gelieve <a>contact op te nemen met uw dienstbeheerder</a> om deze dienst te blijven gebruiken.",
"Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt",
"Sign in with single sign-on": "Inloggen met eenmalig inloggen",
@ -1272,7 +1272,7 @@
"Upload files (%(current)s of %(total)s)": "Bestanden versturen (%(current)s van %(total)s)",
"Upload files": "Bestanden versturen",
"Upload": "Versturen",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is <b>te groot</b> om te versturen. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is <b>te groot</b> om te versturen. Het limiet is %(limit)s en dit bestand is %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Deze bestanden zijn <b>te groot</b> om te versturen. De bestandsgroottelimiet is %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn <b>te groot</b> om te versturen. De bestandsgroottelimiet is %(limit)s.",
"Upload %(count)s other files|other": "%(count)s overige bestanden versturen",
@ -1402,7 +1402,7 @@
"Summary": "Samenvatting",
"Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.",
"You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt niet inloggen met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.",
"This account has been deactivated.": "Deze account is gesloten.",
"This account has been deactivated.": "Dit account is gesloten.",
"Messages": "Berichten",
"Actions": "Acties",
"Displays list of commands with usages and descriptions": "Toont een lijst van beschikbare opdrachten, met hun gebruiken en beschrijvingen",
@ -1497,7 +1497,7 @@
"Share this email in Settings to receive invites directly in %(brand)s.": "Deel in de instellingen dit e-mailadres om uitnodigingen direct in %(brand)s te ontvangen.",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. <default>Gebruik de standaardserver (%(defaultIdentityServerName)s)</default> of beheer de server in de <settings>Instellingen</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de <settings>Instellingen</settings>.",
"Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.",
"Please fill why you're reporting.": "Geef aan waarom u deze melding indient.",
"Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw homeserver",
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Dit bericht melden zal zijn unieke gebeurtenis-ID versturen naar de beheerder van uw homeserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw homeserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.",
"Send report": "Rapport versturen",
@ -1564,7 +1564,7 @@
"Session already verified!": "Sessie al geverifieerd!",
"WARNING: Session already verified, but keys do NOT MATCH!": "PAS OP: de sessie is al geverifieerd, maar de sleutels komen NIET OVEREEN!",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "PAS OP: sleutelverificatie MISLUKT! De combinatie %(userId)s + sessie %(deviceId)s is ondertekend met %(fprint)s - maar de opgegeven sleutel is %(fingerprint)s. Wellicht worden uw berichten onderschept!",
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.",
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte sleutel en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.",
"%(senderName)s placed a voice call.": "%(senderName)s probeert u te bellen.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s poogt u te bellen, maar uw browser ondersteunt dat niet",
"%(senderName)s placed a video call.": "%(senderName)s doet een video-oproep.",
@ -2333,7 +2333,7 @@
"Switch to dark mode": "Naar donkere modus wisselen",
"Switch to light mode": "Naar lichte modus wisselen",
"Appearance": "Weergave",
"All settings": "Alle instellingen",
"All settings": "Instellingen",
"Error removing address": "Fout bij verwijderen van adres",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Er is een fout opgetreden bij het verwijderen van dit adres. Deze bestaat mogelijk niet meer, of er is een tijdelijke fout opgetreden.",
"You don't have permission to delete the address.": "U heeft geen toestemming om het adres te verwijderen.",
@ -2517,7 +2517,7 @@
"You can only pin up to %(count)s widgets|other": "U kunt maar %(count)s widgets vastzetten",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "In versleutelde gesprekken zijn uw berichten beveiligd, enkel de ontvanger en u hebben de unieke sleutels om ze te ontsleutelen.",
"Waiting for you to accept on your other session…": "Wachten totdat u uw uitnodiging in uw andere sessie aanneemt…",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat gebruikers dit gesprek via uw homeserver (%(localDomain)s) kunnen vinden",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat personen dit gesprek via uw homeserver (%(localDomain)s) kunnen vinden",
"Local Addresses": "Lokale adressen",
"Local address": "Lokaal adres",
"The server has denied your request.": "De server heeft uw verzoek afgewezen.",
@ -2564,7 +2564,7 @@
"Use app for a better experience": "Gebruik de app voor een betere ervaring",
"Enable desktop notifications": "Bureaubladmeldingen inschakelen",
"Don't miss a reply": "Mis geen antwoord",
"Unknown App": "Onbekende App",
"Unknown App": "Onbekende app",
"Error leaving room": "Fout bij verlaten gesprek",
"Unexpected server error trying to leave the room": "Onverwachte serverfout bij het verlaten van dit gesprek",
"See <b>%(msgtype)s</b> messages posted to your active room": "Zie <b>%(msgtype)s</b>-berichten verstuurd in uw actieve gesprek",
@ -2803,7 +2803,7 @@
"Successfully restored %(sessionCount)s keys": "Succesvol %(sessionCount)s sleutels hersteld",
"Keys restored": "Sleutels hersteld",
"Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Back-up kon niet worden ontsleuteld met dit veiligheidswachtwoord: controleer of u het juiste veiligheidswachtwoord hebt ingevoerd.",
"Incorrect Security Phrase": "Onjuist Veiligheidswachtwoord",
"Incorrect Security Phrase": "Onjuist veiligheidswachtwoord",
"Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Back-up kon niet worden ontcijferd met deze veiligheidssleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd.",
"Security Key mismatch": "Verkeerde veiligheidssleutel",
"%(completed)s of %(total)s keys restored": "%(completed)s van %(total)s sleutels hersteld",
@ -3021,9 +3021,9 @@
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd, omdat deze homeserver is geblokkeerd door zijn beheerder. Gelieve <a>contact op te nemen met uw beheerder</a> om de dienst te blijven gebruiken.",
"Are you sure you want to leave the space '%(spaceName)s'?": "Weet u zeker dat u de space '%(spaceName)s' wilt verlaten?",
"This space is not public. You will not be able to rejoin without an invite.": "Deze space is niet openbaar. Zonder uitnodiging zult u niet opnieuw kunnen toetreden.",
"Start audio stream": "Audio-stream starten",
"Start audio stream": "Audiostream starten",
"Failed to start livestream": "Starten van livestream is mislukt",
"Unable to start audio streaming.": "Kan audio-streaming niet starten.",
"Unable to start audio streaming.": "Kan audiostream niet starten.",
"Save Changes": "Wijzigingen opslaan",
"Saving...": "Opslaan...",
"View dev tools": "Bekijk dev tools",
@ -3034,7 +3034,7 @@
"Edit settings relating to your space.": "Bewerk instellingen gerelateerd aan uw space.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, gebruikersnaam (zoals <userId/>) of <a>deel deze space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, e-mailadres, gebruikersnaam (zoals <userId/>) of <a>deel deze space</a>.",
"Unnamed Space": "Naamloze Space",
"Unnamed Space": "Naamloze space",
"Invite to %(spaceName)s": "Voor %(spaceName)s uitnodigen",
"Failed to add rooms to space": "Het toevoegen van gesprekken aan de space is mislukt",
"Apply": "Toepassen",
@ -3158,7 +3158,7 @@
"%(count)s people you know have already joined|other": "%(count)s personen die u kent hebben zijn al geregistreerd",
"Accept on your other login…": "Accepteer op uw andere login…",
"Stop & send recording": "Stop & verstuur opname",
"Record a voice message": "Audiobericht opnemen",
"Record a voice message": "Spraakbericht opnemen",
"Invite messages are hidden by default. Click to show the message.": "Uitnodigingen zijn standaard verborgen. Klik om de uitnodigingen weer te geven.",
"Quick actions": "Snelle acties",
"Invite to just this room": "Uitnodigen voor alleen dit gesprek",
@ -3176,7 +3176,7 @@
"If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.",
"Only do this if you have no other device to complete verification with.": "Doe dit alleen als u geen ander apparaat hebt om de verificatie mee uit te voeren.",
"Reset everything": "Alles opnieuw instellen",
"Forgotten or lost all recovery methods? <a>Reset all</a>": "Alles vergeten of alle herstelmethoden verloren? <a>Alles opnieuw instellen</a>",
"Forgotten or lost all recovery methods? <a>Reset all</a>": "Alles vergeten en alle herstelmethoden verloren? <a>Alles opnieuw instellen</a>",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Als u dat doet, let wel geen van uw berichten wordt verwijderd, maar de zoekresultaten zullen gedurende enkele ogenblikken verslechteren terwijl de index opnieuw wordt aangemaakt",
"View message": "Bericht bekijken",
"Zoom in": "Inzoomen",
@ -3369,5 +3369,108 @@
"%(targetName)s accepted an invitation": "%(targetName)s accepteerde de uitnodiging",
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepteerde de uitnodiging voor %(displayName)s",
"Some invites couldn't be sent": "Sommige uitnodigingen konden niet verstuurd worden",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor <RoomName/>"
"We sent the others, but the below people couldn't be invited to <RoomName/>": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor <RoomName/>",
"Unnamed audio": "Naamloze audio",
"Error processing audio message": "Fout bij verwerking audiobericht",
"Show %(count)s other previews|one": "%(count)s andere preview weergeven",
"Show %(count)s other previews|other": "%(count)s andere previews weergeven",
"Images, GIFs and videos": "Afbeeldingen, GIF's en video's",
"Code blocks": "Codeblokken",
"Displaying time": "Tijdsweergave",
"To view all keyboard shortcuts, click here.": "Om alle sneltoetsen te zien, klik hier.",
"Keyboard shortcuts": "Sneltoetsen",
"Use Ctrl + F to search timeline": "Gebruik Ctrl +F om te zoeken in de tijdlijn",
"Use Command + F to search timeline": "Gebruik Command + F om te zoeken in de tijdlijn",
"User %(userId)s is already invited to the room": "De gebruiker %(userId)s is al uitgenodigd voor dit gesprek",
"Integration manager": "Integratiebeheerder",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Met het gebruik van deze widget deelt u mogelijk gegevens <helpIcon /> met %(widgetDomain)s & uw integratiebeheerder.",
"Identity server is": "Identiteitsserver is",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integratiebeheerders ontvangen configuratie-informatie en kunnen widgets aanpassen, gespreksuitnodigingen versturen en machtsniveaus namens u aanpassen.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder om bots, widgets en stickerpakketten te beheren.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder <b>(%(serverName)s)</b> om bots, widgets en stickerpakketten te beheren.",
"Identity server": "Identiteitsserver",
"Identity server (%(server)s)": "Identiteitsserver (%(server)s)",
"Could not connect to identity server": "Kon geen verbinding maken met de identiteitsserver",
"Not a valid identity server (status code %(code)s)": "Geen geldige identiteitsserver (statuscode %(code)s)",
"Identity server URL must be HTTPS": "Identiteitsserver-URL moet HTTPS zijn",
"User Directory": "Gebruikersgids",
"Copy Link": "Link kopieren",
"There was an error loading your notification settings.": "Er was een fout bij het laden van uw meldingsvoorkeuren.",
"Mentions & keywords": "Vermeldingen & trefwoorden",
"Global": "Overal",
"New keyword": "Nieuw trefwoord",
"Keyword": "Trefwoord",
"Enable email notifications for %(email)s": "E-mailmeldingen inschakelen voor %(email)s",
"Enable for this account": "Voor dit account inschakelen",
"An error occurred whilst saving your notification preferences.": "Er is een fout opgetreden tijdens het opslaan van uw meldingsvoorkeuren.",
"Error saving notification preferences": "Fout bij het opslaan van meldingsvoorkeuren",
"Messages containing keywords": "Berichten met trefwoord",
"Transfer Failed": "Doorverbinden is mislukt",
"Unable to transfer call": "Doorverbinden is mislukt",
"Unable to copy a link to the room to the clipboard.": "Kopiëren van gesprekslink naar het klembord is mislukt.",
"Unable to copy room link": "Kopiëren van gesprekslink is mislukt",
"Copy Room Link": "Kopieer gesprekslink",
"Message bubbles": "Berichtenbubbels",
"IRC": "IRC",
"New layout switcher (with message bubbles)": "Nieuwe layout schakelaar (met berichtenbubbels)",
"Downloading": "Downloading",
"The call is in an unknown state!": "Deze oproep heeft een onbekende status!",
"Call back": "Terugbellen",
"You missed this call": "U heeft deze oproep gemist",
"This call has failed": "Deze oproep is mislukt",
"Unknown failure: %(reason)s)": "Onbekende fout: %(reason)s",
"No answer": "Geen antwoord",
"An unknown error occurred": "Er is een onbekende fout opgetreden",
"Their device couldn't start the camera or microphone": "Het andere apparaat kon de camera of microfoon niet starten",
"Connection failed": "Verbinding mislukt",
"Could not connect media": "Mediaverbinding mislukt",
"This call has ended": "Deze oproep is beëindigd",
"Connected": "Verbonden",
"Spaces with access": "Spaces met toegang",
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Iedereen in een space kan het gesprek vinden en aan deelnemen. <a>Wijzig welke spaces toegang hebben hier.</a>",
"Currently, %(count)s spaces have access|other": "Momenteel hebben %(count)s spaces toegang",
"& %(count)s more|other": "& %(count)s meer",
"Upgrade required": "Upgrade noodzakelijk",
"Anyone can find and join.": "Iedereen kan hem vinden en deelnemen.",
"Only invited people can join.": "Alleen uitgenodigde personen kunnen deelnemen.",
"Private (invite only)": "Privé (alleen op uitnodiging)",
"This upgrade will allow members of selected spaces access to this room without an invite.": "Deze upgrade maakt het mogelijk voor leden van geselecteerde spaces om toegang te krijgen tot dit gesprek zonder een uitnodiging.",
"This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Dit maakt het makkelijk om gesprekken privé te houden voor een space, terwijl personen in de space hem kunnen vinden en aan deelnemen. Alle nieuwe gesprekken in deze space hebben deze optie beschikbaar.",
"To help space members find and join a private room, go to that room's Security & Privacy settings.": "Om space leden te helpen met het vinden van en deel te nemen aan privégesprekken, ga naar uw gespreksinstellingen voor veiligheid & privacy.",
"Help space members find private rooms": "Help space leden privégesprekken te vinden",
"Help people in spaces to find and join private rooms": "Help personen in spaces om privégesprekken te vinden en aan deel te nemen",
"New in the Spaces beta": "Nieuw in de spaces beta",
"Everyone in <SpaceName/> will be able to find and join this room.": "Iedereen in <SpaceName/> kan dit gesprek vinden en aan deelnemen.",
"Image": "Afbeelding",
"Sticker": "Sticker",
"They didn't pick up": "Ze hebben niet opgenomen",
"Call again": "Opnieuw bellen",
"They declined this call": "Ze weigerden deze oproep",
"You declined this call": "U heeft deze oproep geweigerd",
"The voice message failed to upload.": "Het spraakbericht versturen is mislukt.",
"Access": "Toegang",
"People with supported clients will be able to join the room without having a registered account.": "Personen met geschikte apps zullen aan de gesprekken kunnen deelnemen zonder een account te hebben.",
"Decide who can join %(roomName)s.": "Kies wie kan deelnemen aan %(roomName)s.",
"Space members": "Space leden",
"Anyone in a space can find and join. You can select multiple spaces.": "Iedereen in een space kan zoeken en deelnemen. U kunt meerdere spaces selecteren.",
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Iedereen in %(spaceName)s kan zoeken en deelnemen. U kunt ook andere spaces selecteren.",
"Visible to space members": "Zichtbaar voor space leden",
"Public room": "Openbaar gesprek",
"Private room (invite only)": "Privégesprek (alleen op uitnodiging)",
"Create a room": "Gesprek aanmaken",
"Only people invited will be able to find and join this room.": "Alleen uitgenodigde personen kunnen dit gesprek vinden en aan deelnemen.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Iedereen kan dit gesprek vinden en aan deelnemen, niet alleen leden van <SpaceName/>.",
"You can change this at any time from room settings.": "U kan dit op elk moment wijzigen vanuit de gespreksinstellingen.",
"Error downloading audio": "Fout bij downloaden van audio",
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Let op bijwerken maakt een nieuwe versie van dit gesprek</b>. Alle huidige berichten blijven in dit gearchiveerde gesprek.",
"Automatically invite members from this room to the new one": "Automatisch leden uitnodigen van dit gesprek in de nieuwe",
"These are likely ones other room admins are a part of.": "Er zijn waarschijnlijk gesprekken waar andere gespreksbeheerders deel van uitmaken.",
"Other spaces or rooms you might not know": "Andere spaces of gesprekken die u misschien niet kent",
"Spaces you know that contain this room": "Spaces die u kent met dit gesprek",
"Search spaces": "Spaces zoeken",
"Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Kies welke spaces toegang hebben tot dit gesprek. Als een space is geselecteerd kunnen deze leden <RoomName/> vinden en aan deelnemen.",
"Select spaces": "Spaces selecteren",
"You're removing all spaces. Access will default to invite only": "U verwijderd alle spaces. De toegang zal standaard alleen op uitnodiging zijn",
"Room visibility": "Gesprekszichtbaarheid"
}

View file

@ -1376,5 +1376,12 @@
"Identity Server": "Identitetstenar",
"Email Address": "E-postadresse",
"Go Back": "Gå attende",
"Notification settings": "Varslingsinnstillingar"
"Notification settings": "Varslingsinnstillingar",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Du bør <b>fjerne dine personlege data</b> frå identitetstenaren <idserver /> før du koplar frå. Dessverre er identitetstenaren <idserver /> utilgjengeleg og kan ikkje nåast akkurat no.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Vi tilrår at du slettar personleg informasjon, som e-postadresser og telefonnummer frå identitetstenaren før du koplar frå.",
"Privacy": "Personvern",
"Versions": "Versjonar",
"Legal": "Juridisk",
"Identity server is": "Identitetstenaren er",
"Identity server": "Identitetstenar"
}

View file

@ -2368,5 +2368,15 @@
"Some suggestions may be hidden for privacy.": "Niektóre propozycje mogą być ukryte z uwagi na prywatność.",
"If you can't see who youre looking for, send them your invite link below.": "Jeżeli nie możesz zobaczyć osób, których szukasz, wyślij im poniższy odnośnik z zaproszeniem.",
"Or send invite link": "Lub wyślij odnośnik z zaproszeniem",
"We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a)."
"We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a).",
"Integration manager": "Menedżer Integracji",
"Identity server is": "Serwer tożsamości to",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji aby zarządzać botami, widżetami i pakietami naklejek.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji <b>%(serverName)s</b> aby zarządzać botami, widżetami i pakietami naklejek.",
"Identity server": "Serwer toższamości",
"Identity server (%(server)s)": "Serwer tożsamości (%(server)s)",
"Could not connect to identity server": "Nie można połączyć z serwerem tożsamości",
"Not a valid identity server (status code %(code)s)": "Nieprawidłowy serwer tożsamości (kod statusu %(code)s)",
"Identity server URL must be HTTPS": "URL serwera tożsamości musi być HTTPS"
}

View file

@ -572,5 +572,7 @@
"Your user agent": "O seu user agent",
"Explore rooms": "Explorar rooms",
"Sign In": "Iniciar sessão",
"Create Account": "Criar conta"
"Create Account": "Criar conta",
"Not a valid identity server (status code %(code)s)": "Servidor de Identidade inválido (código de status %(code)s)",
"Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS"
}

View file

@ -3110,5 +3110,17 @@
"Inviting...": "Convidando...",
"Invite by username": "Convidar por nome de usuário",
"Support": "Suporte",
"Original event source": "Fonte do evento original"
"Original event source": "Fonte do evento original",
"Integration manager": "Gerenciador de integrações",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Seu %(brand)s não permite que você use o gerenciador de integrações para fazer isso. Entre em contato com o administrador.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Se você usar esse widget, os dados poderão ser compartilhados <helpIcon /> com %(widgetDomain)s & seu gerenciador de integrações.",
"Identity server is": "O servidor de identificação é",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "O gerenciador de integrações recebe dados de configuração e pode modificar widgets, enviar convites para salas e definir níveis de permissão em seu nome.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações para gerenciar bots, widgets e pacotes de figurinhas.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações em <b>(%(serverName)s)</b> para gerenciar bots, widgets e pacotes de figurinhas.",
"Identity server": "Servidor de identidade",
"Identity server (%(server)s)": "Servidor de identidade (%(server)s)",
"Could not connect to identity server": "Não foi possível conectar-se ao servidor de identidade",
"Not a valid identity server (status code %(code)s)": "Servidor de identidade inválido (código de status %(code)s)",
"Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS"
}

View file

@ -3219,5 +3219,17 @@
"Send and receive voice messages": "Отправлять и получать голосовые сообщения",
"%(deviceId)s from %(ip)s": "%(deviceId)s с %(ip)s",
"The user you called is busy.": "Вызываемый пользователь занят.",
"User Busy": "Пользователь занят"
"User Busy": "Пользователь занят",
"Integration manager": "Менеджер интеграции",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не позволяет вам использовать для этого Менеджер Интеграции. Пожалуйста, свяжитесь с администратором.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Используя этот виджет, вы можете делиться данными <helpIcon /> с %(widgetDomain)s и вашим Менеджером Интеграции.",
"Identity server is": "Сервер идентификации",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджеры интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Используйте Менеджер интеграциями для управления ботами, виджетами и стикерами.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Используйте менеджер интеграций <b>%(serverName)s</b> для управления ботами, виджетами и стикерами.",
"Identity server": "Сервер идентификаций",
"Identity server (%(server)s)": "Сервер идентификации (%(server)s)",
"Could not connect to identity server": "Не смог подключиться к серверу идентификации",
"Not a valid identity server (status code %(code)s)": "Неправильный Сервер идентификации (код статуса %(code)s)",
"Identity server URL must be HTTPS": "URL-адрес сервера идентификации должен быть HTTPS"
}

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