Merge branch 'develop' into show-username

This commit is contained in:
Šimon Brandner 2021-04-21 18:08:50 +02:00
commit 951fda4c3d
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
102 changed files with 2373 additions and 622 deletions
.eslintignore.errorfilesCHANGELOG.mdpackage.json
res
src
test
Singleflight-test.ts
end-to-end-tests/src/usecases

View file

@ -1,7 +1,7 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/Markdown.js
src/Velociraptor.js
src/NodeAnimator.js
src/components/structures/RoomDirectory.js
src/components/views/rooms/MemberList.js
src/ratelimitedfunc.js

View file

@ -312,11 +312,12 @@ Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/
## Security notice
matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the
user content sandbox can be abused to trick users into opening unexpected
documents. The content is opened with a `blob` origin that cannot access Matrix
user data, so messages and secrets are not at risk. Thanks to @keerok for
responsibly disclosing this via Matrix's Security Disclosure Policy.
matrix-react-sdk 3.15.0 fixes a moderate severity issue (CVE-2021-21320) where
the user content sandbox can be abused to trick users into opening unexpected
documents after several user interactions. The content can be opened with a
`blob` origin from the Matrix client, so it is possible for a malicious document
to access user messages and secrets. Thanks to @keerok for responsibly
disclosing this via Matrix's Security Disclosure Policy.
## All changes

View file

@ -102,7 +102,6 @@
"tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
"velocity-animate": "^2.0.6",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
},

View file

@ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e
:root {
font-size: 10px;
--transition-short: .1s;
--transition-standard: .3s;
}
@media (prefers-reduced-motion) {
:root {
--transition-short: 0;
--transition-standard: 0;
}
}
html {

View file

@ -22,7 +22,6 @@ limitations under the License.
}
.mx_FilePanel .mx_RoomView_messageListWrapper {
margin-right: 20px;
flex-direction: row;
align-items: center;
justify-content: center;

View file

@ -21,6 +21,5 @@ limitations under the License.
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow-y: hidden;
}
}

View file

@ -117,6 +117,32 @@ limitations under the License.
.mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
.mx_UserMenu_dnd {
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
&::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $muted-fg-color;
}
&.mx_UserMenu_dnd_noisy::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
&.mx_UserMenu_dnd_muted::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
}
}
&.mx_UserMenu_minimized {

View file

@ -101,7 +101,8 @@ limitations under the License.
}
.mx_SearchBox {
margin: 0;
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
@ -123,7 +124,9 @@ limitations under the License.
}
.mx_AddExistingToSpaceDialog_section {
margin-top: 24px;
&:not(:first-child) {
margin-top: 24px;
}
> h3 {
margin: 0;

View file

@ -68,8 +68,8 @@ limitations under the License.
}
&.mx_BasicMessageComposer_input_disabled {
// Ignore all user input to avoid accidentally triggering the composer
pointer-events: none;
cursor: not-allowed;
}
}

View file

@ -159,6 +159,7 @@ $left-gutter: 64px;
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
visibility: visible;
@ -282,6 +283,10 @@ $left-gutter: 64px;
display: inline-block;
height: $font-14px;
width: $font-14px;
transition:
left var(--transition-short) ease-out,
top var(--transition-standard) ease-out;
}
.mx_EventTile_readAvatarRemainder {

View file

@ -216,6 +216,25 @@ $irc-line-height: $font-18px;
}
}
}
.mx_EventTile_emote {
> .mx_EventTile_avatar {
margin-left: initial;
}
}
.mx_MessageTimestamp {
width: initial;
}
/**
* adding the icon back in the document flow
* if it's not present, there's no unwanted wasted space
*/
.mx_EventTile_e2eIcon {
position: relative;
order: -1;
}
}
.mx_ProfileResizer {

View file

@ -18,6 +18,10 @@ limitations under the License.
margin-left: 8px;
margin-bottom: 4px;
&.mx_RoomSublist_hidden {
display: none;
}
.mx_RoomSublist_headerContainer {
// Create a flexbox to make alignment easy
display: flex;
@ -37,7 +41,9 @@ limitations under the License.
// The combined height must be set in the LeftPanel component for sticky headers
// to work correctly.
padding-bottom: 8px;
height: 24px;
// Allow the container to collapse on itself if its children
// are not in the normal document flow
max-height: 24px;
color: $roomlist-header-color;
.mx_RoomSublist_stickable {

View file

@ -53,7 +53,8 @@ limitations under the License.
font-size: $font-14px;
&::before {
// TODO: @@ TravisR: Animate
animation: recording-pulse 2s infinite;
content: '';
background-color: $voice-record-live-circle-color;
width: 10px;
@ -74,3 +75,26 @@ limitations under the License.
width: 42px; // we're not using a monospace font, so fake it
}
}
// The keyframes are slightly weird here to help make a ramping/punch effect
// for the recording dot. We start and end at 100% opacity to help make the
// dot feel a bit like a real lamp that is blinking: the animation ends up
// spending a lot of its time showing a steady state without a fade effect.
// This lamp effect extends into why the 0% opacity keyframe is not in the
// midpoint: lamps take longer to turn off than they do to turn on, and the
// extra frames give it a bit of a realistic punch for when the animation is
// ramping back up to 100% opacity.
//
// Target animation timings: steady for 1.5s, fade out for 0.3s, fade in for 0.2s
// (intended to be used in a loop for 2s animation speed)
@keyframes recording-pulse {
0% {
opacity: 1;
}
35% {
opacity: 0;
}
65% {
opacity: 1;
}
}

View file

@ -189,11 +189,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy _light for variable information
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color;
$voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color;
$voice-record-live-circle-color: #ff4b55;
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -181,10 +181,10 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color;
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
$voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color;
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;

View file

@ -39,7 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore";
import {VoiceRecorder} from "../voice/VoiceRecorder";
import {VoiceRecording} from "../voice/VoiceRecording";
declare global {
interface Window {
@ -71,7 +71,7 @@ declare global {
mxModalWidgetStore: ModalWidgetStore;
mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass;
mxVoiceRecorder: typeof VoiceRecorder;
mxVoiceRecorder: typeof VoiceRecording;
}
interface Document {

View file

@ -1,16 +1,15 @@
import React from "react";
import ReactDom from "react-dom";
import Velocity from "velocity-animate";
import PropTypes from 'prop-types';
/**
* The Velociraptor contains components and animates transitions with velocity.
* The NodeAnimator contains components and animates transitions.
* It will only pick up direct changes to properties ('left', currently), and so
* will not work for animating positional changes where the position is implicit
* from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries.
*/
export default class Velociraptor extends React.Component {
export default class NodeAnimator extends React.Component {
static propTypes = {
// either a list of child nodes, or a single child.
children: PropTypes.any,
@ -20,14 +19,10 @@ export default class Velociraptor extends React.Component {
// a list of state objects to apply to each child node in turn
startStyles: PropTypes.array,
// a list of transition options from the corresponding startStyle
enterTransitionOpts: PropTypes.array,
};
static defaultProps = {
startStyles: [],
enterTransitionOpts: [],
};
constructor(props) {
@ -41,6 +36,18 @@ export default class Velociraptor extends React.Component {
this._updateChildren(this.props.children);
}
/**
*
* @param {HTMLElement} node element to apply styles to
* @param {object} styles a key/value pair of CSS properties
* @returns {void}
*/
_applyStyles(node, styles) {
Object.entries(styles).forEach(([property, value]) => {
node.style[property] = value;
});
}
_updateChildren(newChildren) {
const oldChildren = this.children || {};
this.children = {};
@ -50,17 +57,8 @@ export default class Velociraptor extends React.Component {
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left !== c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => {
// special case visibility because it's nonsensical to animate an invisible element
// so we always hidden->visible pre-transition and visible->hidden after
if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
oldNode.style.visibility = c.props.style.visibility;
}
});
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
oldNode.style.visibility = c.props.style.visibility;
this._applyStyles(oldNode, { left: c.props.style.left });
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
// clone the old element with the props (and children) of the new element
// so prop updates are still received by the children.
@ -94,33 +92,22 @@ export default class Velociraptor extends React.Component {
this.props.startStyles.length > 0
) {
const startStyles = this.props.startStyles;
const transitionOpts = this.props.enterTransitionOpts;
const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc.
for (var i = 1; i < startStyles.length; ++i) {
Velocity(domNode, startStyles[i], transitionOpts[i-1]);
/*
console.log("start:",
JSON.stringify(transitionOpts[i-1]),
"->",
JSON.stringify(startStyles[i]),
);
*/
for (let i = 1; i < startStyles.length; ++i) {
this._applyStyles(domNode, startStyles[i]);
// console.log("start:"
// JSON.stringify(startStyles[i]),
// );
}
// and then we animate to the resting state
Velocity(domNode, restingStyle,
transitionOpts[i-1])
.then(() => {
// once we've reached the resting state, hide the element if
// appropriate
domNode.style.visibility = restingStyle.visibility;
});
setTimeout(() => {
this._applyStyles(domNode, restingStyle);
}, 0);
// console.log("enter:",
// JSON.stringify(transitionOpts[i-1]),
// "->",
// JSON.stringify(restingStyle));
}
this.nodes[k] = node;
@ -128,9 +115,7 @@ export default class Velociraptor extends React.Component {
render() {
return (
<span>
{ Object.values(this.children) }
</span>
<>{ Object.values(this.children) }</>
);
}
}

View file

@ -383,6 +383,10 @@ export const Notifier = {
// don't bother notifying as user was recently active in this room
return;
}
if (SettingsStore.getValue("doNotDisturb")) {
// Don't bother the user if they didn't ask to be bothered
return;
}
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);

View file

@ -1222,4 +1222,5 @@ export function getCommand(input: string) {
args,
};
}
return {};
}

View file

@ -1,17 +0,0 @@
import Velocity from "velocity-animate";
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
let pow2;
let bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2
}
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
}
Velocity.Easings.easeOutBounce = function(p) {
return 1 - bounce(1 - p);
};

View file

@ -154,7 +154,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private doStickyHeaders(list: HTMLDivElement) {
const topEdge = list.scrollTop;
const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin;

View file

@ -1096,8 +1096,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
const warnings = [];
const memberCount = roomToLeave.currentState.getJoinedMemberCount();
if (memberCount === 1) {
warnings.push((
<span className="warning" key="only_member_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ }
{ _t("You are the only person here. " +
"If you leave, no one will be able to join in the future, including you.") }
</span>
));
return warnings;
}
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
if (joinRules) {
const rule = joinRules.getContent().join_rule;
if (rule !== "public") {

View file

@ -659,6 +659,7 @@ export default class MessagePanel extends React.Component {
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>
</li>,

View file

@ -74,6 +74,7 @@ interface IState {
export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription;
@ -89,6 +90,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
// Force update is the easiest way to trigger the UI update (we don't store state for this)
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
}
private get hasHomePage(): boolean {
@ -103,6 +107,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove();
@ -288,6 +293,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({contextMenuPosition: null}); // also close the menu
};
private onDndToggle = (ev) => {
ev.stopPropagation();
const current = SettingsStore.getValue("doNotDisturb");
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
};
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
@ -534,6 +545,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
{/* masked image in CSS */}
</span>
);
let dnd;
if (this.state.selectedSpace) {
name = (
<div className="mx_UserMenu_doubleName">
@ -560,6 +572,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
</div>
);
isPrototype = true;
} else if (SettingsStore.getValue("feature_dnd")) {
const isDnd = SettingsStore.getValue("doNotDisturb");
dnd = <AccessibleButton
onClick={this.onDndToggle}
className={classNames({
"mx_UserMenu_dnd": true,
"mx_UserMenu_dnd_noisy": !isDnd,
"mx_UserMenu_dnd_muted": isDnd,
})}
/>;
}
if (this.props.isMinimized) {
name = null;
@ -595,6 +617,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</span>
{name}
{dnd}
{buttons}
</div>
</ContextMenuButton>

View file

@ -436,6 +436,8 @@ export default class Registration extends React.Component<IProps, IState> {
// ok fine, there's still no session: really go to the login page
this.props.onLoginClick();
}
return sessionLoaded;
};
private renderRegisterComponent() {
@ -557,7 +559,12 @@ export default class Registration extends React.Component<IProps, IState> {
loggedInUserId: this.state.differentLoggedInUserId,
},
)}</p>
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this.onLoginClickWithCheck}>
<p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
const sessionLoaded = await this.onLoginClickWithCheck(event);
if (sessionLoaded) {
dis.dispatch({action: "view_welcome_page"});
}
}}>
{_t("Continue with previous account")}
</AccessibleButton></p>
</div>;

View file

@ -57,21 +57,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const existingSubspacesSet = new Set(existingSubspaces);
const spaces = SpaceStore.instance.getSpaces().filter(s => {
return !existingSubspacesSet.has(s) // not already in space
&& space !== s // not the top-level space
&& selectedSpace !== s // not the selected space
&& s.name.toLowerCase().includes(lcQuery); // contains query
});
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
const existingRoomsSet = new Set(existingRooms);
const rooms = cli.getVisibleRooms().filter(room => {
return !existingRoomsSet.has(room) // not already in space
&& !room.isSpaceRoom() // not a space itself
&& room.name.toLowerCase().includes(lcQuery) // contains query
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
});
const joinRule = selectedSpace.getJoinRule();
const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => {
if (room.getMyMembership() !== "join") return arr;
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
if (room.isSpaceRoom()) {
if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) {
arr[0].push(room);
}
} else if (!existingRoomsSet.has(room) && joinRule !== "public") {
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room);
}
return arr;
}, [[], [], []]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
@ -172,7 +174,28 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div>
) : null }
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>

View file

@ -31,6 +31,7 @@ import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
IInvite3PID,
} from "../../../createRoom";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard";
@ -618,13 +619,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
_startDm = async () => {
this.setState({busy: true});
const client = MatrixClientPeg.get();
const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room;
if (targetIds.length === 1) {
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]);
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
@ -646,7 +648,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
@ -656,35 +657,41 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>;
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
createRoomPromise = createRoom(createRoomOptions);
} else if (isSelf) {
createRoomPromise = createRoom(createRoomOptions);
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
return inviteMultipleToRoom(roomId, targetIds);
}).then(result => {
if (this._shouldAbortAfterInviteError(result)) {
return true; // abort
}
});
}
try {
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
}
// the createRoom call will show the room for us, so we don't need to worry about that.
createRoomPromise.then(abort => {
if (abort === true) return; // only abort on true booleans, not roomIds or something
if (targetIds.length > 1) {
createRoomOptions.createOpts = targetIds.reduce(
(roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
)
}
await createRoom(createRoomOptions);
this.props.onFinished();
}).catch(err => {
} catch (err) {
console.error(err);
this.setState({
busy: false,
errorText: _t("We couldn't create your DM."),
});
});
}
};
_inviteUsers = async () => {

View file

@ -36,7 +36,7 @@ export default class SeshatResetDialog extends React.PureComponent<IDialogProps>
{_t("You most likely do not want to reset your event index store")}
<br />
{_t("If you do, please note that none of your messages will be deleted, " +
"but the search experience might be degraded for a few moments" +
"but the search experience might be degraded for a few moments " +
"whilst the index is recreated",
)}
</p>

View file

@ -262,7 +262,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback}
forceOnRight
alignment={Tooltip.Alignment.Right}
/>;
}

View file

@ -38,7 +38,7 @@ const MAX_ZOOM = 300;
// This is used for the buttons
const ZOOM_STEP = 10;
// This is used for mouse wheel events
const ZOOM_COEFFICIENT = 10;
const ZOOM_COEFFICIENT = 7.5;
// If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10;
@ -209,6 +209,10 @@ export default class ImageView extends React.Component<IProps, IState> {
ev.stopPropagation();
ev.preventDefault();
// Don't do anything if we pressed any
// other button than the left one
if (ev.button !== 0) return;
// Zoom in if we are completely zoomed out
if (this.state.zoom === MIN_ZOOM) {
this.setState({zoom: MAX_ZOOM});

View file

@ -18,8 +18,8 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import Tooltip from './Tooltip';
import { _t } from "../../../languageHandler";
import Tooltip, {Alignment} from './Tooltip';
import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps {
@ -61,7 +61,7 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
className="mx_InfoTooltip_container"
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
label={tooltip || title}
forceOnRight={true}
alignment={Alignment.Right}
/> : <div />;
return (
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">

View file

@ -139,6 +139,8 @@ export default class PersistedElement extends React.Component {
_onAction(payload) {
if (payload.action === 'timeline_resize') {
this._repositionChild();
} else if (payload.action === 'logout') {
PersistedElement.destroyElement(this.props.persistKey);
}
}

View file

@ -25,6 +25,14 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
const MIN_TOOLTIP_HEIGHT = 25;
export enum Alignment {
Natural, // Pick left or right
Left,
Right,
Top, // Centered
Bottom, // Centered
}
interface IProps {
// Class applied to the element used to position the tooltip
className?: string;
@ -36,7 +44,7 @@ interface IProps {
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode;
forceOnRight?: boolean;
alignment?: Alignment; // defaults to Natural
yOffset?: number;
}
@ -46,10 +54,14 @@ export default class Tooltip extends React.Component<IProps> {
private tooltip: void | Element | Component<Element, any, any>;
private parent: Element;
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
// so we expose the Alignment options off of us statically.
public static readonly Alignment = Alignment;
public static readonly defaultProps = {
visible: true,
yOffset: 0,
alignment: Alignment.Natural,
};
// Create a wrapper for the tooltip outside the parent and attach it to the body element
@ -86,11 +98,35 @@ export default class Tooltip extends React.Component<IProps> {
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset;
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
} else {
style.left = parentBox.right + window.pageXOffset + 6;
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
const top = baseTop + offset;
const right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
const left = parentBox.right + window.pageXOffset + 6;
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
switch (this.props.alignment) {
case Alignment.Natural:
if (parentBox.right > window.innerWidth / 2) {
style.right = right;
style.top = top;
break;
}
// fall through to Right
case Alignment.Right:
style.left = left;
style.top = top;
break;
case Alignment.Left:
style.right = right;
style.top = top;
break;
case Alignment.Top:
style.top = baseTop - 16;
style.left = horizontalCenter;
break;
case Alignment.Bottom:
style.top = baseTop + parentBox.height;
style.left = horizontalCenter;
break;
}
return style;

View file

@ -140,7 +140,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}
public componentDidUpdate(prevProps: IProps) {
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
// We need to re-check the placeholder when the enabled state changes because it causes the
// placeholder element to remount, which gets rid of the `::before` class. Re-evaluating the
// placeholder means we get a proper `::before` with the placeholder.
const enabledChange = this.props.disabled !== prevProps.disabled;
const placeholderChanged = this.props.placeholder !== prevProps.placeholder;
if (this.props.placeholder && (placeholderChanged || enabledChange)) {
const {isEmpty} = this.props.model;
if (isEmpty) {
this.showPlaceholder();
@ -670,8 +675,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
});

View file

@ -260,6 +260,9 @@ export default class EventTile extends React.Component {
// whether or not to show flair at all
enableFlair: PropTypes.bool,
// whether or not to show read receipts
showReadReceipts: PropTypes.bool,
};
static defaultProps = {
@ -858,8 +861,6 @@ export default class EventTile extends React.Component {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
const readAvatars = this.getReadAvatars();
let avatar;
let sender;
let avatarSize;
@ -988,6 +989,16 @@ export default class EventTile extends React.Component {
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
let msgOption;
if (this.props.showReadReceipts) {
const readAvatars = this.getReadAvatars();
msgOption = (
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
);
}
switch (this.props.tileShape) {
case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
@ -1107,9 +1118,7 @@ export default class EventTile extends React.Component {
{ reactionsRow }
{ actionBar }
</div>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{msgOption}
{
// The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids

View file

@ -29,11 +29,12 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
import {UIFeature} from "../../../settings/UIFeature";
import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
import {RecordingState} from "../../../voice/VoiceRecording";
import Tooltip, {Alignment} from "../elements/Tooltip";
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -178,17 +179,15 @@ export default class MessageComposer extends React.Component {
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate);
this._dispatcherRef = null;
this.state = {
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
isComposerEmpty: true,
haveRecording: false,
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
};
}
@ -204,14 +203,6 @@ export default class MessageComposer extends React.Component {
}
};
_onWidgetUpdate = () => {
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
};
_onActiveWidgetUpdate = () => {
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
};
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
@ -238,8 +229,7 @@ export default class MessageComposer extends React.Component {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
}
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate);
dis.unregister(this.dispatcherRef);
}
@ -327,8 +317,18 @@ export default class MessageComposer extends React.Component {
});
}
onVoiceUpdate = (haveRecording: boolean) => {
this.setState({haveRecording});
_onVoiceStoreUpdate = () => {
const recording = VoiceRecordingStore.instance.activeRecording;
this.setState({haveRecording: !!recording});
if (recording) {
// 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.
recording.on(RecordingState.EndingSoon, ({secondsLeft}) => {
this.setState({recordingTimeLeftSeconds: secondsLeft});
setTimeout(() => this.setState({recordingTimeLeftSeconds: null}), 3000);
});
}
};
render() {
@ -352,7 +352,6 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
// TODO: @@ TravisR - Disabling the composer doesn't work
disabled={this.state.haveRecording}
/>,
);
@ -373,8 +372,7 @@ export default class MessageComposer extends React.Component {
if (SettingsStore.getValue("feature_voice_messages")) {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
room={this.props.room}
onRecording={this.onVoiceUpdate} />);
room={this.props.room} />);
}
if (!this.state.isComposerEmpty || this.state.haveRecording) {
@ -411,8 +409,18 @@ export default class MessageComposer extends React.Component {
);
}
let recordingTooltip;
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
if (secondsLeft) {
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", {seconds: secondsLeft})}
alignment={Alignment.Top} yOffset={-50}
/>;
}
return (
<div className="mx_MessageComposer mx_GroupLayout">
{recordingTooltip}
<div className="mx_MessageComposer_wrapper">
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
<div className="mx_MessageComposer_row">

View file

@ -17,22 +17,13 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import '../../../VelocityBounce';
import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor";
import NodeAnimator from "../../../NodeAnimator";
import * as sdk from "../../../index";
import {toPx} from "../../../utils/units";
import {replaceableComponent} from "../../../utils/replaceableComponent";
let bounce = false;
try {
if (global.localStorage) {
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
}
} catch (e) {
}
@replaceableComponent("views.rooms.ReadReceiptMarker")
export default class ReadReceiptMarker extends React.PureComponent {
static propTypes = {
@ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
// we've already done our display - nothing more to do.
return;
}
this._animateMarker();
}
componentDidUpdate(prevProps) {
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
const visibilityChanged = prevProps.hidden !== this.props.hidden;
if (differentLeftOffset || visibilityChanged) {
this._animateMarker();
}
}
_animateMarker() {
// treat new RRs as though they were off the top of the screen
let oldTop = -15;
@ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
}
const startStyles = [];
const enterTransitionOpts = [];
if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px",
left: toPx(oldInfo.left) });
const reorderTransitionOpts = {
duration: 100,
easing: 'easeOut',
};
enterTransitionOpts.push(reorderTransitionOpts);
}
// then shift to the rightmost column,
// and then it will drop down to its resting position
//
// XXX: We use a small left value to trick velocity-animate into actually animating.
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
// skip applying it, thus making our read receipt at +14px instead of +0px like it
// should be. This does cause a tiny amount of drift for read receipts, however with a
// value so small it's not perceived by a user.
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
// fail to fall down or cause gaps.
startStyles.push({ top: startTopOffset+'px', left: '1px' });
enterTransitionOpts.push({
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
});
startStyles.push({ top: startTopOffset+'px', left: '0' });
this.setState({
suppressDisplay: false,
startStyles: startStyles,
enterTransitionOpts: enterTransitionOpts,
});
}
@ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent {
const style = {
left: toPx(this.props.leftOffset),
top: '0px',
visibility: this.props.hidden ? 'hidden' : 'visible',
};
let title;
@ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent {
}
return (
<Velociraptor
startStyles={this.state.startStyles}
enterTransitionOpts={this.state.enterTransitionOpts} >
<NodeAnimator
startStyles={this.state.startStyles} >
<MemberAvatar
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
@ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
onClick={this.props.onClick}
inputRef={this._avatar}
/>
</Velociraptor>
</NodeAnimator>
);
}
}

View file

@ -289,12 +289,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
// shallow-copy from the template as we need to make modifications to it
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
this.updateDmAddRoomAction();
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
}
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
@ -502,59 +501,50 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
private renderSublists(): React.ReactElement[] {
const components: React.ReactElement[] = [];
const tagOrder = TAG_ORDER.reduce((p, c) => {
if (c === CUSTOM_TAGS_BEFORE_TAG) {
const customTags = Object.keys(this.state.sublists)
.filter(t => isCustomTag(t));
p.push(...customTags);
}
p.push(c);
return p;
}, [] as TagID[]);
// show a skeleton UI if the user is in no rooms and they are not filtering
const showSkeleton = !this.state.isNameFiltering &&
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
for (const orderedTagId of tagOrder) {
const orderedRooms = this.state.sublists[orderedTagId] || [];
let extraTiles = null;
if (orderedTagId === DefaultTagID.Invite) {
extraTiles = this.renderCommunityInvites();
} else if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms();
return TAG_ORDER.reduce((tags, tagId) => {
if (tagId === CUSTOM_TAGS_BEFORE_TAG) {
const customTags = Object.keys(this.state.sublists)
.filter(tagId => isCustomTag(tagId));
tags.push(...customTags);
}
tags.push(tagId);
return tags;
}, [] as TagID[])
.map(orderedTagId => {
let extraTiles = null;
if (orderedTagId === DefaultTagID.Invite) {
extraTiles = this.renderCommunityInvites();
} else if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms();
}
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
continue; // skip tag - not needed
}
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId)
: this.tagAesthetics[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId)
: this.tagAesthetics[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
components.push(<RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
showSkeleton={showSkeleton}
extraTiles={extraTiles}
/>);
}
return components;
// The cost of mounting/unmounting this component offsets the cost
// of keeping it in the DOM and hiding it when it is not required
return <RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
showSkeleton={showSkeleton}
extraTiles={extraTiles}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
/>
});
}
public render() {

View file

@ -74,6 +74,7 @@ interface IProps {
tagId: TagID;
onResize: () => void;
showSkeleton?: boolean;
alwaysVisible?: boolean;
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
@ -125,8 +126,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
};
// Why Object.assign() and not this.state.height? Because TypeScript says no.
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
}
private calculateInitialHeight() {
@ -242,6 +241,11 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return false;
}
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
}
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
@ -759,6 +763,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
'mx_RoomSublist': true,
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist_minimized': this.props.isMinimized,
'mx_RoomSublist_hidden': !this.state.rooms.length && this.props.alwaysVisible !== true,
});
let content = null;

View file

@ -97,22 +97,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
// generatePreview() will return nothing if the user has previews disabled
messagePreview: this.generatePreview(),
};
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
this.props.room.on("Room.name", this.onRoomNameUpdate);
}
private onRoomNameUpdate = (room) => {
@ -167,6 +153,20 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
if (this.state.selected) {
this.scrollIntoView();
}
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.roomProps.on("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
}
public componentWillUnmount() {
@ -182,8 +182,15 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
);
this.props.room.off("Room.name", this.onRoomNameUpdate);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.roomProps.off("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
}
private onAction = (payload: ActionPayload) => {
@ -547,7 +554,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
/>;
let badge: React.ReactNode;
if (!this.props.isMinimized) {
if (!this.props.isMinimized && this.notificationState) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = (
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
@ -563,7 +570,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
let messagePreview = null;
if (this.showMessagePreview && this.state.messagePreview) {
messagePreview = (
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
<div
className="mx_RoomTile_messagePreview"
id={messagePreviewId(this.props.room.roomId)}
title={this.state.messagePreview}
>
{this.state.messagePreview}
</div>
);

View file

@ -477,6 +477,10 @@ export default class SendMessageComposer extends React.Component {
}
onAction = (payload) => {
// don't let the user into the composer if it is disabled - all of these branches lead
// to the cursor being in the composer
if (this.props.disabled) return;
switch (payload.action) {
case 'reply_to_event':
case Action.FocusComposer:

View file

@ -17,21 +17,21 @@ limitations under the License.
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {_t} from "../../../languageHandler";
import React from "react";
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
import {VoiceRecording} from "../../../voice/VoiceRecording";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import classNames from "classnames";
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
interface IProps {
room: Room;
onRecording: (haveRecording: boolean) => void;
}
interface IState {
recorder?: VoiceRecorder;
recorder?: VoiceRecording;
}
/**
@ -57,13 +57,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
msgtype: "org.matrix.msc2516.voice",
url: mxc,
});
await VoiceRecordingStore.instance.disposeRecording();
this.setState({recorder: null});
this.props.onRecording(false);
return;
}
const recorder = new VoiceRecorder(MatrixClientPeg.get());
const recorder = VoiceRecordingStore.instance.startRecording();
await recorder.start();
this.props.onRecording(true);
this.setState({recorder});
};

View file

@ -15,12 +15,12 @@ limitations under the License.
*/
import React from "react";
import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder";
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import Clock from "./Clock";
interface IProps {
recorder: VoiceRecorder;
recorder: VoiceRecording;
}
interface IState {

View file

@ -15,14 +15,14 @@ limitations under the License.
*/
import React from "react";
import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder";
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
import {percentageOf} from "../../../utils/numbers";
import Waveform from "./Waveform";
interface IProps {
recorder: VoiceRecorder;
recorder: VoiceRecording;
}
interface IState {

View file

@ -90,6 +90,12 @@ export interface IOpts {
parentSpace?: Room;
}
export interface IInvite3PID {
id_server: string,
medium: 'email',
address: string,
}
/**
* Create a new room, and switch to it.
*

View file

@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
// Regexp based on Simpler Version from https://gist.github.com/gregseth/5582254 - matches RFC2822
const EMAIL_ADDRESS_REGEX = new RegExp(
"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*" + // localpart
"@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$", "i");
export function looksValid(email: string): boolean {
return EMAIL_ADDRESS_REGEX.test(email);

View file

@ -1551,5 +1551,6 @@
"You've reached the maximum number of simultaneous calls.": "لقد وصلت للحد الاقصى من المكالمات المتزامنة.",
"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.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح."
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.",
"Explore rooms": "استكشِف الغرف"
}

View file

@ -380,5 +380,8 @@
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "Bu otaqda %(groups)s üçün %(senderDisplayName)s aktiv oldu.",
"powered by Matrix": "Matrix tərəfindən təchiz edilmişdir",
"Custom Server Options": "Fərdi Server Seçimləri",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu."
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.",
"Create Account": "Hesab Aç",
"Explore rooms": "Otaqları kəşf edin",
"Sign In": "Daxil ol"
}

View file

@ -950,5 +950,6 @@
"Confirm": "Confirma",
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.",
"Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon",
"Unable to access microphone": "No s'ha pogut accedir al micròfon"
"Unable to access microphone": "No s'ha pogut accedir al micròfon",
"Explore rooms": "Explora sales"
}

View file

@ -3165,5 +3165,42 @@
"Edit devices": "Upravit zařízení",
"Check your devices": "Zkontrolujte svá zařízení",
"You have unverified logins": "Máte neověřená přihlášení",
"Open": "Otevřít"
"Open": "Otevřít",
"Share decryption keys for room history when inviting users": "Při pozvání uživatelů sdílet dešifrovací klíče pro historii místnosti",
"Manage & explore rooms": "Spravovat a prozkoumat místnosti",
"Message search initilisation failed": "Inicializace vyhledávání zpráv se nezdařila",
"%(count)s people you know have already joined|one": "%(count)s osoba, kterou znáte, se již připojila",
"Invited people will be able to read old messages.": "Pozvaní lidé budou moci číst staré zprávy.",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Pokud tak učiníte, nezapomeňte, že žádná z vašich zpráv nebude smazána, ale vyhledávání může být na několik okamžiků degradováno, zatímco index bude znovu vytvářen",
"You can add more later too, including already existing ones.": "Později můžete přidat i další, včetně již existujících.",
"Verify your identity to access encrypted messages and prove your identity to others.": "Ověřte svou identitu, abyste získali přístup k šifrovaným zprávám a prokázali svou identitu ostatním.",
"Sends the given message as a spoiler": "Odešle danou zprávu jako spoiler",
"Review to ensure your account is safe": "Zkontrolujte, zda je váš účet v bezpečí",
"%(deviceId)s from %(ip)s": "%(deviceId)s z %(ip)s",
"Send and receive voice messages (in development)": "Odesílat a přijímat hlasové zprávy (ve vývoji)",
"unknown person": "neznámá osoba",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Konzultace s %(transferTarget)s. <a>Převod na %(transferee)s</a>",
"Warn before quitting": "Varovat před ukončením",
"Invite to just this room": "Pozvat jen do této místnosti",
"Quick actions": "Rychlé akce",
"Invite messages are hidden by default. Click to show the message.": "Zprávy s pozvánkou jsou ve výchozím nastavení skryté. Kliknutím zobrazíte zprávu.",
"Record a voice message": "Nahrát hlasovou zprávu",
"Stop & send recording": "Zastavit a odeslat záznam",
"Accept on your other login…": "Přijměte ve svém dalším přihlášení…",
"%(count)s people you know have already joined|other": "%(count)s lidí, které znáte, se již připojili",
"Add existing rooms": "Přidat stávající místnosti",
"Adding...": "Přidávání...",
"We couldn't create your DM.": "Nemohli jsme vytvořit vaši přímou zprávu.",
"Consult first": "Nejprve se poraďte",
"You most likely do not want to reset your event index store": "Pravděpodobně nechcete resetovat úložiště indexů událostí",
"Reset event store": "Resetovat úložiště událostí",
"Reset event store?": "Resetovat úložiště událostí?",
"Verify other login": "Ověřit další přihlášení",
"Avatar": "Avatar",
"Verification requested": "Žádost ověření",
"Please choose a strong password": "Vyberte silné heslo",
"What are some things you want to discuss in %(spaceName)s?": "O kterých tématech chcete diskutovat v %(spaceName)s?",
"Let's create a room for each of them.": "Vytvořme pro každé z nich místnost.",
"Use another login": "Použijte jiné přihlašovací jméno",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Bez ověření nebudete mít přístup ke všem svým zprávám a ostatním se můžete zobrazit jako nedůvěryhodný."
}

View file

@ -8,5 +8,8 @@
"The version of %(brand)s": "Fersiwn %(brand)s",
"Whether or not you're logged in (we don't record your username)": "Os ydych wedi mewngofnodi ai peidio (nid ydym yn cofnodi'ch enw defnyddiwr)",
"Your language of choice": "Eich iaith o ddewis",
"The version of %(brand)s": "Fersiwn %(brand)s"
"Sign In": "Mewngofnodi",
"Create Account": "Creu Cyfrif",
"Dismiss": "Wfftio",
"Explore rooms": "Archwilio Ystafelloedd"
}

View file

@ -54,7 +54,7 @@
"OK": "OK",
"Search": "Søg",
"Custom Server Options": "Brugerdefinerede serverindstillinger",
"Dismiss": "Afskedige",
"Dismiss": "Afslut",
"powered by Matrix": "Drevet af Matrix",
"Close": "Luk",
"Cancel": "Afbryd",
@ -618,5 +618,45 @@
"Unable to access microphone": "Kan ikke tilgå mikrofonen",
"The call could not be established": "Opkaldet kunne ikke etableres",
"Call Declined": "Opkald afvist",
"Folder": "Mappe"
"Folder": "Mappe",
"We couldn't log you in": "Vi kunne ikke logge dig ind",
"Try again": "Prøv igen",
"Already in call": "",
"You're already in a call with this person.": "Du har allerede i et opkald med denne person.",
"Chile": "Chile",
"Call failed because webcam or microphone could not be accessed. Check that:": "Opkald fejlede på grund af kamera og mikrofon ikke kunne nås. Tjek dette:",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Opkald fejlede på grund af mikrofon ikke kunne nås. Tjek at din mikrofon er tilsluttet og sat op rigtigt.",
"India": "Indien",
"Iceland": "Island",
"Hong Kong": "Hong Kong",
"Greenland": "Grønland",
"Greece": "Grækenland",
"Ghana": "Ghana",
"Germany": "Tyskland",
"Faroe Islands": "Færøerne",
"Estonia": "Estonien",
"Ecuador": "Ecuador",
"Czech Republic": "Tjekkiet",
"Colombia": "Colombien",
"Chad": "Chad",
"Bulgaria": "Bulgarien",
"Brazil": "Brazilien",
"Bosnia": "Bosnien",
"Bolivia": "Bolivien",
"Belarus": "Hviderusland",
"Austria": "Østrig",
"Australia": "Australien",
"Armenia": "Armenien",
"Argentina": "Argentina",
"Antarctica": "Antarktis",
"Angola": "Angola",
"Albania": "Albanien",
"Afghanistan": "Afghanistan",
"United States": "Amerikas Forenede Stater",
"United Kingdom": "Storbritanien",
"This will end the conference for everyone. Continue?": "Dette vil afbryde opkaldet for alle. Fortsæt?",
"No other application is using the webcam": "Ingen anden application bruger kameraet",
"A microphone and webcam are plugged in and set up correctly": "En mikrofon og kamera er tilsluttet og sat op rigtigt",
"Croatia": "Kroatien",
"Answered Elsewhere": "Svaret andet sted"
}

File diff suppressed because it is too large Load diff

View file

@ -786,6 +786,7 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
"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.",
@ -1473,6 +1474,7 @@
"The conversation continues here.": "The conversation continues here.",
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
"You do not have permission to post to this room": "You do not have permission to post to this room",
"%(seconds)ss left": "%(seconds)ss left",
"Bold": "Bold",
"Italics": "Italics",
"Strikethrough": "Strikethrough",
@ -2012,6 +2014,7 @@
"Add existing rooms": "Add existing rooms",
"Filter your rooms and spaces": "Filter your rooms and spaces",
"Spaces": "Spaces",
"Direct Messages": "Direct Messages",
"Don't want to add an existing room?": "Don't want to add an existing room?",
"Create a new room": "Create a new room",
"Failed to add rooms to space": "Failed to add rooms to space",
@ -2202,7 +2205,6 @@
"Suggestions": "Suggestions",
"May include members not in %(communityName)s": "May include members not in %(communityName)s",
"Recently Direct Messaged": "Recently Direct Messaged",
"Direct Messages": "Direct Messages",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Start a conversation with someone using their name, email address or username (like <userId/>).",
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
@ -2308,7 +2310,7 @@
"About homeservers": "About homeservers",
"Reset event store?": "Reset event store?",
"You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated",
"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": "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",
"Reset event store": "Reset event store",
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
@ -2554,6 +2556,7 @@
"Failed to reject invitation": "Failed to reject invitation",
"Cannot create rooms in this community": "Cannot create rooms in this community",
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "You are the only person here. If you leave, no one will be able to join in the future, including you.",
"This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?",

View file

@ -650,5 +650,12 @@
"Error upgrading room": "Error upgrading room",
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Changes the avatar of the current room": "Changes the avatar of the current room",
"Changes your avatar in all rooms": "Changes your avatar in all rooms"
"Changes your avatar in all rooms": "Changes your avatar in all rooms",
"Favourited": "Favorited",
"Explore rooms": "Explore rooms",
"Click the button below to confirm adding this email address.": "Click the button below to confirm adding this email address.",
"Confirm adding email": "Confirm adding email",
"Single Sign On": "Single Sign On",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity.",
"Use Single Sign On to continue": "Use Single Sign On to continue"
}

View file

@ -3188,5 +3188,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "De %(deviceName)s (%(deviceId)s) en",
"Check your devices": "Comprueba tus dispositivos",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Alguien está iniciando sesión a tu cuenta: %(name)s (%(deviceID)s) en %(ip)s",
"You have unverified logins": "Tienes inicios de sesión sin verificar"
"You have unverified logins": "Tienes inicios de sesión sin verificar",
"Verification requested": "Verificación solicitada",
"Avatar": "Imagen de perfil",
"Verify other login": "Verificar otro inicio de sesión",
"Consult first": "Consultar primero",
"Invited people will be able to read old messages.": "Las personas invitadas podrán leer mensajes antiguos.",
"We couldn't create your DM.": "No hemos podido crear tu mensaje directo.",
"Adding...": "Añadiendo...",
"Add existing rooms": "Añadir salas existentes",
"%(count)s people you know have already joined|one": "%(count)s persona que ya conoces se ha unido",
"%(count)s people you know have already joined|other": "%(count)s personas que ya conoces se han unido",
"Accept on your other login…": "Acepta en tu otro inicio de sesión…",
"Stop & send recording": "Parar y enviar grabación",
"Record a voice message": "Grabar un mensaje de voz",
"Quick actions": "Acciones rápidas",
"Invite to just this room": "Invitar solo a esta sala",
"Warn before quitting": "Avisar antes de salir",
"Manage & explore rooms": "Gestionar y explorar salas",
"unknown person": "persona desconocida",
"Share decryption keys for room history when inviting users": "Compartir claves para descifrar el historial de la sala al invitar a gente",
"Send and receive voice messages (in development)": "Enviar y recibir mensajes de voz (en desarrollo)",
"%(deviceId)s from %(ip)s": "%(deviceId)s desde %(ip)s",
"Review to ensure your account is safe": "Revisa que tu cuenta esté segura",
"Sends the given message as a spoiler": "Envía el mensaje como un spoiler",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consultando a %(transferTarget)s. <a>Transferir a %(transferee)s</a>",
"Message search initilisation failed": "Ha fallado la inicialización de la búsqueda de mensajes",
"Reset event store?": "¿Restablecer almacenamiento de eventos?",
"You most likely do not want to reset your event index store": "Lo más probable es que no quieras restablecer tu almacenamiento de índice de ecentos",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Si lo haces, ten en cuenta que no se borrarán tus mensajes, pero la experiencia de búsqueda será peor durante unos momentos mientras se recrea el índice",
"Reset event store": "Restablecer el almacenamiento de eventos",
"What are some things you want to discuss in %(spaceName)s?": "¿De qué quieres hablar en %(spaceName)s?",
"Let's create a room for each of them.": "Crearemos una sala para cada uno.",
"You can add more later too, including already existing ones.": "Puedes añadir más después, incluso si ya existen.",
"Please choose a strong password": "Por favor, elige una contraseña segura",
"Use another login": "Usar otro inicio de sesión",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifica tu identidad para acceder a mensajes cifrados y probar tu identidad a otros.",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Si no verificas no tendrás acceso a todos tus mensajes y puede que aparezcas como no confiable para otros usuarios.",
"Invite messages are hidden by default. Click to show the message.": "Los mensajes de invitación no se muestran por defecto. Haz clic para mostrarlo."
}

View file

@ -2517,7 +2517,7 @@
"Join the conference from the room information card on the right": "Liitu konverentsiga selle jututoa infolehelt paremal",
"Video conference ended by %(senderName)s": "%(senderName)s lõpetas video rühmakõne",
"Video conference updated by %(senderName)s": "%(senderName)s uuendas video rühmakõne",
"Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne",
"Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõnet",
"End conference": "Lõpeta videokonverents",
"This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
"Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine",
@ -3226,5 +3226,42 @@
"Open": "Ava",
"Check your devices": "Kontrolli oma seadmeid",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Uus sisselogimissessioon kasutab sinu Matrixi kontot: %(name)s %(deviceID)s aadressil %(ip)s",
"You have unverified logins": "Sul on verifitseerimata sisselogimissessioone"
"You have unverified logins": "Sul on verifitseerimata sisselogimissessioone",
"Manage & explore rooms": "Halda ja uuri jututubasid",
"Warn before quitting": "Hoiata enne rakenduse töö lõpetamist",
"Invite to just this room": "Kutsi vaid siia jututuppa",
"Quick actions": "Kiirtoimingud",
"Adding...": "Lisan...",
"Sends the given message as a spoiler": "Saadab selle sõnumi rõõmurikkujana",
"unknown person": "tundmatu isik",
"Send and receive voice messages (in development)": "Saada ja võta vastu häälsõnumeid (arendusjärgus)",
"%(deviceId)s from %(ip)s": "%(deviceId)s ip-aadressil %(ip)s",
"Review to ensure your account is safe": "Tagamaks, et su konto on sinu kontrolli all, vaata andmed üle",
"Share decryption keys for room history when inviting users": "Kasutajate kutsumisel jaga jututoa ajaloo võtmeid",
"Record a voice message": "Salvesta häälsõnum",
"Stop & send recording": "Lõpeta salvestamine ja saada häälsõnum",
"Add existing rooms": "Lisa olemasolevaid jututubasid",
"%(count)s people you know have already joined|other": "%(count)s sulle tuttavat kasutajat on juba liitunud",
"We couldn't create your DM.": "Otsesuhtluse loomine ei õnnestunud.",
"Invited people will be able to read old messages.": "Kutse saanud kasutajad saavad lugeda vanu sõnumeid.",
"Consult first": "Pea esmalt nõu",
"Reset event store?": "Kas lähtestame sündmuste andmekogu?",
"Reset event store": "Lähtesta sündmuste andmekogu",
"Verify other login": "Verifitseeri muu sisselogimissessioon",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Suhtlen teise osapoolega %(transferTarget)s. <a>Saadan andmeid kasutajale %(transferee)s</a>",
"Message search initilisation failed": "Sõnumite otsingu alustamine ei õnnestunud",
"Invite messages are hidden by default. Click to show the message.": "Kutsed on vaikimisi peidetud. Sõnumi nägemiseks klõpsi.",
"Accept on your other login…": "Nõustu oma teise sisselogimissessiooniga…",
"Avatar": "Tunnuspilt",
"Verification requested": "Verifitseerimistaotlus on saadetud",
"%(count)s people you know have already joined|one": "%(count)s sulle tuttav kasutaja on juba liitunud",
"You most likely do not want to reset your event index store": "Pigem sa siiski ei taha lähtestada sündmuste andmekogu ja selle indeksit",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Kui sa siiski soovid seda teha, siis sinu sõnumeid me ei kustuta, aga seniks kuni sõnumite indeks taustal uuesti luuakse, toimib otsing aeglaselt ja ebatõhusalt",
"You can add more later too, including already existing ones.": "Sa võid ka hiljem siia luua uusi jututubasid või lisada olemasolevaid.",
"What are some things you want to discuss in %(spaceName)s?": "Mida sa sooviksid arutada %(spaceName)s kogukonnakeskuses?",
"Please choose a strong password": "Palun tee üks korralik salasõna",
"Use another login": "Pruugi muud kasutajakontot",
"Verify your identity to access encrypted messages and prove your identity to others.": "Tagamaks ligipääsu oma krüptitud sõnumitele ja tõestamaks oma isikut teistele kasutajatale, verifitseeri end.",
"Let's create a room for each of them.": "Teeme siis iga teema jaoks oma jututoa.",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Ilma verifitseerimiseta sul puudub ligipääs kõikidele oma sõnumitele ning teised ei näe sinu kasutajakontot usaldusväärsena."
}

View file

@ -311,5 +311,8 @@
"Your device resolution": "وضوح دستگاه شما",
"e.g. <CurrentPageURL>": "برای مثال <CurrentPageURL>",
"Every page you use in the app": "هر صفحه‌ی برنامه از که آن استفاده می‌کنید",
"e.g. %(exampleValue)s": "برای مثال %(exampleValue)s"
"e.g. %(exampleValue)s": "برای مثال %(exampleValue)s",
"Explore rooms": "کاوش اتاق",
"Sign In": "ورود",
"Create Account": "ایجاد اکانت"
}

View file

@ -3225,5 +3225,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Sur %(deviceName)s %(deviceId)s depuis %(ip)s",
"Check your devices": "Vérifiez vos appareils",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Une nouvelle session a accès à votre compte : %(name)s %(deviceID)s depuis %(ip)s",
"You have unverified logins": "Vous avez des sessions non-vérifiées"
"You have unverified logins": "Vous avez des sessions non-vérifiées",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Sans vérification vous naurez pas accès à tous vos messages et napparaîtrez pas comme de confiance aux autres.",
"Verify your identity to access encrypted messages and prove your identity to others.": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres.",
"Use another login": "Utiliser un autre identifiant",
"Please choose a strong password": "Merci de choisir un mot de passe fort",
"You can add more later too, including already existing ones.": "Vous pourrez en ajouter plus tard, y compris certains déjà existant.",
"Let's create a room for each of them.": "Créons un salon pour chacun dentre eux.",
"What are some things you want to discuss in %(spaceName)s?": "De quoi voulez vous discuter dans %(spaceName)s ?",
"Verification requested": "Vérification requise",
"Avatar": "Avatar",
"Verify other login": "Vérifier lautre connexion",
"Reset event store": "Réinitialiser le magasin dévénements",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Si vous le faites, notes quaucun de vos messages ne sera supprimé, mais la recherche pourrait être dégradée pendant quelques instants, le temps de recréer lindex",
"You most likely do not want to reset your event index store": "Il est probable que vous ne vouliez pas réinitialiser votre magasin dindex dévénements",
"Reset event store?": "Réinitialiser le magasin dévénements ?",
"Consult first": "Consulter dabord",
"Invited people will be able to read old messages.": "Les personnes invitées pourront lire les anciens messages.",
"We couldn't create your DM.": "Nous navons pas pu créer votre message direct.",
"Adding...": "Ajout…",
"Add existing rooms": "Ajouter des salons existants",
"%(count)s people you know have already joined|one": "%(count)s personne que vous connaissez en fait déjà partie",
"%(count)s people you know have already joined|other": "%(count)s personnes que vous connaissez en font déjà partie",
"Accept on your other login…": "Acceptez sur votre autre connexion…",
"Stop & send recording": "Terminer et envoyer lenregistrement",
"Record a voice message": "Enregistrer un message vocal",
"Invite messages are hidden by default. Click to show the message.": "Les messages dinvitation sont masqués par défaut. Cliquez pour voir le message.",
"Quick actions": "Actions rapides",
"Invite to just this room": "Inviter seulement dans ce salon",
"Warn before quitting": "Avertir avant de quitter",
"Message search initilisation failed": "Échec de linitialisation de la recherche de message",
"Manage & explore rooms": "Gérer et découvrir les salons",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consultation avec %(transferTarget)s. <a>Transfert à %(transferee)s</a>",
"unknown person": "personne inconnue",
"Share decryption keys for room history when inviting users": "Partager les clés de déchiffrement lors de linvitation dutilisateurs",
"Send and receive voice messages (in development)": "Envoyez et recevez des messages vocaux (en développement)",
"%(deviceId)s from %(ip)s": "%(deviceId)s depuis %(ip)s",
"Review to ensure your account is safe": "Vérifiez pour assurer la sécurité de votre compte",
"Sends the given message as a spoiler": "Envoie le message flouté"
}

View file

@ -3248,5 +3248,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Desde %(deviceName)s%(deviceId)s en %(ip)s",
"Check your devices": "Comproba os teus dispositivos",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Hai unha nova conexión á túa conta: %(name)s %(deviceID)s desde %(ip)s",
"You have unverified logins": "Tes conexións sen verificar"
"You have unverified logins": "Tes conexións sen verificar",
"Sends the given message as a spoiler": "Envía a mensaxe dada como un spoiler",
"Review to ensure your account is safe": "Revisa para asegurarte de que a túa conta está protexida",
"Share decryption keys for room history when inviting users": "Comparte chaves de descifrado para o historial da sala ao convidar usuarias",
"Warn before quitting": "Aviso antes de saír",
"Invite to just this room": "Convida só a esta sala",
"Stop & send recording": "Deter e enviar e a gravación",
"We couldn't create your DM.": "Non puidemos crear o teu MD.",
"Invited people will be able to read old messages.": "As persoas convidadas poderán ler as mensaxes antigas.",
"Reset event store?": "Restablecer almacenaxe do evento?",
"You most likely do not want to reset your event index store": "Probablemente non queiras restablecer o índice de almacenaxe do evento",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Se o fas, ten en conta que ningunha das mensaxes será eliminada, pero a experiencia de busca podería degradarse durante o tempo en que o índice volve a crearse",
"Avatar": "Avatar",
"Please choose a strong password": "Escolle un contrasinal forte",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifica a túa identidade para acceder a mensaxes cifradas e acreditar a túa identidade ante outras.",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Sen verificación, non terás acceso a tódalas túas mensaxes e poderías aparecer antes outras como non confiable.",
"%(deviceId)s from %(ip)s": "%(deviceId)s desde %(ip)s",
"Send and receive voice messages (in development)": "Enviar e recibir mensaxes de voz (en desenvolvemento)",
"unknown person": "persoa descoñecida",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consultando con %(transferTarget)s. <a>Transferir a %(transferee)s</a>",
"Manage & explore rooms": "Xestionar e explorar salas",
"Message search initilisation failed": "Fallo a inicialización da busca de mensaxes",
"Quick actions": "Accións rápidas",
"Invite messages are hidden by default. Click to show the message.": "As mensaxes de convite están agochadas por defecto. Preme para amosar a mensaxe.",
"Record a voice message": "Gravar mensaxe de voz",
"Accept on your other login…": "Acepta na túa outra sesión…",
"%(count)s people you know have already joined|other": "%(count)s persoas que coñeces xa se uniron",
"%(count)s people you know have already joined|one": "%(count)s persoa que coñeces xa se uniu",
"Add existing rooms": "Engadir salas existentes",
"Adding...": "Engadindo...",
"Consult first": "Preguntar primeiro",
"Reset event store": "Restablecer almacenaxe de eventos",
"Verify other login": "Verificar outra conexión",
"Verification requested": "Verificación solicitada",
"What are some things you want to discuss in %(spaceName)s?": "Sobre que temas queres conversar en %(spaceName)s?",
"Let's create a room for each of them.": "Crea unha sala para cada un deles.",
"You can add more later too, including already existing ones.": "Podes engadir máis posteriormente, incluíndo os xa existentes.",
"Use another login": "Usar outra conexión"
}

View file

@ -52,7 +52,7 @@
"Operation failed": "פעולה נכשלה",
"Search": "חפש",
"Custom Server Options": "הגדרות שרת מותאמות אישית",
"Dismiss": "שחרר",
"Dismiss": "התעלם",
"powered by Matrix": "מופעל ע\"י Matrix",
"Error": "שגיאה",
"Remove": "הסר",

View file

@ -585,5 +585,8 @@
"You cannot modify widgets in this room.": "आप इस रूम में विजेट्स को संशोधित नहीं कर सकते।",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ने कमरे में शामिल होने के लिए %(targetDisplayName)s के निमंत्रण को रद्द कर दिया।",
"User %(userId)s is already in the room": "उपयोगकर्ता %(userId)s पहले से ही रूम में है",
"The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।"
"The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।",
"Explore rooms": "रूम का अन्वेषण करें",
"Sign In": "साइन करना",
"Create Account": "खाता बनाएं"
}

View file

@ -4,5 +4,6 @@
"Failed to verify email address: make sure you clicked the link in the email": "Nismo u mogućnosti verificirati Vašu email adresu. Provjerite dali ste kliknuli link u mailu",
"The platform you're on": "Platforma na kojoj se nalazite",
"The version of %(brand)s": "Verzija %(brand)s",
"Your language of choice": "Izabrani jezik"
"Your language of choice": "Izabrani jezik",
"Dismiss": "Odbaci"
}

View file

@ -3243,5 +3243,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Innen: %(deviceName)s (%(deviceId)s), %(ip)s",
"Check your devices": "Ellenőrizze az eszközeit",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Új bejelentkezéssel hozzáférés történik a fiókjához: %(name)s (%(deviceID)s), %(ip)s",
"You have unverified logins": "Ellenőrizetlen bejelentkezései vannak"
"You have unverified logins": "Ellenőrizetlen bejelentkezései vannak",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Az ellenőrzés nélkül nem fér hozzá az összes üzenetéhez és mások számára megbízhatatlannak fog látszani.",
"Verify your identity to access encrypted messages and prove your identity to others.": "Ellenőrizze a személyazonosságát, hogy hozzáférjen a titkosított üzeneteihez és másoknak is bizonyítani tudja személyazonosságát.",
"Use another login": "Másik munkamenet használata",
"Please choose a strong password": "Kérem válasszon erős jelszót",
"You can add more later too, including already existing ones.": "Később is hozzáadhat többet, beleértve meglévőket is.",
"Let's create a room for each of them.": "Készítsünk szobát mindhez.",
"What are some things you want to discuss in %(spaceName)s?": "Mik azok amikről beszélni szeretne itt: %(spaceName)s?",
"Verification requested": "Hitelesítés kérés elküldve",
"Avatar": "Profilkép",
"Verify other login": "Másik munkamenet ellenőrzése",
"Reset event store": "Az esemény tárolót alaphelyzetbe állítása",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Ha ezt teszi, tudnia kell, hogy az üzenetek nem kerülnek törlésre de keresés nem lesz tökéletes amíg az indexek nem készülnek el újra",
"You most likely do not want to reset your event index store": "Az esemény index tárolót nagy valószínűséggel nem szeretné alaphelyzetbe állítani",
"Reset event store?": "Az esemény tárolót alaphelyzetbe állítja?",
"Consult first": "Kérjen először véleményt",
"Invited people will be able to read old messages.": "A meghívott személyek el tudják olvasni a régi üzeneteket.",
"We couldn't create your DM.": "Nem tudjuk elkészíteni a közvetlen üzenetét.",
"Adding...": "Hozzáadás…",
"Add existing rooms": "Létező szobák hozzáadása",
"%(count)s people you know have already joined|one": "%(count)s ismerős már csatlakozott",
"%(count)s people you know have already joined|other": "%(count)s ismerős már csatlakozott",
"Accept on your other login…": "Egy másik bejelentkezésében fogadta el…",
"Stop & send recording": "Megállít és a felvétel elküldése",
"Record a voice message": "Hang üzenet felvétele",
"Invite messages are hidden by default. Click to show the message.": "A meghívók alapesetben rejtve vannak. A megjelenítéshez kattintson.",
"Quick actions": "Gyors műveletek",
"Invite to just this room": "Meghívás csak ebbe a szobába",
"Warn before quitting": "Kilépés előtt figyelmeztet",
"Message search initilisation failed": "Üzenet keresés beállítása sikertelen",
"Manage & explore rooms": "Szobák kezelése és felderítése",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Egyeztetés vele: %(transferTarget)s. <a>Átadás ide: %(transferee)s</a>",
"unknown person": "ismeretlen személy",
"Share decryption keys for room history when inviting users": "Visszafejtéshez szükséges kulcsok megosztása a szoba előzményekhez felhasználók meghívásakor",
"Send and receive voice messages (in development)": "Hang üzenetek küldése és fogadása (fejlesztés alatt)",
"%(deviceId)s from %(ip)s": "%(deviceId)s innen: %(ip)s",
"Review to ensure your account is safe": "Tekintse át, hogy meggyőződjön arról, hogy a fiókja biztonságban van",
"Sends the given message as a spoiler": "A megadott üzenet szpojlerként küldése"
}

View file

@ -3248,5 +3248,42 @@
"Check your devices": "Controlla i tuoi dispositivi",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Una nuova sessione sta accedendo al tuo account: %(name)s (%(deviceID)s) al %(ip)s",
"You have unverified logins": "Hai accessi non verificati",
"Open": "Apri"
"Open": "Apri",
"Send and receive voice messages (in development)": "Invia e ricevi messaggi vocali (in sviluppo)",
"unknown person": "persona sconosciuta",
"Sends the given message as a spoiler": "Invia il messaggio come spoiler",
"Review to ensure your account is safe": "Controlla per assicurarti che l'account sia sicuro",
"%(deviceId)s from %(ip)s": "%(deviceId)s da %(ip)s",
"Share decryption keys for room history when inviting users": "Condividi le chiavi di decifrazione della cronologia della stanza quando inviti utenti",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consultazione con %(transferTarget)s. <a>Trasferisci a %(transferee)s</a>",
"Manage & explore rooms": "Gestisci ed esplora le stanze",
"Invite to just this room": "Invita solo in questa stanza",
"%(count)s people you know have already joined|other": "%(count)s persone che conosci sono già entrate",
"%(count)s people you know have already joined|one": "%(count)s persona che conosci è già entrata",
"Message search initilisation failed": "Inizializzazione ricerca messaggi fallita",
"Add existing rooms": "Aggiungi stanze esistenti",
"Warn before quitting": "Avvisa prima di uscire",
"Invited people will be able to read old messages.": "Le persone invitate potranno leggere i vecchi messaggi.",
"You most likely do not want to reset your event index store": "Probabilmente non hai bisogno di reinizializzare il tuo archivio indice degli eventi",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Se lo fai, ricorda che nessuno dei tuoi messaggi verrà eliminato, ma l'esperienza di ricerca potrà peggiorare per qualche momento mentre l'indice viene ricreato",
"Avatar": "Avatar",
"Verification requested": "Verifica richiesta",
"What are some things you want to discuss in %(spaceName)s?": "Quali sono le cose di cui vuoi discutere in %(spaceName)s?",
"Please choose a strong password": "Scegli una password robusta",
"Quick actions": "Azioni rapide",
"Invite messages are hidden by default. Click to show the message.": "I messaggi di invito sono nascosti in modo predefinito. Clicca per mostrare il messaggio.",
"Record a voice message": "Registra un messaggio vocale",
"Stop & send recording": "Ferma e invia la registrazione",
"Accept on your other login…": "Accetta nella tua altra sessione…",
"Adding...": "Aggiunta...",
"We couldn't create your DM.": "Non abbiamo potuto creare il tuo messaggio diretto.",
"Consult first": "Prima consulta",
"Reset event store?": "Reinizializzare l'archivio eventi?",
"Reset event store": "Reinizializza archivio eventi",
"Verify other login": "Verifica l'altra sessione",
"Let's create a room for each of them.": "Creiamo una stanza per ognuno di essi.",
"You can add more later too, including already existing ones.": "Puoi aggiungerne anche altri in seguito, inclusi quelli già esistenti.",
"Use another login": "Usa un altro accesso",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifica la tua identità per accedere ai messaggi cifrati e provare agli altri che sei tu.",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Senza la verifica, non avrai accesso a tutti i tuoi messaggi e potresti apparire agli altri come non fidato."
}

View file

@ -580,5 +580,8 @@
"%(displayName)s cancelled verification.": ".i la'o zoi. %(displayName)s .zoi co'u co'a lacri",
"Decrypt %(text)s": "nu facki le du'u mifra la'o zoi. %(text)s .zoi",
"Download %(text)s": "nu kibycpa la'o zoi. %(text)s .zoi",
"Download this file": "nu kibycpa le vreji"
"Download this file": "nu kibycpa le vreji",
"Explore rooms": "nu facki le du'u ve zilbe'i",
"Create Account": "nu pa re'u co'a jaspu",
"Dismiss": "nu mipri"
}

View file

@ -2,7 +2,7 @@
"Confirm": "Sentem",
"Analytics": "Tiselḍin",
"Error": "Tuccḍa",
"Dismiss": "Agi",
"Dismiss": "Agwi",
"OK": "IH",
"Permission Required": "Tasiregt tlaq",
"Continue": "Kemmel",

View file

@ -1666,5 +1666,6 @@
"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!": "경고: 키 검증 실패! 제공된 키인 \"%(fingerprint)s\"가 사용자 %(userId)s와 %(deviceId)s 세션의 서명 키인 \"%(fprint)s\"와 일치하지 않습니다. 이는 통신이 탈취되고 있는 중일 수도 있다는 뜻입니다!",
"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.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다."
"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": "계정 만들기"
}

View file

@ -1184,7 +1184,7 @@
"Manage integrations": "Valdyti integracijas",
"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.",
"Invalid theme schema.": "Klaidinga temos schema.",
"Error downloading theme information.": "Klaida parsisiunčiant temos informaciją.",
"Error downloading theme information.": "Klaida atsisiunčiant temos informaciją.",
"Theme added!": "Tema pridėta!",
"Custom theme URL": "Pasirinktinės temos URL",
"Add theme": "Pridėti temą",
@ -2091,5 +2091,16 @@
"Successfully restored %(sessionCount)s keys": "Sėkmingai atkurti %(sessionCount)s raktai",
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Įspėjimas: Jūsų asmeniniai duomenys (įskaitant šifravimo raktus) vis dar yra saugomi šiame seanse. Išvalykite juos, jei baigėte naudoti šį seansą, arba norite prisijungti prie kitos paskyros.",
"Reason (optional)": "Priežastis (nebūtina)",
"Reason: %(reason)s": "Priežastis: %(reason)s"
"Reason: %(reason)s": "Priežastis: %(reason)s",
"Already have an account? <a>Sign in here</a>": "Jau turite paskyrą? <a>Prisijunkite čia</a>",
"Host account on": "Kurti paskyrą serveryje",
"Forgotten your password?": "Pamiršote savo slaptažodį?",
"Homeserver": "Serveris",
"New? <a>Create account</a>": "Naujas vartotojas? <a>Sukurkite paskyrą</a>",
"Forgot password?": "Pamiršote slaptažodį?",
"Preparing to download logs": "Ruošiamasi parsiųsti žurnalus",
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Jūs galite naudoti serverio parinktis, norėdami prisijungti prie kitų Matrix serverių, nurodydami kitą serverio URL. Tai leidžia jums naudoti Element su egzistuojančia paskyra kitame serveryje.",
"Server Options": "Serverio Parinktys",
"Your homeserver": "Jūsų serveris",
"Download logs": "Parsisiųsti žurnalus"
}

View file

@ -300,7 +300,7 @@
"You need to be logged in.": "Tev ir jāpierakstās.",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Jūsu epasta adrese nav piesaistīta nevienam Matrix ID šajā bāzes serverī.",
"You seem to be in a call, are you sure you want to quit?": "Izskatās, ka atrodies zvana režīmā. Vai tiešām vēlies iziet?",
"You seem to be uploading files, are you sure you want to quit?": "Izskatās, ka šobrīd augšuplādē failus. Vai tiešām vēlies iziet?",
"You seem to be uploading files, are you sure you want to quit?": "Izskatās, ka šobrīd notiek failu augšupielāde. Vai tiešām vēlaties iziet?",
"Sun": "Sv.",
"Mon": "P.",
"Tue": "O.",
@ -747,7 +747,7 @@
"Unhide Preview": "Rādīt priekšskatījumu",
"Unable to join network": "Neizdodas pievienoties tīklam",
"Sorry, your browser is <b>not</b> able to run %(brand)s.": "Atvaino, diemžēl tavs tīmekļa pārlūks <b>nespēj</b> darbināt %(brand)s.",
"Uploaded on %(date)s by %(user)s": "Augšuplādēja %(user)s %(date)s",
"Uploaded on %(date)s by %(user)s": "Augšupielādēja %(user)s %(date)s",
"Messages in group chats": "Ziņas grupas čatos",
"Yesterday": "Vakardien",
"Error encountered (%(errorDetail)s).": "Gadījās kļūda (%(errorDetail)s).",
@ -1559,5 +1559,27 @@
"Verify this session": "Verificēt šo sesiju",
"You signed in to a new session without verifying it:": "Jūs pierakstījāties jaunā sesijā, neveicot tās verifikāciju:",
"You're already in a call with this person.": "Jums jau notiek zvans ar šo personu.",
"Already in call": "Notiek zvans"
"Already in call": "Notiek zvans",
"%(deviceId)s from %(ip)s": "%(deviceId)s no %(ip)s",
"%(count)s people you know have already joined|other": "%(count)s pazīstami cilvēki ir jau pievienojusies",
"%(count)s people you know have already joined|one": "%(count)s pazīstama persona ir jau pievienojusies",
"Saving...": "Saglabā…",
"%(count)s members|one": "%(count)s dalībnieks",
"Save Changes": "Saglabāt izmaiņas",
"%(count)s messages deleted.|other": "%(count)s ziņas ir dzēstas.",
"%(count)s messages deleted.|one": "%(count)s ziņa ir dzēsta.",
"Welcome to <name/>": "Laipni lūdzam uz <name/>",
"Room name": "Istabas nosaukums",
"%(count)s members|other": "%(count)s dalībnieki",
"Room List": "Istabu saraksts",
"Send as message": "Nosūtīt kā ziņu",
"%(brand)s URL": "%(brand)s URL",
"Send a message…": "Nosūtīt ziņu…",
"Send a reply…": "Nosūtīt atbildi…",
"Room version": "Istabas versija",
"Room list": "Istabu saraksts",
"Failed to set topic": "Neizdevās iestatīt tematu",
"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)"
}

View file

@ -127,5 +127,8 @@
"Failed to change settings": "സജ്ജീകരണങ്ങള്‍ മാറ്റുന്നവാന്‍ സാധിച്ചില്ല",
"View Source": "സോഴ്സ് കാണുക",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "നിങ്ങളുടെ ഇപ്പോളത്തെ ബ്രൌസര്‍ റയട്ട് പ്രവര്‍ത്തിപ്പിക്കാന്‍ പൂര്‍ണമായും പര്യാപത്മല്ല. പല ഫീച്ചറുകളും പ്രവര്‍ത്തിക്കാതെയിരിക്കാം. ഈ ബ്രൌസര്‍ തന്നെ ഉപയോഗിക്കണമെങ്കില്‍ മുന്നോട്ട് പോകാം. പക്ഷേ നിങ്ങള്‍ നേരിടുന്ന പ്രശ്നങ്ങള്‍ നിങ്ങളുടെ ഉത്തരവാദിത്തത്തില്‍ ആയിരിക്കും!",
"Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു..."
"Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു...",
"Explore rooms": "മുറികൾ കണ്ടെത്തുക",
"Sign In": "പ്രവേശിക്കുക",
"Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക"
}

View file

@ -1 +1,6 @@
{}
{
"Explore rooms": "Өрөөнүүд үзэх",
"Sign In": "Нэвтрэх",
"Create Account": "Хэрэглэгч үүсгэх",
"Dismiss": "Орхих"
}

View file

@ -1507,5 +1507,479 @@
"This will end the conference for everyone. Continue?": "Dette vil avslutte konferansen for alle. Fortsett?",
"End conference": "Avslutt konferanse",
"You're already in a call with this person.": "Du er allerede i en samtale med denne personen.",
"Already in call": "Allerede i en samtale"
"Already in call": "Allerede i en samtale",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Er du sikke på at du vil fjerne '%(roomName)s' fra %(groupId)s?",
"Burundi": "Burundi",
"Burkina Faso": "Burkina Faso",
"Bulgaria": "Bulgaria",
"Brunei": "Brunei",
"Brazil": "Brazil",
"Botswana": "Botswana",
"Bolivia": "Bolivia",
"Bhutan": "Bhutan",
"Bermuda": "Bermuda",
"Benin": "Benin",
"Belize": "Belize",
"Belarus": "Hviterussland",
"Barbados": "Barbados",
"Bangladesh": "Bangladesh",
"Bahrain": "Bahrain",
"Bahamas": "Bahamas",
"Azerbaijan": "Azerbaijan",
"Austria": "Østerrike",
"Australia": "Australia",
"Aruba": "Aruba",
"Armenia": "Armenia",
"Argentina": "Argentina",
"Antigua & Barbuda": "Antigua og Barbuda",
"Antarctica": "Antarktis",
"Anguilla": "Anguilla",
"Angola": "Angola",
"Andorra": "Andorra",
"Algeria": "Algeria",
"Albania": "Albania",
"Åland Islands": "Åland",
"Afghanistan": "Afghanistan",
"United Kingdom": "Storbritannia",
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Din hjemmeserver kunne ikke nås, og kan derfor ikke logge deg inn. Vennligst prøv igjen. Hvis dette fortsetter, kontakt administratoren til din hjemmeserver.",
"Only continue if you trust the owner of the server.": "Fortsett kun om du stoler på eieren av serveren.",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Denne handlingen krever tilgang til standard identitetsserver <server /> for å kunne validere en epostaddresse eller telefonnummer, men serveren har ikke bruksvilkår.",
"Too Many Calls": "For mange samtaler",
"Call failed because webcam or microphone could not be accessed. Check that:": "Samtalen mislyktes fordi du fikk ikke tilgang til webkamera eller mikrofon. Sørg for at:",
"Unable to access webcam / microphone": "Ingen tilgang til webkamera / mikrofon",
"The call was answered on another device.": "Samtalen ble besvart på en annen enhet.",
"The call could not be established": "Samtalen kunne ikke etableres",
"The other party declined the call.": "Den andre parten avviste samtalen.",
"Call Declined": "Samtale avvist",
"Click the button below to confirm adding this phone number.": "Klikk knappen nedenfor for å bekrefte dette telefonnummeret.",
"Single Sign On": "Single Sign On",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Bekreft dette telefonnummeret ved å bruke Single Sign On for å bevise din identitet.",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Befrekt denne e-postadressen ved å bruke Single Sign On for å bevise din identitet.",
"Show stickers button": "Vis klistremerkeknappen",
"Recently visited rooms": "Nylig besøkte rom",
"Windows": "Vinduer",
"Abort": "Avbryt",
"You have unverified logins": "Du har uverifiserte pålogginger",
"Check your devices": "Sjekk enhetene dine",
"Record a voice message": "Send en stemmebeskjed",
"Edit devices": "Rediger enheter",
"Homeserver": "Hjemmetjener",
"Edit Values": "Rediger verdier",
"Add existing room": "Legg til et eksisterende rom",
"Spell check dictionaries": "Stavesjekk-ordbøker",
"Invite to this space": "Inviter til dette området",
"Send message": "Send melding",
"Cookie Policy": "Infokapselretningslinjer",
"Invite to %(roomName)s": "Inviter til %(roomName)s",
"Resume": "Fortsett",
"Avatar": "Profilbilde",
"A confirmation email has been sent to %(emailAddress)s": "En bekreftelses-E-post har blitt sendt til %(emailAddress)s",
"Suggested Rooms": "Foreslåtte rom",
"Welcome %(name)s": "Velkommen, %(name)s",
"Upgrade to %(hostSignupBrand)s": "Oppgrader til %(hostSignupBrand)s",
"Verification requested": "Verifisering ble forespurt",
"%(count)s members|one": "%(count)s medlem",
"Removing...": "Fjerner …",
"No results found": "Ingen resultater ble funnet",
"Public space": "Offentlig område",
"Private space": "Privat område",
"Support": "Support",
"What projects are you working on?": "Hvilke prosjekter jobber du på?",
"Suggested": "Anbefalte",
"%(deviceId)s from %(ip)s": "%(deviceId)s fra %(ip)s",
"Accept on your other login…": "Aksepter på din andre pålogging …",
"Value:": "Verdi:",
"Leave Space": "Forlat området",
"View dev tools": "Vis utviklerverktøy",
"Saving...": "Lagrer …",
"Save Changes": "Lagre endringer",
"Verify other login": "Verifiser en annen pålogging",
"You don't have permission": "Du har ikke tillatelse",
"%(count)s rooms|other": "%(count)s rom",
"%(count)s rooms|one": "%(count)s rom",
"Invite by username": "Inviter etter brukernavn",
"Delete": "Slett",
"Your public space": "Ditt offentlige område",
"Your private space": "Ditt private område",
"Invite to %(spaceName)s": "Inviter til %(spaceName)s",
"%(count)s members|other": "%(count)s medlemmer",
"Random": "Tilfeldig",
"unknown person": "ukjent person",
"Public": "Offentlig",
"Private": "Privat",
"Click to copy": "Klikk for å kopiere",
"Share invite link": "Del invitasjonslenke",
"Leave space": "Forlat området",
"Warn before quitting": "Advar før avslutning",
"Quick actions": "Hurtigvalg",
"Screens": "Skjermer",
"%(count)s people you know have already joined|other": "%(count)s personer du kjenner har allerede blitt med",
"Add existing rooms": "Legg til eksisterende rom",
"Don't want to add an existing room?": "Vil du ikke legge til et eksisterende rom?",
"Create a new room": "Opprett et nytt rom",
"Adding...": "Legger til …",
"Settings Explorer": "Innstillingsutforsker",
"Value": "Verdi",
"Setting:": "Innstilling:",
"Caution:": "Advarsel:",
"Level": "Nivå",
"Privacy Policy": "Personvern",
"You should know": "Du bør vite",
"Room name": "Rommets navn",
"Skip for now": "Hopp over for nå",
"Creating rooms...": "Oppretter rom …",
"Share %(name)s": "Del %(name)s",
"Just me": "Bare meg selv",
"Inviting...": "Inviterer …",
"Please choose a strong password": "Vennligst velg et sterkt passord",
"New? <a>Create account</a>": "Er du ny her? <a>Opprett en konto</a>",
"Use another login": "Bruk en annen pålogging",
"Use Security Key or Phrase": "Bruk sikkerhetsnøkkel eller -frase",
"Use Security Key": "Bruk sikkerhetsnøkkel",
"Upgrade private room": "Oppgrader privat rom",
"Upgrade public room": "Oppgrader offentlig rom",
"Decline All": "Avslå alle",
"Enter Security Key": "Skriv inn sikkerhetsnøkkel",
"Germany": "Tyskland",
"Malta": "Malta",
"Uruguay": "Uruguay",
"Community settings": "Fellesskapsinnstillinger",
"Youre all caught up": "Du har lest deg opp på alt det nye",
"Remember this": "Husk dette",
"Move right": "Gå til høyre",
"Notify the whole room": "Varsle hele rommet",
"Got an account? <a>Sign in</a>": "Har du en konto? <a>Logg på</a>",
"You created this room.": "Du opprettet dette rommet.",
"Security Phrase": "Sikkerhetsfrase",
"Start a Conversation": "Start en samtale",
"Open dial pad": "Åpne nummerpanelet",
"Message deleted on %(date)s": "Meldingen ble slettet den %(date)s",
"Approve": "Godkjenn",
"Create community": "Opprett fellesskap",
"Already have an account? <a>Sign in here</a>": "Har du allerede en konto? <a>Logg på</a>",
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s eller %(usernamePassword)s",
"That username already exists, please try another.": "Det brukernavnet finnes allerede, vennligst prøv et annet et",
"New here? <a>Create an account</a>": "Er du ny her? <a>Opprett en konto</a>",
"Now, let's help you get started": "Nå, la oss hjelpe deg med å komme i gang",
"Forgot password?": "Glemt passord?",
"Enter email address": "Legg inn e-postadresse",
"Enter phone number": "Skriv inn telefonnummer",
"Please enter the code it contains:": "Vennligst skriv inn koden den inneholder:",
"Token incorrect": "Sjetongen er feil",
"A text message has been sent to %(msisdn)s": "En SMS har blitt sendt til %(msisdn)s",
"Open the link in the email to continue registration.": "Åpne lenken i E-posten for å fortsette registreringen.",
"This room is public": "Dette rommet er offentlig",
"Move left": "Gå til venstre",
"Take a picture": "Ta et bilde",
"Hold": "Hold",
"Enter Security Phrase": "Skriv inn sikkerhetsfrase",
"Security Key": "Sikkerhetsnøkkel",
"Invalid Security Key": "Ugyldig sikkerhetsnøkkel",
"Wrong Security Key": "Feil sikkerhetsnøkkel",
"About homeservers": "Om hjemmetjenere",
"New Recovery Method": "Ny gjenopprettingsmetode",
"Generate a Security Key": "Generer en sikkerhetsnøkkel",
"Confirm your Security Phrase": "Bekreft sikkerhetsfrasen din",
"Your Security Key": "Sikkerhetsnøkkelen din",
"Repeat your Security Phrase...": "Gjenta sikkerhetsfrasen din",
"Set up with a Security Key": "Sett opp med en sikkerhetsnøkkel",
"Use app": "Bruk app",
"Learn more": "Lær mer",
"Use app for a better experience": "Bruk appen for en bedre opplevelse",
"Continue with %(provider)s": "Fortsett med %(provider)s",
"This address is already in use": "Denne adressen er allerede i bruk",
"<a>In reply to</a> <pill>": "<a>Som svar på</a> <pill>",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sendret navnet sitt %(count)s ganger",
"%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)sfikk sin invitasjon trukket tilbake",
"%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)sfikk sine invitasjoner trukket tilbake",
"%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)savslo invitasjonen sin",
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sforlot og ble med igjen",
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sforlot og ble med igjen",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sble med og forlot igjen",
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sble med og forlot igjen",
"Information": "Informasjon",
"Add rooms to this community": "Legg til rom i dette fellesskapet",
"%(name)s cancelled verifying": "%(name)s avbrøt verifiseringen",
"You cancelled verifying %(name)s": "Du avbrøt verifiseringen av %(name)s",
"Invalid file%(extra)s": "Ugyldig fil%(extra)s",
"Failed to ban user": "Mislyktes i å bannlyse brukeren",
"Room settings": "Rominnstillinger",
"Show files": "Vis filer",
"Not encrypted": "Ikke kryptert",
"About": "Om",
"Widgets": "Komponenter",
"Room Info": "Rominfo",
"Favourited": "Favorittmerket",
"Forget Room": "Glem rommet",
"Show previews of messages": "Vis forhåndsvisninger av meldinger",
"Invalid URL": "Ugyldig URL",
"Continuing without email": "Fortsetter uten E-post",
"Are you sure you want to sign out?": "Er du sikker på at du vil logge av?",
"Transfer": "Overfør",
"Invite by email": "Inviter gjennom E-post",
"Waiting for partner to confirm...": "Venter på at partneren skal bekrefte …",
"Report a bug": "Rapporter en feil",
"Comment": "Kommentar",
"Add comment": "Legg til kommentar",
"Active Widgets": "Aktive moduler",
"Create a room in %(communityName)s": "Opprett et rom i %(communityName)s",
"Reason (optional)": "Årsak (valgfritt)",
"Send %(count)s invites|one": "Send %(count)s invitasjon",
"Send %(count)s invites|other": "Send %(count)s invitasjoner",
"Add another email": "Legg til en annen E-postadresse",
"%(count)s results|one": "%(count)s resultat",
"%(count)s results|other": "%(count)s resultater",
"Start a new chat": "Start en ny chat",
"Custom Tag": "Egendefinert merkelapp",
"Explore public rooms": "Utforsk offentlige rom",
"Explore community rooms": "Utforsk samfunnsrom",
"Invite to this community": "Inviter til dette fellesskapet",
"Verify the link in your inbox": "Verifiser lenken i innboksen din",
"Bridges": "Broer",
"Privacy": "Personvern",
"Reject all %(invitedRooms)s invites": "Avslå alle %(invitedRooms)s-invitasjoner",
"Upgrade Room Version": "Oppgrader romversjon",
"You cancelled verification.": "Du avbrøt verifiseringen.",
"Ask %(displayName)s to scan your code:": "Be %(displayName)s om å skanne koden:",
"Role": "Rolle",
"Failed to deactivate user": "Mislyktes i å deaktivere brukeren",
"Accept all %(invitedRooms)s invites": "Aksepter alle %(invitedRooms)s-invitasjoner",
"<not supported>": "<ikke støttet>",
"Custom theme URL": "URL-en til et selvvalgt tema",
"not ready": "ikke klar",
"ready": "klar",
"Algorithm:": "Algoritme:",
"Backing up %(sessionsRemaining)s keys...": "Sikkerhetskopierer %(sessionsRemaining)s nøkler …",
"Away": "Borte",
"Start chat": "Start chat",
"Show Widgets": "Vis moduler",
"Hide Widgets": "Skjul moduler",
"Unknown for %(duration)s": "Ukjent i %(duration)s",
"Update %(brand)s": "Oppdater %(brand)s",
"You are currently ignoring:": "Du ignorerer for øyeblikket:",
"Unknown caller": "Ukjent oppringer",
"Dial pad": "Nummerpanel",
"%(name)s on hold": "%(name)s står på vent",
"Fill Screen": "Fyll skjermen",
"Voice Call": "Taleanrop",
"Video Call": "Videoanrop",
"sends confetti": "sender konfetti",
"System font name": "Systemskrifttypenavn",
"Use a system font": "Bruk en systemskrifttype",
"Waiting for answer": "Venter på svar",
"Call in progress": "Anrop pågår",
"Channel: <channelLink/>": "Kanal: <channelLink/>",
"Enable desktop notifications": "Aktiver skrivebordsvarsler",
"Don't miss a reply": "Ikke gå glipp av noen svar",
"Help us improve %(brand)s": "Hjelp oss å forbedre %(brand)s",
"Unknown App": "Ukjent app",
"Short keyboard patterns are easy to guess": "Korte tastatur mønstre er lett å gjette",
"This is similar to a commonly used password": "Dette ligner på et passord som er brukt mye",
"Predictable substitutions like '@' instead of 'a' don't help very much": "Forutsigbar erstatninger som @ istedet for a hjelper ikke mye",
"Reversed words aren't much harder to guess": "Ord som er skrevet baklengs er vanskeligere å huske.",
"All-uppercase is almost as easy to guess as all-lowercase": "Bare store bokstaver er nesten like enkelt å gjette som bare små bokstaver",
"Capitalization doesn't help very much": "Store bokstaver er ikke spesielt nyttig",
"Use a longer keyboard pattern with more turns": "Bruke et lengre og mer uventet tastatur mønster",
"No need for symbols, digits, or uppercase letters": "Ikke nødvendig med symboler, sifre eller bokstaver",
"See images posted to this room": "Se bilder som er lagt ut i dette rommet",
"%(senderName)s declined the call.": "%(senderName)s avslo oppringingen.",
"(an error occurred)": "(en feil oppstod)",
"(connection failed)": "(tilkobling mislyktes)",
"Change the topic of this room": "Endre dette rommets tema",
"Effects": "Effekter",
"Zimbabwe": "Zimbabwe",
"Yemen": "Jemen",
"Zambia": "Zambia",
"Western Sahara": "Vest-Sahara",
"Wallis & Futuna": "Wallis og Futuna",
"Venezuela": "Venezuela",
"Vietnam": "Vietnam",
"Vatican City": "Vatikanstaten",
"Vanuatu": "Vanuatu",
"Uzbekistan": "Usbekistan",
"United Arab Emirates": "De forente arabiske emirater",
"Ukraine": "Ukraina",
"U.S. Virgin Islands": "De amerikanske jomfruøyene",
"Uganda": "Uganda",
"Tuvalu": "Tuvalu",
"Turks & Caicos Islands": "Turks- og Caicosøyene",
"Turkmenistan": "Turkmenistan",
"Tunisia": "Tunis",
"Turkey": "Tyrkia",
"Trinidad & Tobago": "Trinidad og Tobago",
"Tonga": "Tonga",
"Tokelau": "Tokelau",
"Togo": "Togo",
"Timor-Leste": "Timor-Leste",
"Thailand": "Thailand",
"Tanzania": "Tanzania",
"Tajikistan": "Tadsjikistan",
"Taiwan": "Taiwan",
"São Tomé & Príncipe": "São Tomé og Príncipe",
"Syria": "Syria",
"Sweden": "Sverige",
"Switzerland": "Sveits",
"Swaziland": "Swaziland",
"Svalbard & Jan Mayen": "Svalbard og Jan Mayen",
"Suriname": "Surinam",
"Sudan": "Sudan",
"St. Vincent & Grenadines": "St. Vincent og Grenadinene",
"St. Kitts & Nevis": "St. Kitts og Nevis",
"St. Helena": "St. Helena",
"Sri Lanka": "Sri Lanka",
"Spain": "Spania",
"South Sudan": "Sør-Sudan",
"South Korea": "Syd-Korea",
"Somalia": "Somalia",
"South Africa": "Sør-Afrika",
"Solomon Islands": "Solomonøyene",
"Slovenia": "Slovenia",
"Slovakia": "Slovakia",
"Sint Maarten": "Sint Maarten",
"Singapore": "Singapore",
"Sierra Leone": "Sierra Leone",
"Seychelles": "Seyschellene",
"Serbia": "Serbia",
"Saudi Arabia": "Saudi-Arabia",
"Senegal": "Senegal",
"San Marino": "San Marino",
"Samoa": "Samoa",
"Réunion": "Réunion",
"Rwanda": "Rwanda",
"Russia": "Russland",
"Qatar": "Qatar",
"Romania": "Romania",
"Puerto Rico": "Puerto Rico",
"Portugal": "Portugal",
"Poland": "Polen",
"Pitcairn Islands": "Pitcairn-øyene",
"Philippines": "Filippinene",
"Peru": "Peru",
"Papua New Guinea": "Papua New Guinea",
"Paraguay": "Paraguay",
"Panama": "Panama",
"Palestine": "Palestina",
"Pakistan": "Pakistan",
"Palau": "Palau",
"Oman": "Oman",
"Norway": "Norge",
"Northern Mariana Islands": "Northern Mariana Islands",
"North Korea": "Nord-Korea",
"Norfolk Island": "Norfolkøyene",
"Niue": "Niue",
"Nigeria": "Nigeria",
"Niger": "Niger",
"New Zealand": "New Zealand",
"Nicaragua": "Nicaragua",
"New Caledonia": "New Caledonia",
"Netherlands": "Nederland",
"Nepal": "Nepal",
"Nauru": "Nauru",
"Namibia": "Namibia",
"Myanmar": "Myanmar",
"Mozambique": "Mosambik",
"Morocco": "Marokko",
"Montenegro": "Montenegro",
"Montserrat": "Montserrat",
"Mongolia": "Mongolia",
"Monaco": "Monaco",
"Moldova": "Moldova",
"Micronesia": "Mikronesia",
"Mexico": "Mexico",
"Mayotte": "Mayotte",
"Mauritius": "Mauritius",
"Mauritania": "Mauretania",
"Martinique": "Martinique",
"Marshall Islands": "Marshall Islands",
"Maldives": "Maldivene",
"Mali": "Mali",
"Malaysia": "Malaysia",
"Malawi": "Malawi",
"Madagascar": "Madagaskar",
"Macedonia": "Nord-Makedonia",
"Macau": "Macau",
"Luxembourg": "Luxemburg",
"Lithuania": "Litauen",
"Liechtenstein": "Liechtenstein",
"Libya": "Libya",
"Liberia": "Liberia",
"Lesotho": "Lesotho",
"Lebanon": "Libanon",
"Latvia": "Latvia",
"Laos": "Laos",
"Kyrgyzstan": "Kirgistan",
"Kuwait": "Kuwait",
"Kosovo": "Kosovo",
"Kiribati": "Kiribati",
"Kazakhstan": "Kasakstan",
"Kenya": "Kenya",
"Jamaica": "Jamaica",
"Isle of Man": "Man",
"Iceland": "Island",
"Hungary": "Ungarn",
"Hong Kong": "Hong Kong",
"Honduras": "Honduras",
"Haiti": "Haiti",
"Guinea-Bissau": "Guinea-Bissau",
"Guyana": "Guyana",
"Guinea": "Guinea",
"Guernsey": "Guernsey",
"Guatemala": "Guatemala",
"Guam": "Guam",
"Guadeloupe": "Guadeloupe",
"Grenada": "Grenada",
"Greece": "Hellas",
"Greenland": "Grønland",
"Gibraltar": "Gibraltar",
"Ghana": "Ghana",
"Georgia": "Georgia",
"Gambia": "Gambia",
"Gabon": "Gabon",
"French Southern Territories": "De franske sørterritoriene",
"French Polynesia": "Fransk polynesia",
"French Guiana": "Fransk Guyana",
"France": "Frankrike",
"Finland": "Finnland",
"Fiji": "Fiji",
"Falkland Islands": "Falklandsøyene",
"Faroe Islands": "Færøyene",
"Ethiopia": "Etiopia",
"Estonia": "Estland",
"Eritrea": "Eritrea",
"Equatorial Guinea": "Ekvatorial-Guinea",
"El Salvador": "El Salvador",
"Egypt": "Egypt",
"Ecuador": "Ecuador",
"Dominican Republic": "Dominikanske republikk",
"Djibouti": "Djibouti",
"Dominica": "Dominica",
"Denmark": "Danmark",
"Côte dIvoire": "Elfenbenskysten",
"Czech Republic": "Tsjekkia",
"Cyprus": "Kypros",
"Curaçao": "Curaçao",
"Cuba": "Kuba",
"Colombia": "Colombia",
"Comoros": "Komorene",
"Cocos (Keeling) Islands": "Cocos- (Keeling) øyene",
"Christmas Island": "Juløya",
"China": "Kina",
"Chad": "Tsjad",
"Chile": "Chile",
"Central African Republic": "Sentralafrikanske republikk",
"Cayman Islands": "Caymanøyene",
"Caribbean Netherlands": "Karibisk Nederland",
"Cape Verde": "Kapp Verde",
"Canada": "Canada",
"Cameroon": "Kamerun",
"Cambodia": "Kambodsja",
"British Virgin Islands": "De britiske jomfruøyer",
"British Indian Ocean Territory": "Britiske havområder i det indiske hav",
"Bouvet Island": "Bouvetøya",
"Bosnia": "Bosnia",
"Croatia": "Kroatia",
"Costa Rica": "Costa Rica",
"Cook Islands": "Cook-øyene",
"All keys backed up": "Alle nøkler er sikkerhetskopiert",
"Secret storage:": "Hemmelig lagring:"
}

View file

@ -198,7 +198,7 @@
"Join Room": "Gesprek toetreden",
"%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.",
"Jump to first unread message.": "Spring naar het eerste ongelezen bericht.",
"Labs": "Experimenteel",
"Labs": "Labs",
"Last seen": "Laatst gezien",
"Leave room": "Gesprek verlaten",
"%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",
@ -632,7 +632,7 @@
"The version of %(brand)s": "De versie van %(brand)s",
"Your language of choice": "De door jou gekozen taal",
"Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie je eventueel gebruikt",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Of je de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt",
"Your homeserver's URL": "De URL van je homeserver",
"<a>In reply to</a> <pill>": "<a>Als antwoord op</a> <pill>",
"This room is not public. You will not be able to rejoin without an invite.": "Dit is geen openbaar gesprek. Slechts op uitnodiging zult u opnieuw kunnen toetreden.",
@ -1255,7 +1255,7 @@
"The homeserver may be unavailable or overloaded.": "De homeserver is mogelijk onbereikbaar of overbelast.",
"You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
"You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of je de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt",
"Replying With Files": "Beantwoorden met bestanden",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Het is momenteel niet mogelijk met een bestand te antwoorden. Wil je dit bestand uploaden zonder te antwoorden?",
"The file '%(fileName)s' failed to upload.": "Het bestand %(fileName)s kon niet geüpload worden.",
@ -1758,7 +1758,7 @@
"Cancelling…": "Bezig met annuleren…",
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "In %(brand)s ontbreken enige modulen vereist voor het veilig lokaal bewaren van versleutelde berichten. Wilt u deze functie uittesten, compileer dan een aangepaste versie van %(brand)s Desktop <nativeLink>die de zoekmodulen bevat</nativeLink>.",
"This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Deze sessie <b>maakt geen back-ups van uw sleutels</b>, maar u beschikt over een reeds bestaande back-up waaruit u kunt herstellen en waaraan u nieuwe sleutels vanaf nu kunt toevoegen.",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliseer uw ervaring met experimentele functies. <a>Klik hier voor meer informatie</a>.",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliseer uw ervaring met experimentele labs functies. <a>Lees verder</a>.",
"Cross-signing": "Kruiselings ondertekenen",
"Your key share request has been sent - please check your other sessions for key share requests.": "Uw sleuteldeelverzoek is verstuurd - controleer de sleuteldeelverzoeken op uw andere sessies.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Sleuteldeelverzoeken worden automatisch naar andere sessies verstuurd. Als u op uw andere sessies het sleuteldeelverzoek geweigerd of genegeerd hebt, kunt u hier klikken op de sleutels voor deze sessie opnieuw aan te vragen.",
@ -3134,5 +3134,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Van %(deviceName)s (%(deviceId)s) op %(ip)s",
"Check your devices": "Controleer uw apparaten",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Een nieuwe login heeft toegang tot uw account: %(name)s (%(deviceID)s) op %(ip)s",
"You have unverified logins": "U heeft ongeverifieerde logins"
"You have unverified logins": "U heeft ongeverifieerde logins",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Zonder verifiëren heeft u geen toegang tot al uw berichten en kan u als onvertrouwd aangemerkt staan bij anderen.",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en uw identiteit te bewijzen voor anderen.",
"Use another login": "Gebruik andere login",
"Please choose a strong password": "Kies een sterk wachtwoord",
"You can add more later too, including already existing ones.": "U kunt er later nog meer toevoegen, inclusief al bestaande gesprekken.",
"Let's create a room for each of them.": "Laten we voor elk een los gesprek maken.",
"What are some things you want to discuss in %(spaceName)s?": "Wat wilt u allemaal bespreken in %(spaceName)s?",
"Verification requested": "Verificatieverzocht",
"Avatar": "Avatar",
"Verify other login": "Verifieer andere login",
"You most likely do not want to reset your event index store": "U wilt waarschijnlijk niet uw gebeurtenisopslag-index resetten",
"Reset event store?": "Gebeurtenisopslag resetten?",
"Reset event store": "Gebeurtenisopslag resetten",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Als u dit wilt, let op uw berichten worden niet verwijderd, zal het zoeken tijdelijk minder goed werken terwijl we uw index opnieuw opbouwen",
"Consult first": "Eerst overleggen",
"Invited people will be able to read old messages.": "Uitgenodigde personen kunnen de oude berichten lezen.",
"We couldn't create your DM.": "We konden uw DM niet aanmaken.",
"Adding...": "Toevoegen...",
"Add existing rooms": "Bestaande gesprekken toevoegen",
"%(count)s people you know have already joined|one": "%(count)s persoon die u kent is al geregistreerd",
"%(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",
"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",
"Warn before quitting": "Waarschuwen voordat u afsluit",
"Message search initilisation failed": "Zoeken in berichten opstarten is mislukt",
"Manage & explore rooms": "Beheer & ontdek gesprekken",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Overleggen met %(transferTarget)s. <a>Verstuur naar %(transferee)s</a>",
"unknown person": "onbekend persoon",
"Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd",
"Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)",
"%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s",
"Review to ensure your account is safe": "Controleer om u te verzekeren dat uw account veilig is",
"Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler"
}

View file

@ -62,7 +62,7 @@
"Server error": "Error servidor",
"Single Sign On": "Autentificacion unica",
"Confirm": "Confirmar",
"Dismiss": "Far desaparéisser",
"Dismiss": "Refusar",
"OK": "Dacòrdi",
"Continue": "Contunhar",
"Go Back": "En arrièr",
@ -118,7 +118,7 @@
"Incoming call": "Sonada entranta",
"Accept": "Acceptar",
"Start": "Començament",
"Cancelling…": "Anullacion...",
"Cancelling…": "Anullacion",
"Fish": "Pes",
"Butterfly": "Parpalhòl",
"Tree": "Arborescéncia",
@ -338,5 +338,13 @@
"Esc": "Escap",
"Enter": "Entrada",
"Space": "Espaci",
"End": "Fin"
"End": "Fin",
"Explore rooms": "Percórrer las salas",
"Create Account": "Crear un compte",
"Click the button below to confirm adding this email address.": "Clicatz sus lo boton aicí dejós per confirmar l'adicion de l'adreça e-mail.",
"Confirm adding email": "Confirmar l'adicion de l'adressa e-mail",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Confirmatz l'adicion d'aquela adreça e-mail en utilizant l'autentificacion unica per provar la vòstra identitat.",
"Use Single Sign On to continue": "Utilizar l'autentificacion unica (SSO) per contunhar",
"This phone number is already in use": "Aquel numèro de telefòn es ja utilizat",
"This email address is already in use": "Aquela adreça e-mail es ja utilizada"
}

View file

@ -1277,8 +1277,8 @@
"Enable desktop notifications for this session": "Włącz powiadomienia na pulpicie dla tej sesji",
"Enable audible notifications for this session": "Włącz powiadomienia dźwiękowe dla tej sesji",
"Direct Messages": "Wiadomości bezpośrednie",
"Create Account": "Utwórz konto",
"Sign In": "Zaloguj się",
"Create Account": "Stwórz konto",
"Sign In": "Zaloguj",
"a few seconds ago": "kilka sekund temu",
"%(num)s minutes ago": "%(num)s minut temu",
"%(num)s hours ago": "%(num)s godzin temu",

View file

@ -569,5 +569,8 @@
"Try using turn.matrix.org": "Tente utilizar turn.matrix.org",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Quer esteja a usar o %(brand)s num dispositivo onde o touch é o mecanismo de entrada primário",
"Whether you're using %(brand)s as an installed Progressive Web App": "Quer esteja a usar o %(brand)s como uma Progressive Web App (PWA)",
"Your user agent": "O seu user agent"
"Your user agent": "O seu user agent",
"Explore rooms": "Explorar rooms",
"Sign In": "Iniciar sessão",
"Create Account": "Criar conta"
}

View file

@ -1175,7 +1175,7 @@
"Learn More": "Saiba mais",
"Sign In or Create Account": "Faça login ou crie uma conta",
"Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.",
"Create Account": "Criar conta",
"Create Account": "Criar Conta",
"Sign In": "Entrar",
"Custom (%(level)s)": "Personalizado (%(level)s)",
"Messages": "Mensagens",

View file

@ -70,5 +70,9 @@
"Add to community": "Adăugați la comunitate",
"Failed to invite the following users to %(groupId)s:": "Nu a putut fi invitat următorii utilizatori %(groupId)s",
"Failed to invite users to community": "Nu a fost posibilă invitarea utilizatorilor la comunitate",
"Failed to invite users to %(groupId)s": "Nu a fost posibilă invitarea utilizatorilor la %(groupId)s"
"Failed to invite users to %(groupId)s": "Nu a fost posibilă invitarea utilizatorilor la %(groupId)s",
"Explore rooms": "Explorează camerele",
"Sign In": "Autentificare",
"Create Account": "Înregistare",
"Dismiss": "Închide"
}

View file

@ -3169,5 +3169,46 @@
"Decrypted event source": "Расшифрованный исходный код",
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s комната и %(numSpaces)s пространств",
"%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s комнат и %(numSpaces)s пространств",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "Если вы не можете найти комнату, попросите приглашение или <a>создайте новую комнату</a>."
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "Если вы не можете найти комнату, попросите приглашение или <a>создайте новую комнату</a>.",
"Values at explicit levels in this room:": "Значения уровня чувствительности в этой комнате:",
"Values at explicit levels:": "Значения уровня чувствительности:",
"Values at explicit levels in this room": "Значения уровня чувствительности в этой комнате",
"Values at explicit levels": "Значения уровня чувствительности",
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "Мы создадим комнаты для каждого из них. Вы можете добавить ещё больше позже, включая уже существующие.",
"What projects are you working on?": "Над какими проектами вы работаете?",
"Invite by username": "Пригласить по имени пользователя",
"Make sure the right people have access. You can invite more later.": "Убедитесь, что правильные люди имеют доступ. Вы можете пригласить больше людей позже.",
"Invite your teammates": "Пригласите своих товарищей по команде",
"Inviting...": "Приглашение…",
"Failed to invite the following users to your space: %(csvUsers)s": "Не удалось пригласить следующих пользователей в ваше пространство: %(csvUsers)s",
"Me and my teammates": "Я и мои товарищи по команде",
"A private space for you and your teammates": "Приватное пространство для вас и ваших товарищей по команде",
"A private space to organise your rooms": "Приватное пространство для организации ваших комнат",
"Just me": "Только я",
"Make sure the right people have access to %(name)s": "Убедитесь, что правильные люди имеют доступ к %(name)s",
"Who are you working with?": "С кем ты работаешь?",
"Go to my first room": "Перейти в мою первую комнату",
"It's just you at the moment, it will be even better with others.": "Сейчас здесь только ты, с другими будет ещё лучше.",
"Share %(name)s": "Поделиться %(name)s",
"Creating rooms...": "Создание комнат…",
"Skip for now": "Пропустить сейчас",
"Failed to create initial space rooms": "Не удалось создать первоначальные комнаты пространства",
"Room name": "Название комнаты",
"Support": "Поддержка",
"Random": "Случайный",
"Welcome to <name/>": "Добро пожаловать в <name/>",
"Your server does not support showing space hierarchies.": "Ваш сервер не поддерживает отображение пространственных иерархий.",
"Add existing rooms & spaces": "Добавить существующие комнаты и пространства",
"Private space": "Приватное пространство",
"Public space": "Публичное пространство",
"<inviter/> invites you": "<inviter/> пригласил(а) тебя",
"Search names and description": "Искать имена и описание",
"You may want to try a different search or check for typos.": "Вы можете попробовать другой поиск или проверить опечатки.",
"No results found": "Результаты не найдены",
"Mark as suggested": "Отметить как рекомендуется",
"Mark as not suggested": "Отметить как не рекомендуется",
"Removing...": "Удаление…",
"Failed to remove some rooms. Try again later": "Не удалось удалить несколько комнат. Попробуйте позже",
"%(count)s rooms and 1 space|one": "%(count)s комната и одно пространство",
"%(count)s rooms and 1 space|other": "%(count)s комнат и одно пространство"
}

View file

@ -27,5 +27,7 @@
"Your homeserver's URL": "URL domačega strežnika",
"End": "Konec",
"Use default": "Uporabi privzeto",
"Change": "Sprememba"
"Change": "Sprememba",
"Explore rooms": "Raziščite sobe",
"Create Account": "Registracija"
}

View file

@ -874,7 +874,7 @@
"Incompatible Database": "Bazë të dhënash e Papërputhshme",
"Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar",
"Unable to load! Check your network connectivity and try again.": "Sarrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.",
"Forces the current outbound group session in an encrypted room to be discarded": "",
"Forces the current outbound group session in an encrypted room to be discarded": "E detyron të hidhet tej sesionin e tanishëm outbound grupi në një dhomë të fshehtëzuar",
"Delete Backup": "Fshije Kopjeruajtjen",
"Unable to load key backup status": "Sarrihet të ngarkohet gjendje kopjeruajtjeje kyçesh",
"Backup version: ": "Version kopjeruajtjeje: ",
@ -3240,5 +3240,37 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Nga %(deviceName)s (%(deviceId)s) te %(ip)s",
"Check your devices": "Kontrolloni pajisjet tuaja",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Në llogarinë tuaj po hyhet nga një palë kredenciale të reja: %(name)s (%(deviceID)s) te %(ip)s",
"You have unverified logins": "Keni kredenciale të erifikuar"
"You have unverified logins": "Keni kredenciale të erifikuar",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Pa e verifikuar, sdo të mund të hyni te krejt mesazhet tuaja dhe mund të dukeni jo i besueshëm për të tjerët.",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifikoni identitetin tuaj që të hyhet në mesazhe të fshehtëzuar dhe tu provoni të tjerëve identitetin tuaj.",
"Use another login": "Përdorni të tjera kredenciale hyrjesh",
"Please choose a strong password": "Ju lutemi, zgjidhni një fjalëkalim të fuqishëm",
"You can add more later too, including already existing ones.": "Mund të shtoni edhe të tjera më vonë, përfshi ato ekzistueset tashmë.",
"Let's create a room for each of them.": "Le të krijojmë një dhomë për secilën prej tyre.",
"What are some things you want to discuss in %(spaceName)s?": "Cilat janë disa nga gjërat që doni të diskutoni në %(spaceName)s?",
"Verification requested": "U kërkua verifikim",
"Avatar": "Avatar",
"Verify other login": "Verifikoni kredencialet e tjera për hyrje",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Nëse e bëni, ju lutemi, kini parasysh se sdo të fshihet asnjë prej mesazheve tuaja, por puna me kërkimet mund të bjerë, për ca çaste, teksa rikrijohet treguesi",
"Consult first": "Konsultohu së pari",
"Invited people will be able to read old messages.": "Personat e ftuar do të jenë në gjendje të lexojnë mesazhe të vjetër.",
"We couldn't create your DM.": "Se krijuam dot DM-në tuaj.",
"Adding...": "Po shtohet…",
"Add existing rooms": "Shtoni dhoma ekzistuese",
"%(count)s people you know have already joined|one": "%(count)s person që e njihni është bërë pjesë tashmë",
"%(count)s people you know have already joined|other": "%(count)s persona që i njihni janë bërë pjesë tashmë",
"Stop & send recording": "Ndale & dërgo incizimin",
"Record a voice message": "Incizoni një mesazh zanor",
"Invite messages are hidden by default. Click to show the message.": "Mesazhet e ftesave, si parazgjedhje, janë të fshehur. Klikoni që të shfaqet mesazhi.",
"Quick actions": "Veprime të shpejta",
"Invite to just this room": "Ftoje thjesht te kjo dhomë",
"Warn before quitting": "Sinjalizo përpara daljes",
"Message search initilisation failed": "Dështoi gatitje kërkimi mesazhesh",
"Manage & explore rooms": "Administroni & eksploroni dhoma",
"unknown person": "person i panjohur",
"Sends the given message as a spoiler": "E dërgon mesazhin e dhënë si <em>spoiler</em>",
"Share decryption keys for room history when inviting users": "Ndani me përdorues kyçe shfshehtëzimi, kur ftohen përdorues",
"Send and receive voice messages (in development)": "Dërgoni dhe merrni mesazhe zanorë (në zhvillim)",
"%(deviceId)s from %(ip)s": "%(deviceId)s prej %(ip)s",
"Review to ensure your account is safe": "Shqyrtojeni për tu siguruar se llogaria është e parrezik"
}

View file

@ -58,5 +58,6 @@
"Failed to invite users to the room:": "Nije uspelo pozivanje korisnika u sobu:",
"You need to be logged in.": "Morate biti prijavljeni",
"You need to be able to invite users to do that.": "Mora vam biti dozvoljeno da pozovete korisnike kako bi to uradili.",
"Failed to send request.": "Slanje zahteva nije uspelo."
"Failed to send request.": "Slanje zahteva nije uspelo.",
"Create Account": "Napravite nalog"
}

View file

@ -3180,5 +3180,41 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "Från %(deviceName)s %(deviceId)s på %(ip)s",
"Check your devices": "Kolla dina enheter",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "En ny inloggning kommer åt ditt konto: %(name)s %(deviceID)s på %(ip)s",
"You have unverified logins": "Du har overifierade inloggningar"
"You have unverified logins": "Du har overifierade inloggningar",
"%(count)s people you know have already joined|other": "%(count)s personer du känner har redan gått med",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Om du gör det, observera att inga av dina meddelanden kommer att raderas, men din sökupplevelse kommer att degraderas en stund medans registret byggs upp igen",
"What are some things you want to discuss in %(spaceName)s?": "Vad är några saker du vill diskutera i %(spaceName)s?",
"You can add more later too, including already existing ones.": "Du kan lägga till flera senare också, inklusive redan existerande.",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Tillfrågar %(transferTarget)s. <a>%(transferTarget)sÖverför till %(transferee)s</a>",
"Review to ensure your account is safe": "Granska för att försäkra dig om att ditt konto är säkert",
"%(deviceId)s from %(ip)s": "%(deviceId)s från %(ip)s",
"Send and receive voice messages (in development)": "Skicka och ta emot röstmeddelanden (under utveckling)",
"unknown person": "okänd person",
"Warn before quitting": "Varna innan avslutning",
"Invite to just this room": "Bjud in till bara det här rummet",
"Invite messages are hidden by default. Click to show the message.": "Inbjudningsmeddelanden är dolda som förval. Klicka för att visa meddelandet.",
"Record a voice message": "Spela in ett röstmeddelande",
"Stop & send recording": "Stoppa och skicka inspelning",
"Accept on your other login…": "Acceptera på din andra inloggning…",
"%(count)s people you know have already joined|one": "%(count)s person du känner har redan gått med",
"Quick actions": "Snabbhandlingar",
"Add existing rooms": "Lägg till existerande rum",
"Adding...": "Lägger till…",
"We couldn't create your DM.": "Vi kunde inte skapa ditt DM.",
"Reset event store": "Återställ händelselagring",
"Invited people will be able to read old messages.": "Inbjudna personer kommer att kunna läsa gamla meddelanden.",
"Reset event store?": "Återställ händelselagring?",
"You most likely do not want to reset your event index store": "Du vill troligen inte återställa din händelseregisterlagring",
"Consult first": "Tillfråga först",
"Verify other login": "Verifiera annan inloggning",
"Avatar": "Avatar",
"Let's create a room for each of them.": "Låt oss skapa ett rum för varje.",
"Verification requested": "Verifiering begärd",
"Sends the given message as a spoiler": "Skickar det angivna meddelandet som en spoiler",
"Manage & explore rooms": "Hantera och utforska rum",
"Message search initilisation failed": "Initialisering av meddelandesökning misslyckades",
"Please choose a strong password": "Vänligen välj ett starkt lösenord",
"Use another login": "Använd annan inloggning",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verifiera din identitet för att komma åt krypterade meddelanden och bevisa din identitet för andra.",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Om du inte verifierar så kommer du inte ha åtkomst till alla dina meddelanden och kan synas som ej betrodd för andra."
}

View file

@ -179,5 +179,7 @@
"Mar": "மார்ச்",
"Apr": "ஏப்ரல்",
"May": "மே",
"Jun": "ஜூன்"
"Jun": "ஜூன்",
"Explore rooms": "அறைகளை ஆராயுங்கள்",
"Create Account": "உங்கள் கணக்கை துவங்குங்கள்"
}

View file

@ -26,7 +26,7 @@
"Results from DuckDuckGo": "ผลจาก DuckDuckGo",
"%(brand)s version:": "เวอร์ชัน %(brand)s:",
"Cancel": "ยกเลิก",
"Dismiss": "ไม่สนใจ",
"Dismiss": "ปิด",
"Mute": "เงียบ",
"Notifications": "การแจ้งเตือน",
"Operation failed": "การดำเนินการล้มเหลว",
@ -378,5 +378,10 @@
"Unable to fetch notification target list": "ไม่สามารถรับรายชื่ออุปกรณ์แจ้งเตือน",
"Quote": "อ้างอิง",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "การแสดงผลของโปรแกรมอาจผิดพลาด ฟังก์ชันบางอย่างหรือทั้งหมดอาจไม่ทำงานในเบราว์เซอร์ปัจจุบันของคุณ หากคุณต้องการลองดำเนินการต่อ คุณต้องรับมือกับปัญหาที่อาจจะเกิดขึ้นด้วยตัวคุณเอง!",
"Checking for an update...": "กำลังตรวจหาอัปเดต..."
"Checking for an update...": "กำลังตรวจหาอัปเดต...",
"Explore rooms": "สำรวจห้อง",
"Sign In": "ลงชื่อเข้า",
"Create Account": "สร้างบัญชี",
"Add Email Address": "เพิ่มที่อยู่อีเมล",
"Confirm": "ยืนยัน"
}

View file

@ -293,5 +293,7 @@
"Enable URL previews by default for participants in this room": "Bật mặc định xem trước nội dung đường link cho mọi người trong phòng",
"Room Colour": "Màu phòng chat",
"Enable widget screenshots on supported widgets": "Bật widget chụp màn hình cho các widget có hỗ trợ",
"Sign In": "Đăng nhập"
"Sign In": "Đăng nhập",
"Explore rooms": "Khám phá phòng chat",
"Create Account": "Tạo tài khoản"
}

View file

@ -1443,5 +1443,7 @@
"Terms of service not accepted or the identity server is invalid.": "Dienstvoorwoardn nie anveird, of den identiteitsserver is oungeldig.",
"Enter a new identity server": "Gift e nieuwen identiteitsserver in",
"Remove %(email)s?": "%(email)s verwydern?",
"Remove %(phone)s?": "%(phone)s verwydern?"
"Remove %(phone)s?": "%(phone)s verwydern?",
"Explore rooms": "Gesprekkn ountdekkn",
"Create Account": "Account anmoakn"
}

View file

@ -3251,5 +3251,42 @@
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "從 %(deviceName)s (%(deviceId)s) 於 %(ip)s",
"Check your devices": "檢查您的裝置",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "新登入正在存取您的帳號:%(name)s (%(deviceID)s) 於 %(ip)s",
"You have unverified logins": "您有未驗證的登入"
"You have unverified logins": "您有未驗證的登入",
"unknown person": "不明身份的人",
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "與 %(transferTarget)s 進行協商。<a>轉讓至 %(transferee)s</a>",
"Message search initilisation failed": "訊息搜尋初始化失敗",
"Invite to just this room": "邀請到此聊天室",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "如果這樣做,請注意,您的任何訊息都不會被刪除,但是在重新建立索引的同時,搜索體驗可能會降低片刻",
"Let's create a room for each of them.": "讓我們為每個主題建立一個聊天室吧。",
"Verify your identity to access encrypted messages and prove your identity to others.": "驗證您的身份來存取已加密的訊息並對其他人證明您的身份。",
"Sends the given message as a spoiler": "將指定訊息以劇透傳送",
"Review to ensure your account is safe": "請審閱以確保您的帳號安全",
"%(deviceId)s from %(ip)s": "從 %(ip)s 而來的 %(deviceId)s",
"Send and receive voice messages (in development)": "傳送與接收語音訊息(開發中)",
"Share decryption keys for room history when inviting users": "邀請使用者時分享聊天室歷史紀錄的解密金鑰",
"Manage & explore rooms": "管理與探索聊天室",
"Warn before quitting": "離開前警告",
"Quick actions": "快速動作",
"Invite messages are hidden by default. Click to show the message.": "邀請訊息預設隱藏。點擊以顯示訊息。",
"Record a voice message": "錄製語音訊息",
"Stop & send recording": "停止並傳送錄音",
"Accept on your other login…": "接受您的其他登入……",
"%(count)s people you know have already joined|other": "%(count)s 個您認識的人已加入",
"%(count)s people you know have already joined|one": "%(count)s 個您認識的人已加入",
"Add existing rooms": "新增既有聊天室",
"Adding...": "正在新增……",
"We couldn't create your DM.": "我們無法建立您的直接訊息。",
"Invited people will be able to read old messages.": "被邀請的人將能閱讀舊訊息。",
"Consult first": "先協商",
"Reset event store?": "重設活動儲存?",
"You most likely do not want to reset your event index store": "您很可能不想重設您的活動索引儲存",
"Reset event store": "重設活動儲存",
"Verify other login": "驗證其他登入",
"Avatar": "大頭貼",
"Verification requested": "已請求驗證",
"What are some things you want to discuss in %(spaceName)s?": "您想在 %(spaceName)s 中討論什麼?",
"You can add more later too, including already existing ones.": "您稍後可以新增更多內容,包含既有的。",
"Please choose a strong password": "請選擇強密碼",
"Use another login": "使用其他登入",
"Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "未經驗證,您將無法存取您的所有訊息,且可能不被其他人信任。"
}

View file

@ -128,6 +128,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new ReloadOnChangeController(),
},
"feature_dnd": {
isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_voice_messages": {
isFeature: true,
displayName: _td("Send and receive voice messages (in development)"),
@ -226,6 +232,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"doNotDisturb": {
supportedLevels: [SettingLevel.DEVICE],
default: false,
},
"mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT],
default: [],

View file

@ -51,6 +51,12 @@ export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
const MAX_SUGGESTED_ROOMS = 20;
const getLastViewedRoomsStorageKey = (space?: Room) => {
const lastViewRooms = "mx_last_viewed_rooms";
const homeSpace = "home_space";
return `${lastViewRooms}_${space?.roomId || homeSpace}`;
}
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
result[room.isSpaceRoom() ? 0 : 1].push(room);
@ -111,6 +117,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
// view last selected room from space
const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace));
if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: roomId,
context_switch: true,
});
} else if (space) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
context_switch: true,
});
} else {
defaultDispatcher.dispatch({
action: "view_home_page",
});
}
// persist space selected
if (space) {
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
@ -421,11 +448,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
};
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => {
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees
const oldTags = lastEvent.getContent()?.tags;
const newTags = ev.getContent()?.tags;
const oldTags = lastEvent?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {};
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
this.onRoomUpdate(room);
}
@ -488,6 +515,21 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
case "view_room": {
const room = this.matrixClient?.getRoom(payload.room_id);
// Don't auto-switch rooms when reacting to a context-switch
// as this is not helpful and can create loops of rooms/space switching
if (payload.context_switch) break;
// persist last viewed room from a space
// Don't save if the room is a space room. This would cause a problem:
// When switching to a space home, we first view that room and
// only after that we switch to that space. This causes us to
// save the space home to be the last viewed room in the home
// space.
if (room && !room.isSpaceRoom()) {
window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id);
}
if (room?.getMyMembership() === "join") {
if (room.isSpaceRoom()) {
this.setActiveSpace(room);
@ -525,6 +567,26 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.notificationStateMap.set(key, state);
return state;
}
// traverse space tree with DFS calling fn on each space including the given root one
public traverseSpace(
spaceId: string,
fn: (roomId: string) => void,
includeRooms = false,
parentPath?: Set<string>,
) {
if (parentPath && parentPath.has(spaceId)) return; // prevent cycles
fn(spaceId);
const newPath = new Set(parentPath).add(spaceId);
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
if (includeRooms) {
childRooms.forEach(r => fn(r.roomId));
}
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
}
}
export default class SpaceStore {

View file

@ -0,0 +1,80 @@
/*
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 {AsyncStoreWithClient} from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import {ActionPayload} from "../dispatcher/payloads";
import {VoiceRecording} from "../voice/VoiceRecording";
interface IState {
recording?: VoiceRecording;
}
export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
private static internalInstance: VoiceRecordingStore;
public constructor() {
super(defaultDispatcher, {});
}
/**
* Gets the active recording instance, if any.
*/
public get activeRecording(): VoiceRecording | null {
return this.state.recording;
}
public static get instance(): VoiceRecordingStore {
if (!VoiceRecordingStore.internalInstance) {
VoiceRecordingStore.internalInstance = new VoiceRecordingStore();
}
return VoiceRecordingStore.internalInstance;
}
protected async onAction(payload: ActionPayload): Promise<void> {
// Nothing to do, but we're required to override the function
return;
}
/**
* Starts a new recording if one isn't already in progress. Note that this simply
* creates a recording instance - whether or not recording is actively in progress
* can be seen via the VoiceRecording class.
* @returns {VoiceRecording} The recording.
*/
public startRecording(): VoiceRecording {
if (!this.matrixClient) throw new Error("Cannot start a recording without a MatrixClient");
if (this.state.recording) throw new Error("A recording is already in progress");
const recording = new VoiceRecording(this.matrixClient);
// noinspection JSIgnoredPromiseFromCall - we can safely run this async
this.updateState({recording});
return recording;
}
/**
* Disposes of the current recording, no matter the state of it.
* @returns {Promise<void>} Resolves when complete.
*/
public disposeRecording(): Promise<void> {
if (this.state.recording) {
this.state.recording.destroy(); // stops internally
}
return this.updateState({recording: null});
}
}

View file

@ -22,6 +22,7 @@ import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
interface IState {}
@ -47,7 +48,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
// This will include highlights from the previous version of the room internally
const globalState = new SummarizedNotificationState();
for (const room of this.matrixClient.getVisibleRooms()) {
globalState.add(this.getRoomState(room));
if (VisibilityProvider.instance.isRoomVisible(room)) {
globalState.add(this.getRoomState(room));
}
}
return globalState;
}

View file

@ -28,12 +28,22 @@ export class SpaceWatcher {
private activeSpace: Room = SpaceStore.instance.activeSpace;
constructor(private store: RoomListStoreClass) {
this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state
this.updateFilter(); // get the filter into a consistent state
store.addFilter(this.filter);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
}
private onSelectedSpaceUpdated = (activeSpace) => {
this.filter.updateSpace(this.activeSpace = activeSpace);
private onSelectedSpaceUpdated = (activeSpace: Room) => {
this.activeSpace = activeSpace;
this.updateFilter();
};
private updateFilter = () => {
if (this.activeSpace) {
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
});
}
this.filter.updateSpace(this.activeSpace);
};
}

View file

@ -42,10 +42,16 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
private onStoreUpdate = async (): Promise<void> => {
const beforeRoomIds = this.roomIds;
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
// clone the set as it may be mutated by the space store internally
this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space));
if (setHasDiff(beforeRoomIds, this.roomIds)) {
this.emit(FILTER_CHANGED);
// XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a
// tags transition seem to be ignored, so refire in the next tick to work around it
setImmediate(() => {
this.emit(FILTER_CHANGED);
});
}
};

126
src/utils/Singleflight.ts Normal file
View file

@ -0,0 +1,126 @@
/*
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 {EnhancedMap} from "./maps";
// Inspired by https://pkg.go.dev/golang.org/x/sync/singleflight
const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
/**
* Access class to get a singleflight context. Singleflights execute a
* function exactly once, unless instructed to forget about a result.
*
* Typically this is used to de-duplicate an action, such as a save button
* being pressed, without having to track state internally for an operation
* already being in progress. This doesn't expose a flag which can be used
* to disable a button, however it would be capable of returning a Promise
* from the first call.
*
* The result of the function call is cached indefinitely, just in case a
* second call comes through late. There are various functions named "forget"
* to have the cache be cleared of a result.
*
* Singleflights in our usecase are tied to an instance of something, combined
* with a string key to differentiate between multiple possible actions. This
* means that a "save" key will be scoped to the instance which defined it and
* not leak between other instances. This is done to avoid having to concatenate
* variables to strings to essentially namespace the field, for most cases.
*/
export class Singleflight {
private constructor() {
}
/**
* A void marker to help with returning a value in a singleflight context.
* If your code doesn't return anything, return this instead.
*/
public static Void = Symbol("void");
/**
* Acquire a singleflight context.
* @param {Object} instance An instance to associate the context with. Can be any object.
* @param {string} key A string key relevant to that instance to namespace under.
* @returns {SingleflightContext} Returns the context to execute the function.
*/
public static for(instance: Object, key: string): SingleflightContext {
if (!instance || !key) throw new Error("An instance and key must be supplied");
return new SingleflightContext(instance, key);
}
/**
* Forgets all results for a given instance.
* @param {Object} instance The instance to forget about.
*/
public static forgetAllFor(instance: Object) {
keyMap.delete(instance);
}
/**
* Forgets all cached results for all instances. Intended for use by tests.
*/
public static forgetAll() {
for (const k of keyMap.keys()) {
keyMap.remove(k);
}
}
}
class SingleflightContext {
public constructor(private instance: Object, private key: string) {
}
/**
* Forget this particular instance and key combination, discarding the result.
*/
public forget() {
const map = keyMap.get(this.instance);
if (!map) return;
map.remove(this.key);
if (!map.size) keyMap.remove(this.instance);
}
/**
* Execute a function. If a result is already known, that will be returned instead
* of executing the provided function. However, if no result is known then the function
* will be called, with its return value cached. The function must return a value
* other than `undefined` - take a look at Singleflight.Void if you don't have a return
* to make.
*
* Note that this technically allows the caller to provide a different function each time:
* this is largely considered a bad idea and should not be done. Singleflights work off the
* premise that something needs to happen once, so duplicate executions will be ignored.
*
* For ideal performance and behaviour, functions which return promises are preferred. If
* a function is not returning a promise, it should return as soon as possible to avoid a
* second call potentially racing it. The promise returned by this function will be that
* of the first execution of the function, even on duplicate calls.
* @param {Function} fn The function to execute.
* @returns The recorded value.
*/
public do<T>(fn: () => T): T {
const map = keyMap.getOrCreate(this.instance, new EnhancedMap<string, unknown>());
// We have to manually getOrCreate() because we need to execute the fn
let val = <T>map.get(this.key);
if (val === undefined) {
val = fn();
map.set(this.key, val);
}
return val;
}
}

View file

@ -20,17 +20,30 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import CallMediaHandler from "../CallMediaHandler";
import {SimpleObservable} from "matrix-widget-api";
import {clamp} from "../utils/numbers";
import EventEmitter from "events";
import {IDestroyable} from "../utils/IDestroyable";
import {Singleflight} from "../utils/Singleflight";
const CHANNELS = 1; // stereo isn't important
const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus.
const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files.
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
export interface IRecordingUpdate {
waveform: number[]; // floating points between 0 (low) and 1 (high).
timeSeconds: number; // float
}
export class VoiceRecorder {
export enum RecordingState {
Started = "started",
EndingSoon = "ending_soon", // emits an object with a single numerical value: secondsLeft
Ended = "ended",
Uploading = "uploading",
Uploaded = "uploaded",
}
export class VoiceRecording extends EventEmitter implements IDestroyable {
private recorder: Recorder;
private recorderContext: AudioContext;
private recorderSource: MediaStreamAudioSourceNode;
@ -43,6 +56,7 @@ export class VoiceRecorder {
private observable: SimpleObservable<IRecordingUpdate>;
public constructor(private client: MatrixClient) {
super();
}
private async makeRecorder() {
@ -124,7 +138,7 @@ export class VoiceRecorder {
return this.mxc;
}
private tryUpdateLiveData = (ev: AudioProcessingEvent) => {
private processAudioUpdate = (ev: AudioProcessingEvent) => {
if (!this.recording) return;
// The time domain is the input to the FFT, which means we use an array of the same
@ -150,6 +164,19 @@ export class VoiceRecorder {
waveform: translatedData,
timeSeconds: ev.playbackTime,
});
// Now that we've updated the data/waveform, let's do a time check. We don't want to
// go horribly over the limit. We also emit a warning state if needed.
const secondsLeft = TARGET_MAX_LENGTH - ev.playbackTime;
if (secondsLeft <= 0) {
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
this.stop();
} else if (secondsLeft <= TARGET_WARN_TIME_LEFT) {
Singleflight.for(this, "ending_soon").do(() => {
this.emit(RecordingState.EndingSoon, {secondsLeft});
return Singleflight.Void;
});
}
};
public async start(): Promise<void> {
@ -164,33 +191,44 @@ export class VoiceRecorder {
}
this.observable = new SimpleObservable<IRecordingUpdate>();
await this.makeRecorder();
this.recorderProcessor.addEventListener("audioprocess", this.tryUpdateLiveData);
this.recorderProcessor.addEventListener("audioprocess", this.processAudioUpdate);
await this.recorder.start();
this.recording = true;
this.emit(RecordingState.Started);
}
public async stop(): Promise<Uint8Array> {
if (!this.recording) {
throw new Error("No recording to stop");
}
return Singleflight.for(this, "stop").do(async () => {
if (!this.recording) {
throw new Error("No recording to stop");
}
// Disconnect the source early to start shutting down resources
this.recorderSource.disconnect();
await this.recorder.stop();
// Disconnect the source early to start shutting down resources
this.recorderSource.disconnect();
await this.recorder.stop();
// close the context after the recorder so the recorder doesn't try to
// connect anything to the context (this would generate a warning)
await this.recorderContext.close();
// close the context after the recorder so the recorder doesn't try to
// connect anything to the context (this would generate a warning)
await this.recorderContext.close();
// Now stop all the media tracks so we can release them back to the user/OS
this.recorderStream.getTracks().forEach(t => t.stop());
// Now stop all the media tracks so we can release them back to the user/OS
this.recorderStream.getTracks().forEach(t => t.stop());
// Finally do our post-processing and clean up
this.recording = false;
this.recorderProcessor.removeEventListener("audioprocess", this.tryUpdateLiveData);
await this.recorder.close();
// Finally do our post-processing and clean up
this.recording = false;
this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate);
await this.recorder.close();
this.emit(RecordingState.Ended);
return this.buffer;
return this.buffer;
});
}
public destroy() {
// noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here
this.stop();
this.removeAllListeners();
Singleflight.forgetAllFor(this);
}
public async upload(): Promise<string> {
@ -200,13 +238,15 @@ export class VoiceRecorder {
if (this.mxc) return this.mxc;
this.emit(RecordingState.Uploading);
this.mxc = await this.client.uploadContent(new Blob([this.buffer], {
type: "audio/ogg",
}), {
onlyContentUri: false, // to stop the warnings in the console
}).then(r => r['content_uri']);
this.emit(RecordingState.Uploaded);
return this.mxc;
}
}
window.mxVoiceRecorder = VoiceRecorder;
window.mxVoiceRecorder = VoiceRecording;

115
test/Singleflight-test.ts Normal file
View file

@ -0,0 +1,115 @@
/*
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 {Singleflight} from "../src/utils/Singleflight";
describe('Singleflight', () => {
afterEach(() => {
Singleflight.forgetAll();
});
it('should throw for bad context variables', () => {
const permutations: [Object, string][] = [
[null, null],
[{}, null],
[null, "test"],
];
for (const p of permutations) {
try {
Singleflight.for(p[0], p[1]);
// noinspection ExceptionCaughtLocallyJS
throw new Error("failed to fail: " + JSON.stringify(p));
} catch (e) {
expect(e.message).toBe("An instance and key must be supplied");
}
}
});
it('should execute the function once', () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = jest.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(1);
});
it('should execute the function once, even with new contexts', () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = jest.fn().mockReturnValue(val);
let sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
sf = Singleflight.for(instance, key); // RESET FOR TEST
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(1);
});
it('should execute the function twice if the result was forgotten', () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = jest.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
sf.forget();
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
it('should execute the function twice if the instance was forgotten', () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = jest.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
Singleflight.forgetAllFor(instance);
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
it('should execute the function twice if everything was forgotten', () => {
const instance = {};
const key = "test";
const val = {}; // unique object for reference check
const fn = jest.fn().mockReturnValue(val);
const sf = Singleflight.for(instance, key);
const r1 = sf.do(fn);
expect(r1).toBe(val);
expect(fn.mock.calls.length).toBe(1);
Singleflight.forgetAll();
const r2 = sf.do(fn);
expect(r2).toBe(val);
expect(fn.mock.calls.length).toBe(2);
});
});

View file

@ -21,15 +21,7 @@ async function openRoomDirectory(session) {
}
async function findSublist(session, name) {
const sublists = await session.queryAll('.mx_RoomSublist');
for (const sublist of sublists) {
const header = await sublist.$('.mx_RoomSublist_headerText');
const headerText = await session.innerText(header);
if (headerText.toLowerCase().includes(name.toLowerCase())) {
return sublist;
}
}
throw new Error(`could not find room list section that contains '${name}' in header`);
return await session.query(`.mx_RoomSublist[aria-label="${name}" i]`);
}
async function createRoom(session, roomName, encrypted=false) {

View file

@ -31,6 +31,9 @@ module.exports = async function signup(session, username, password, homeserver)
// accept homeserver
await nextButton.click();
}
// Delay required because of local race condition on macOs
// Where the form is not query-able despite being present in the DOM
await session.delay(100);
//fill out form
const usernameField = await session.query("#mx_RegistrationForm_username");
const passwordField = await session.query("#mx_RegistrationForm_password");

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