Merge remote-tracking branch 'origin/develop' into dbkr/support_no_ssss

This commit is contained in:
David Baker 2020-06-15 11:36:39 +01:00
commit 404798d27c
119 changed files with 7093 additions and 2041 deletions

BIN
docs/img/RoomListStore2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View file

@ -2,20 +2,31 @@
It's so complicated it needs its own README.
![](img/RoomListStore2.png)
Legend:
* Orange = External event.
* Purple = Deterministic flow.
* Green = Algorithm definition.
* Red = Exit condition/point.
* Blue = Process definition.
## Algorithms involved
There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting.
Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting
Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting
algorithm determines how rooms get ordered within tags affected by the list algorithm.
Algorithm respectively. The list algorithm determines the primary ordering of a given tag whereas the
tag sorting defines how rooms within that tag get sorted, at the discretion of the list ordering.
Behaviour of the overall room list (sticky rooms, etc) are determined by the generically-named Algorithm
class. Here is where much of the coordination from the room list store is done to figure out which list
algorithm to call, instead of having all the logic in the room list store itself.
Behaviour of the room list takes the shape of determining what features the room list supports, as well
as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which
is described later in this doc, is an example of an algorithm which makes heavy behavioural changes
to the room list.
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
the power to decide when and how to apply the tag sorting, if at all.
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
Each category then gets sorted by the appropriate tag sorting algorithm.
### Tag sorting algorithm: Alphabetical
@ -70,33 +81,33 @@ Conveniently, each tag gets ordered by those categories as presented: red rooms
above bold, etc.
Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example)
gets applied to each category in a sub-list fashion. This should result in the red rooms (for example)
being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
collectively the tag will be sorted into categories with red being at the top.
<!-- TODO: Implement sticky rooms as described below -->
## Sticky rooms
The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing.
The sticky room will remain in position on the room list regardless of other factors going on as typically
clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms
above the selected room at all times, where N is the number of rooms above the selected rooms when it was
selected.
When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm.
From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class
manages which room is sticky. This is to ensure that all algorithms handle it the same.
For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one
room above their selection at all times. If they receive another notification, and the tag ordering is
specified as Recent, they'll see the new notification go to the top position, and the one that was previously
there fall behind the sticky room.
The sticky flag is simply to say it will not move higher or lower down the list while it is active. For
example, if using the importance algorithm, the room would naturally become idle once viewed and thus
would normally fly down the list out of sight. The sticky room concept instead holds it in place, never
letting it fly down until the user moves to another room.
The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the
tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another
room, the previous sticky room gets recalculated to determine which category it needs to be in as the user
could have been scrolled up while new messages were received.
Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky
room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and
selects the middle room, they will see exactly one room above their selection at all times. If they
receive another notification which causes the room to move into the topmost position, the room that was
above the sticky room will move underneath to allow for the new room to take the top slot, maintaining
the sticky room's position.
Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what
kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user
selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.
This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain
2 rooms above the sticky room.
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
and thus the user can see a shift in what kinds of rooms move around their selection. An example would
be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having
the rooms above it read on another device. This would result in 1 red room and 1 other kind of room
above the sticky room as it will try to maintain 2 rooms above the sticky room.
An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
@ -128,13 +139,13 @@ maintain the caching behaviour described above.
## Class breakdowns
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also
responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map:
tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented
to the user). Various list-specific utilities are also included, though they are expected to move
somewhere more general when needed. For example, the `membership` utilities could easily be moved
elsewhere as needed.
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible
for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get
defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the
user). Various list-specific utilities are also included, though they are expected to move somewhere
more general when needed. For example, the `membership` utilities could easily be moved elsewhere
as needed.
The various bits throughout the room list store should also have jsdoc of some kind to help describe
what they do and how they work.

View file

@ -65,8 +65,8 @@
"create-react-class": "^15.6.0",
"diff-dom": "^4.1.3",
"diff-match-patch": "^1.0.4",
"emojibase-data": "^4.0.2",
"emojibase-regex": "^3.0.0",
"emojibase-data": "^5.0.1",
"emojibase-regex": "^4.0.1",
"escape-html": "^1.0.3",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
@ -93,6 +93,8 @@
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1",
"react-resizable": "^1.10.1",
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4",
"text-encoding-utf-8": "^1.0.1",
@ -125,6 +127,7 @@
"@types/qrcode": "^1.3.4",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/react-transition-group": "^4.4.0",
"@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",

View file

@ -19,7 +19,7 @@ limitations under the License.
@import "./_font-sizes.scss";
:root {
font-size: 15px;
font-size: 10px;
}
html {
@ -581,3 +581,118 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
// So it fits in the space provided by the page
max-width: 120px;
}
// A context menu that largely fits the | [icon] [label] | format.
.mx_IconizedContextMenu {
// Put 20px of padding around the whole menu. We do this instead of a
// simple `padding: 20px` rule so the horizontal rules added by the
// optionLists is rendered correctly (full width).
> * {
padding-left: 20px;
padding-right: 20px;
&:first-child {
padding-top: 20px;
}
&:last-child {
padding-bottom: 20px;
}
}
.mx_IconizedContextMenu_optionList {
// the notFirst class is for cases where the optionList might be under a header of sorts.
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
margin-top: 20px;
// This is a bit of a hack when we could just use a simple border-top property,
// however we have a (kinda) good reason for doing it this way: we need opacity.
// To get the right color, we need an opacity modifier which means we have to work
// around the problem. PostCSS doesn't support the opacity() function, and if we
// use something like postcss-functions we quickly run into an issue where the
// function we would define gets passed a CSS variable for custom themes, which
// can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379
//
// Therefore, we just hack in a line and border the thing ourselves
&::before {
border-top: 1px solid $primary-fg-color;
opacity: 0.1;
content: '';
// Counteract the padding problems (width: 100% ignores the 40px padding,
// unless we position it absolutely then it does the right thing).
width: 100%;
position: absolute;
left: 0;
}
}
ul {
list-style: none;
margin: 0;
padding: 0;
li {
margin: 0;
padding: 20px 0 0;
.mx_AccessibleButton {
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
span:last-child { // labels
padding-left: 14px;
width: 100%;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
}
}
&.mx_IconizedContextMenu_compact {
> * {
padding-left: 11px;
padding-right: 16px;
&:first-child {
padding-top: 13px;
}
&:last-child {
padding-bottom: 13px;
}
}
.mx_IconizedContextMenu_optionList {
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
margin-top: 10px;
li:first-child {
padding-top: 10px;
}
}
li:first-child {
padding-top: 0;
}
}
}
}

View file

@ -12,12 +12,14 @@
@import "./structures/_HeaderButtons.scss";
@import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.scss";
@import "./structures/_LeftPanel2.scss";
@import "./structures/_MainSplit.scss";
@import "./structures/_MatrixChat.scss";
@import "./structures/_MyGroups.scss";
@import "./structures/_NotificationPanel.scss";
@import "./structures/_RightPanel.scss";
@import "./structures/_RoomDirectory.scss";
@import "./structures/_RoomSearch.scss";
@import "./structures/_RoomStatusBar.scss";
@import "./structures/_RoomSubList.scss";
@import "./structures/_RoomView.scss";
@ -28,6 +30,7 @@
@import "./structures/_ToastContainer.scss";
@import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss";
@import "./structures/_UserMenuButton.scss";
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
@ -115,6 +118,7 @@
@import "./views/elements/_Slider.scss";
@import "./views/elements/_Spinner.scss";
@import "./views/elements/_StyledCheckbox.scss";
@import "./views/elements/_StyledRadioButton.scss";
@import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss";
@import "./views/elements/_ToggleSwitch.scss";
@ -169,17 +173,22 @@
@import "./views/rooms/_MemberList.scss";
@import "./views/rooms/_MessageComposer.scss";
@import "./views/rooms/_MessageComposerFormatBar.scss";
@import "./views/rooms/_NotificationBadge.scss";
@import "./views/rooms/_PinnedEventTile.scss";
@import "./views/rooms/_PinnedEventsPanel.scss";
@import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomBreadcrumbs2.scss";
@import "./views/rooms/_RoomDropTarget.scss";
@import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomList2.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist2.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss";

View file

@ -14,59 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
$font-1px: 0.067rem;
$font-1-5px: 0.100rem;
$font-2px: 0.133rem;
$font-3px: 0.200rem;
$font-4px: 0.267rem;
$font-5px: 0.333rem;
$font-6px: 0.400rem;
$font-7px: 0.467rem;
$font-8px: 0.533rem;
$font-9px: 0.600rem;
$font-10px: 0.667rem;
$font-10-4px: 0.693rem;
$font-11px: 0.733rem;
$font-12px: 0.800rem;
$font-13px: 0.867rem;
$font-14px: 0.933rem;
$font-15px: 1.000rem;
$font-16px: 1.067rem;
$font-17px: 1.133rem;
$font-18px: 1.200rem;
$font-19px: 1.267rem;
$font-20px: 1.3333333rem;
$font-21px: 1.400rem;
$font-22px: 1.467rem;
$font-23px: 1.533rem;
$font-24px: 1.600rem;
$font-25px: 1.667rem;
$font-26px: 1.733rem;
$font-27px: 1.800rem;
$font-28px: 1.867rem;
$font-29px: 1.933rem;
$font-30px: 2.000rem;
$font-31px: 2.067rem;
$font-32px: 2.133rem;
$font-33px: 2.200rem;
$font-34px: 2.267rem;
$font-35px: 2.333rem;
$font-36px: 2.400rem;
$font-37px: 2.467rem;
$font-38px: 2.533rem;
$font-39px: 2.600rem;
$font-40px: 2.667rem;
$font-41px: 2.733rem;
$font-42px: 2.800rem;
$font-43px: 2.867rem;
$font-44px: 2.933rem;
$font-45px: 3.000rem;
$font-46px: 3.067rem;
$font-47px: 3.133rem;
$font-48px: 3.200rem;
$font-49px: 3.267rem;
$font-50px: 3.333rem;
$font-51px: 3.400rem;
$font-52px: 3.467rem;
$font-88px: 5.887rem;
$font-400px: 26.667rem;
$font-1px: 0.1rem;
$font-1-5px: 0.15rem;
$font-2px: 0.2rem;
$font-3px: 0.3rem;
$font-4px: 0.4rem;
$font-5px: 0.5rem;
$font-6px: 0.6rem;
$font-7px: 0.7rem;
$font-8px: 0.8rem;
$font-9px: 0.9rem;
$font-10px: 1.0rem;
$font-10-4px: 1.04rem;
$font-11px: 1.1rem;
$font-12px: 1.2rem;
$font-13px: 1.3rem;
$font-14px: 1.4rem;
$font-15px: 1.5rem;
$font-16px: 1.6rem;
$font-17px: 1.7rem;
$font-18px: 1.8rem;
$font-19px: 1.9rem;
$font-20px: 2.0rem;
$font-21px: 2.1rem;
$font-22px: 2.2rem;
$font-23px: 2.3rem;
$font-24px: 2.4rem;
$font-25px: 2.5rem;
$font-26px: 2.6rem;
$font-27px: 2.7rem;
$font-28px: 2.8rem;
$font-29px: 2.9rem;
$font-30px: 3.0rem;
$font-31px: 3.1rem;
$font-32px: 3.2rem;
$font-33px: 3.3rem;
$font-34px: 3.4rem;
$font-35px: 3.5rem;
$font-36px: 3.6rem;
$font-37px: 3.7rem;
$font-38px: 3.8rem;
$font-39px: 3.9rem;
$font-40px: 4.0rem;
$font-41px: 4.1rem;
$font-42px: 4.2rem;
$font-43px: 4.3rem;
$font-44px: 4.4rem;
$font-45px: 4.5rem;
$font-46px: 4.6rem;
$font-47px: 4.7rem;
$font-48px: 4.8rem;
$font-49px: 4.9rem;
$font-50px: 5.0rem;
$font-51px: 5.1rem;
$font-52px: 5.2rem;
$font-88px: 8.8rem;
$font-400px: 40rem;

View file

@ -0,0 +1,173 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_LeftPanel during replacement of old component
$tagPanelWidth: 70px; // only applies in this file, used for calculations
.mx_LeftPanel2 {
background-color: $roomlist2-bg-color;
min-width: 260px;
max-width: 50%;
// Create a row-based flexbox for the TagPanel and the room list
display: flex;
.mx_LeftPanel2_tagPanelContainer {
flex-grow: 0;
flex-shrink: 0;
flex-basis: $tagPanelWidth;
height: 100%;
// Create another flexbox so the TagPanel fills the container
display: flex;
// TagPanel handles its own CSS
}
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel2_roomListContainer {
width: calc(100% - $tagPanelWidth);
// Create another flexbox (this time a column) for the room list components
display: flex;
flex-direction: column;
.mx_LeftPanel2_userHeader {
padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom
// Create another flexbox column for the rows to stack within
display: flex;
flex-direction: column;
// There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs
.mx_LeftPanel2_headerRow {
// Create yet another flexbox, this time within the row, to ensure items stay
// aligned correctly. This is also a row-based flexbox.
display: flex;
align-items: center;
}
.mx_LeftPanel2_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
}
.mx_LeftPanel2_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
}
.mx_LeftPanel2_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
.mx_LeftPanel2_breadcrumbsContainer {
width: 100%;
overflow: hidden;
margin-top: 8px;
}
}
.mx_LeftPanel2_filterContainer {
margin-left: 12px;
margin-right: 12px;
// Create a flexbox to organize the inputs
display: flex;
align-items: center;
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
// Cheaty way to return the occupied space to the filter input
margin: 0;
width: 0;
// Don't forget to hide the masked ::before icon
visibility: hidden;
}
.mx_LeftPanel2_exploreButton {
width: 28px;
height: 28px;
border-radius: 20px;
background-color: $roomlist2-button-bg-color;
position: relative;
margin-left: 8px;
&::before {
content: '';
position: absolute;
top: 6px;
left: 6px;
width: 16px;
height: 16px;
mask-image: url('$(res)/img/feather-customised/compass.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
}
.mx_LeftPanel2_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
width: 100%;
max-width: 100%;
// Create a flexbox to trick the layout engine
display: flex;
}
}
// These styles override the defaults for the minimized (66px) layout
&.mx_LeftPanel2_minimized {
min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute.
width: calc(68px + $tagPanelWidth) !important;
.mx_LeftPanel2_roomListContainer {
width: 68px;
.mx_LeftPanel2_userHeader {
.mx_LeftPanel2_headerRow {
justify-content: center;
}
.mx_LeftPanel2_userAvatarContainer {
margin-right: 0;
}
}
.mx_LeftPanel2_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;
justify-content: center;
.mx_LeftPanel2_exploreButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
}
}
}
}

View file

@ -66,7 +66,7 @@ limitations under the License.
}
/* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) {
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) {
background-color: $primary-bg-color;
flex: 1 1 0;

View file

@ -0,0 +1,81 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this component expects to be contained within a flexbox
.mx_RoomSearch {
flex: 1;
border-radius: 20px;
background-color: $roomlist2-button-bg-color;
height: 26px;
padding: 2px;
// Create a flexbox for the icons (easier to manage)
display: flex;
align-items: center;
.mx_RoomSearch_icon {
width: 16px;
height: 16px;
mask: url('$(res)/img/feather-customised/search-input.svg');
mask-repeat: no-repeat;
background: $primary-fg-color;
margin-left: 7px;
}
.mx_RoomSearch_input {
border: none !important; // !important to override default app-wide styles
flex: 1 !important; // !important to override default app-wide styles
color: $primary-fg-color !important; // !important to override default app-wide styles
padding: 0;
height: 100%;
width: 100%;
font-size: $font-12px;
line-height: $font-16px;
&:not(.mx_RoomSearch_inputExpanded)::placeholder {
color: $primary-fg-color !important; // !important to override default app-wide styles
}
}
&.mx_RoomSearch_expanded {
.mx_RoomSearch_clearButton {
width: 16px;
height: 16px;
mask-image: url('$(res)/img/feather-customised/x.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
margin-right: 8px;
}
}
.mx_RoomSearch_clearButton {
width: 0;
height: 0;
}
&.mx_RoomSearch_minimized {
border-radius: 32px;
height: auto;
width: auto;
padding: 8px;
.mx_RoomSearch_icon {
margin-left: 0;
}
}
}

View file

@ -0,0 +1,82 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UserMenuButton {
// No special styles on the button itself
}
.mx_UserMenuButton_contextMenu {
width: 247px;
.mx_UserMenuButton_contextMenu_header {
// Create a flexbox to organize the header a bit easier
display: flex;
align-items: center;
&:nth-child(n + 1) {
// The first header will have appropriate padding, subsequent ones need a margin.
margin-top: 10px;
}
.mx_UserMenuButton_contextMenu_name {
// Create another flexbox of columns to handle large user IDs
display: flex;
flex-direction: column;
// fit the container
flex: 1;
width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button
* {
// Automatically grow all subelements to fit the container
flex: 1;
width: 100%;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenuButton_contextMenu_displayName {
font-weight: bold;
font-size: $font-15px;
line-height: $font-20px;
}
.mx_UserMenuButton_contextMenu_userId {
font-size: $font-15px;
line-height: $font-24px;
}
}
.mx_UserMenuButton_contextMenu_themeButton {
min-width: 32px;
max-width: 32px;
width: 32px;
height: 32px;
margin-left: 8px;
border-radius: 32px;
background-color: $theme-button-bg-color;
cursor: pointer;
// to make alignment easier, create flexbox for the image
display: flex;
align-items: center;
justify-content: center;
}
}
}

View file

@ -24,7 +24,7 @@ limitations under the License.
align-items: flex-start;
input[type=checkbox] {
display: none;
appearance: none;
& + label {
display: flex;
@ -48,6 +48,8 @@ limitations under the License.
border-radius: $border-radius;
img {
display: none;
height: 100%;
width: 100%;
filter: invert(100%);
@ -57,6 +59,10 @@ limitations under the License.
&:checked + label > .mx_Checkbox_background {
background: $accent-color;
border-color: $accent-color;
img {
display: block;
}
}
& + label > *:not(.mx_Checkbox_background) {

View file

@ -0,0 +1,98 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This component expects the parent to specify a positive padding and
* width
*/
.mx_RadioButton {
$radio-circle-color: $muted-fg-color;
$active-radio-circle-color: $accent-color;
position: relative;
display: flex;
align-items: center;
flex-grow: 1;
> span {
flex-grow: 1;
display: flex;
margin-left: 8px;
margin-right: 8px;
}
.mx_RadioButton_spacer {
flex-shrink: 0;
flex-grow: 0;
height: $font-16px;
width: $font-16px;
}
> input[type=radio] {
// Remove the OS's representation
margin: 0;
padding: 0;
appearance: none;
+ div {
flex-shrink: 0;
flex-grow: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: $font-16px;
width: $font-16px;
border: $font-1-5px solid $radio-circle-color;
border-radius: $font-16px;
> div {
box-sizing: border-box;
height: $font-8px;
width: $font-8px;
border-radius: $font-8px;
}
}
}
> input[type=radio]:checked {
+ div {
border-color: $active-radio-circle-color;
> div {
background: $active-radio-circle-color;
}
}
}
> input[type=radio]:disabled {
+ div {
> div {
display: none;
}
}
}
}

View file

@ -108,12 +108,12 @@ $left-gutter: 65px;
top: 27px;
}
.mx_EventTile_continuation .mx_EventTile_readAvatars,
.mx_EventTile_emote .mx_EventTile_readAvatars {
&.mx_EventTile_continuation .mx_EventTile_readAvatars,
&.mx_EventTile_emote .mx_EventTile_readAvatars {
top: 5px;
}
.mx_EventTile_info .mx_EventTile_readAvatars {
&.mx_EventTile_info .mx_EventTile_readAvatars {
top: 4px;
}

View file

@ -43,6 +43,10 @@ $irc-line-height: $font-18px;
> .mx_EventTile_msgOption {
order: 5;
flex-shrink: 0;
.mx_EventTile_readAvatars {
top: 0.2rem; // ($irc-line-height - avatar height) / 2
}
}
> .mx_SenderProfile {
@ -78,7 +82,7 @@ $irc-line-height: $font-18px;
align-items: center;
// Need to use important to override the js provided height and width values.
> .mx_BaseAvatar, .mx_BaseAvatar > * {
> .mx_BaseAvatar, > .mx_BaseAvatar > * {
height: $font-14px !important;
width: $font-14px !important;
font-size: $font-10px !important;
@ -96,10 +100,16 @@ $irc-line-height: $font-18px;
position: relative;
right: unset;
left: unset;
top: 0;
padding: 0;
order: 3;
flex-shrink: 0;
flex-grow: 0;
height: $font-18px;
background-position: center;
}
.mx_EventTile_line {

View file

@ -0,0 +1,70 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_NotificationBadge {
&:not(.mx_NotificationBadge_visible) {
display: none;
}
// Badges are structured a bit weirdly to work around issues with non-monospace
// font styles. The badge pill is actually a background div and the count floats
// within that. For example:
//
// ( 99+ ) <-- Rounded pill is a _bg class.
// ^- The count is an element floating within that.
&.mx_NotificationBadge_visible {
background-color: $roomtile2-default-badge-bg-color;
// Create a flexbox to order the count a bit easier
display: flex;
align-items: center;
justify-content: center;
&.mx_NotificationBadge_highlighted {
// TODO: Use a more specific variable
background-color: $warning-color;
}
// These are the 3 background types
&.mx_NotificationBadge_dot {
width: 6px;
height: 6px;
border-radius: 6px;
}
&.mx_NotificationBadge_2char {
width: 16px;
height: 16px;
border-radius: 16px;
}
&.mx_NotificationBadge_3char {
width: 26px;
height: 16px;
border-radius: 16px;
}
// The following is the floating badge
.mx_NotificationBadge_count {
font-size: $font-10px;
line-height: $font-14px;
color: #fff; // TODO: Variable
}
}
}

View file

@ -0,0 +1,51 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomBreadcrumbs2 {
width: 100%;
// Create a flexbox for the crumbs
display: flex;
flex-direction: row;
align-items: flex-start;
.mx_RoomBreadcrumbs2_crumb {
margin-right: 8px;
width: 32px;
}
// These classes come from the CSSTransition component. There's many more classes we
// could care about, but this is all we worried about for now. The animation works by
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
// sliding it into view.
&.mx_RoomBreadcrumbs2-enter {
margin-left: -40px; // 32px for the avatar, 8px for the margin
}
&.mx_RoomBreadcrumbs2-enter-active {
margin-left: 0;
// Timing function is as-requested by design.
// NOTE: The transition time MUST match the value passed to CSSTransition!
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
}
.mx_RoomBreadcrumbs2_placeholder {
font-weight: 600;
font-size: $font-14px;
line-height: 32px; // specifically to match the height this is not scaled
height: 32px;
}
}

View file

@ -15,6 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomList.mx_RoomList2 {
overflow-y: auto;
}
.mx_RoomList {
/* take up remaining space below TopLeftMenu */
flex: 1;

View file

@ -0,0 +1,27 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_RoomList during replacement of old component
.mx_RoomList2 {
width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
// Create a column-based flexbox for the sublists. That's pretty much all we have to
// worry about in this stylesheet.
display: flex;
flex-direction: column;
flex-wrap: nowrap; // let the column overflow
}

View file

@ -0,0 +1,327 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_RoomSublist during replacement of old component
.mx_RoomSublist2 {
// The sublist is a column of rows, essentially
display: flex;
flex-direction: column;
margin-left: 8px;
margin-top: 12px;
margin-bottom: 12px;
width: 100%;
.mx_RoomSublist2_headerContainer {
// Create a flexbox to make ordering easy
display: flex;
align-items: center;
padding-bottom: 8px;
height: 24px;
.mx_RoomSublist2_badgeContainer {
opacity: 0.8;
width: 16px;
margin-right: 5px; // aligns with the room tile's badge
// Create another flexbox row because it's super easy to position the badge this way.
display: flex;
align-items: center;
justify-content: center;
}
// Both of these buttons are hidden by default until the list is hovered
.mx_RoomSublist2_auxButton,
.mx_RoomSublist2_menuButton {
width: 0;
margin: 0;
visibility: hidden;
position: relative;
border-radius: 32px;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
top: 4px;
left: 4px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $muted-fg-color;
}
}
.mx_RoomSublist2_auxButton::before {
mask-image: url('$(res)/img/feather-customised/plus.svg');
}
.mx_RoomSublist2_menuButton::before {
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
}
.mx_RoomSublist2_headerText {
text-transform: uppercase;
opacity: 0.5;
line-height: $font-16px;
font-size: $font-12px;
flex: 1;
max-width: calc(100% - 16px); // 16px is the badge width
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.mx_RoomSublist2_resizeBox {
margin-bottom: 4px; // for the resize handle
position: relative;
// Create another flexbox column for the tiles
display: flex;
flex-direction: column;
overflow: hidden;
.mx_RoomSublist2_showMoreButton {
cursor: pointer;
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
// This is the same color as the left panel background because it needs
// to occlude the lastmost tile in the list.
background-color: $roomlist2-bg-color;
// Update the render() function for RoomSublist2 if these change
// Update the ListLayout class for minVisibleTiles if these change.
//
// At 24px high and 8px padding on the top this equates to 0.65 of
// a tile due to how the padding calculations work.
height: 24px;
padding-top: 8px;
// We force this to the bottom so it will overlap rooms as needed.
// We account for the space it takes up (24px) in the code through padding.
position: absolute;
bottom: 4px; // the height of the resize handle
left: 0;
right: 0;
// We create a flexbox to cheat at alignment
display: flex;
align-items: center;
.mx_RoomSublist2_showMoreButtonChevron {
position: relative;
width: 16px;
height: 16px;
margin-left: 12px;
margin-right: 18px;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $roomtile2-preview-color;
}
}
// Class name comes from the ResizableBox component
// The hover state needs to use the whole sublist, not just the resizable box,
// so that selector is below and one level higher.
.react-resizable-handle {
cursor: ns-resize;
border-radius: 2px;
// This is positioned directly below the 'show more' button.
position: absolute;
bottom: 0;
left: 0;
right: 0;
// This is to visually align the bar in the list. Should be 12px from
// either side of the list. We define this after the positioning to
// trick the browser.
margin-left: 4px;
margin-right: 4px;
}
}
// The aforementioned selector for the hover state.
&:hover, &.mx_RoomSublist2_hasMenuOpen {
.react-resizable-handle {
opacity: 0.2;
// Update the render() function for RoomSublist2 if this changes
border: 2px solid $primary-fg-color;
}
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer {
// If the header doesn't have an aux button we still need to hide the badge for
// the menu button.
.mx_RoomSublist2_badgeContainer {
// Completely hide the badge
width: 0;
margin: 0;
visibility: hidden;
}
&:not(.mx_RoomSublist2_headerContainer_withAux) {
// The menu button will be the rightmost button, so make it correctly aligned.
.mx_RoomSublist2_menuButton {
margin-right: 1px; // line it up with the badges on the room tiles
}
}
// Both of these buttons have circled backgrounds and are visible at this point,
// so make them so.
.mx_RoomSublist2_auxButton,
.mx_RoomSublist2_menuButton {
width: 24px;
height: 24px;
margin-left: 16px;
visibility: visible;
background-color: $roomlist2-button-bg-color;
}
}
}
&.mx_RoomSublist2_minimized {
.mx_RoomSublist2_headerContainer {
height: auto;
flex-direction: column;
position: relative;
.mx_RoomSublist2_badgeContainer {
order: 1;
align-self: flex-end;
margin-right: 0;
}
.mx_RoomSublist2_headerText {
order: 2;
max-width: 100%;
}
.mx_RoomSublist2_auxButton {
order: 4;
visibility: visible;
width: 32px !important; // !important to override hover styles
height: 32px !important; // !important to override hover styles
margin-left: 0 !important; // !important to override hover styles
background-color: $roomlist2-button-bg-color;
margin-top: 8px;
&::before {
top: 8px;
left: 8px;
}
}
}
.mx_RoomSublist2_resizeBox {
align-items: center;
.mx_RoomSublist2_showMoreButton {
flex-direction: column;
.mx_RoomSublist2_showMoreButtonChevron {
margin-right: 12px; // to center
}
}
}
&:hover, &.mx_RoomSublist2_hasMenuOpen {
.mx_RoomSublist2_menuButton {
visibility: visible;
position: absolute;
bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment
right: 0;
width: 16px;
height: 16px;
border-radius: 0;
z-index: 1; // occlude the list name
// This is the same color as the left panel background because it needs
// to occlude the sublist title
background-color: $roomlist2-bg-color;
&::before {
top: 0;
left: 0;
}
}
.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) {
.mx_RoomSublist2_menuButton {
bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
}
}
}
}
}
// We have a hover style on the room list with no specific list hovered, so account for that
.mx_RoomList2:hover .mx_RoomSublist2:not(.mx_RoomSublist2_minimized),
.mx_RoomSublist2_hasMenuOpen:not(.mx_RoomSublist2_minimized) {
.mx_RoomSublist2_headerContainer_withAux {
.mx_RoomSublist2_badgeContainer {
// Completely hide the badge
width: 0;
margin: 0;
visibility: hidden;
}
.mx_RoomSublist2_auxButton {
// Show the aux button, but not the list button
width: 24px;
height: 24px;
margin-right: 1px; // line it up with the badges on the room tiles
visibility: visible;
}
}
}
.mx_RoomSublist2_contextMenu {
padding: 20px 16px;
width: 250px;
hr {
margin-top: 16px;
margin-bottom: 16px;
margin-right: 16px; // additional 16px
border: 1px solid $roomsublist2-divider-color;
}
.mx_RoomSublist2_contextMenu_title {
font-size: $font-15px;
line-height: $font-20px;
font-weight: 600;
margin-bottom: 4px;
}
.mx_RadioButton, .mx_Checkbox {
margin-top: 8px;
}
.mx_Checkbox {
margin-left: -8px; // to counteract the indent from the component
}
}

View file

@ -0,0 +1,200 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename to mx_RoomTile during replacement of old component
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 {
margin-bottom: 4px;
padding: 4px;
// The tile is also a flexbox row itself
display: flex;
flex-wrap: wrap;
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
background-color: $roomtile2-selected-bg-color;
border-radius: 32px;
}
.mx_RoomTile2_avatarContainer {
margin-right: 8px;
}
.mx_RoomTile2_nameContainer {
flex-grow: 1;
max-width: calc(100% - 58px); // 32px avatar, 18px badge area, 8px margin on avatar
// Create a new column layout flexbox for the name parts
display: flex;
flex-direction: column;
justify-content: center;
.mx_RoomTile2_name,
.mx_RoomTile2_messagePreview {
margin: 0 2px;
width: 100%;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_RoomTile2_name {
font-size: $font-14px;
line-height: $font-18px;
}
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
font-weight: 600;
}
.mx_RoomTile2_messagePreview {
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
}
.mx_RoomTile2_nameWithPreview {
margin-top: -4px; // shift the name up a bit more
}
}
.mx_RoomTile2_badgeContainer {
width: 18px;
height: 32px;
// Create another flexbox row because it's super easy to position the badge at
// the end this way.
display: flex;
align-items: center;
justify-content: center;
}
// The menu button is hidden by default
// TODO: [Notifications] Use mx_RoomTile2_notificationsButton, similar to the following approach:
// https://github.com/matrix-org/matrix-react-sdk/blob/2180a56074f3698fc0241c309a72ba6cad802d1c/res/css/views/rooms/_RoomSublist2.scss#L48-L76
// You'll need to do the same down below on the &:hover selector for the tile.
// ... also remove this 4 line TODO comment.
.mx_RoomTile2_menuButton,
.mx_RoomTile2_notificationsButton {
width: 0;
height: 0;
visibility: hidden;
position: relative;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
.mx_RoomTile2_menuButton::before {
top: 8px;
left: -1px; // this is off-center to align it with the badges
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
}
&:not(.mx_RoomTile2_minimized) {
&:hover, &.mx_RoomTile2_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_RoomTile2_badgeContainer {
width: 0;
height: 0;
visibility: hidden;
}
.mx_RoomTile2_menuButton {
width: 18px;
height: 32px;
visibility: visible;
}
}
}
&.mx_RoomTile2_minimized {
flex-direction: column;
align-items: center;
position: relative;
.mx_RoomTile2_avatarContainer {
margin-right: 0;
}
.mx_RoomTile2_badgeContainer {
position: absolute;
top: 0;
right: 0;
height: 18px;
}
}
}
.mx_RoomTile2_contextMenu {
.mx_RoomTile2_contextMenu_redRow {
.mx_AccessibleButton {
color: $warning-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $warning-color;
}
}
.mx_IconizedContextMenu_icon {
position: relative;
width: 16px;
height: 16px;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
.mx_RoomTile2_iconStar::before {
mask-image: url('$(res)/img/feather-customised/star.svg');
}
.mx_RoomTile2_iconArrowDown::before {
mask-image: url('$(res)/img/feather-customised/arrow-down.svg');
}
.mx_RoomTile2_iconUser::before {
mask-image: url('$(res)/img/feather-customised/user.svg');
}
.mx_RoomTile2_iconSettings::before {
mask-image: url('$(res)/img/feather-customised/settings.svg');
}
.mx_RoomTile2_iconSignOut::before {
mask-image: url('$(res)/img/feather-customised/sign-out.svg');
}
}

View file

@ -43,3 +43,7 @@ limitations under the License.
padding-left: 20px;
padding-right: 5px;
}
.mx_SettingsTab_customFontSizeField {
margin-left: calc($font-16px + 10px);
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 361 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-compass"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>

After

Width:  |  Height:  |  Size: 342 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-horizontal"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>

After

Width:  |  Height:  |  Size: 343 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 339 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>

After

Width:  |  Height:  |  Size: 650 B

View file

@ -103,6 +103,24 @@ $eventtile-meta-color: $roomtopic-color;
$header-divider-color: $header-panel-text-primary-color;
// ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color;
$roomsublist2-divider-color: #e9eaeb;
$roomtile2-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #1A1D23;
// ********************
$roomtile-name-color: $header-panel-text-primary-color;
$roomtile-selected-color: $text-primary-color;
$roomtile-notified-color: $text-primary-color;

View file

@ -172,6 +172,22 @@ $header-divider-color: #91A1C0;
// ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color;
$roomsublist2-divider-color: #e9eaeb;
$roomtile2-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #FFF;
// ********************
$roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color;
$roomtile-selected-color: #212121;

View file

@ -27,7 +27,7 @@ import RoomViewStore from './stores/RoomViewStore';
*/
class ActiveRoomObserver {
constructor() {
this._listeners = {};
this._listeners = {}; // key=roomId, value=function(isActive:boolean)
this._activeRoomId = RoomViewStore.getRoomId();
// TODO: We could self-destruct when the last listener goes away, or at least
@ -35,6 +35,10 @@ class ActiveRoomObserver {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
}
get activeRoomId(): string {
return this._activeRoomId;
}
addListener(roomId, listener) {
if (!this._listeners[roomId]) this._listeners[roomId] = [];
this._listeners[roomId].push(listener);
@ -51,23 +55,23 @@ class ActiveRoomObserver {
}
}
_emit(roomId) {
_emit(roomId, isActive: boolean) {
if (!this._listeners[roomId]) return;
for (const l of this._listeners[roomId]) {
l.call();
l.call(null, isActive);
}
}
_onRoomViewStoreUpdate() {
// emit for the old room ID
if (this._activeRoomId) this._emit(this._activeRoomId);
if (this._activeRoomId) this._emit(this._activeRoomId, false);
// update our cache
this._activeRoomId = RoomViewStore.getRoomId();
// and emit for the new one
if (this._activeRoomId) this._emit(this._activeRoomId);
if (this._activeRoomId) this._emit(this._activeRoomId, true);
}
}

View file

@ -31,6 +31,7 @@ import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions";
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
@ -529,7 +530,7 @@ export default class ContentMessages {
dis.dispatch({action: 'upload_started'});
// Focus the composer view
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
function onProgress(ev) {
upload.total = ev.total;

View file

@ -107,6 +107,29 @@ async function localSearch(searchTerm, roomId = undefined) {
const result = MatrixClientPeg.get()._processRoomEventsSearch(
emptyResult, response);
// Restore our encryption info so we can properly re-verify the events.
for (let i = 0; i < result.results.length; i++) {
const timeline = result.results[i].context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
const ev = timeline[j];
if (ev.event.curve25519Key) {
ev.makeEncrypted(
"m.room.encrypted",
{ algorithm: ev.event.algorithm },
ev.event.curve25519Key,
ev.event.ed25519Key,
);
ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
delete ev.event.curve25519Key;
delete ev.event.ed25519Key;
delete ev.event.algorithm;
delete ev.event.forwardingCurve25519KeyChain;
}
}
}
return result;
}

View file

@ -265,13 +265,22 @@ function textForServerACLEvent(ev) {
return text + changes.join(" ");
}
function textForMessageEvent(ev) {
function textForMessageEvent(ev, skipUserPrefix) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
if (skipUserPrefix) {
message = ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = _t('sent an image.');
}
} else {
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
}
}
return message;
}
@ -612,8 +621,8 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent;
}
export function textForEvent(ev) {
export function textForEvent(ev, skipUserPrefix) {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (handler) return handler(ev);
if (handler) return handler(ev, skipUserPrefix);
return '';
}

View file

@ -96,6 +96,17 @@ export default class WidgetMessaging {
});
}
/**
* Tells the widget that it should terminate now.
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
*/
terminate() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.Terminate,
});
}
/**
* Request a screenshot from a widget
* @return {Promise} To be resolved with screenshot data when it has been generated

View file

@ -22,9 +22,10 @@ import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
const HomePage = () => {

View file

@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({
@ -198,7 +198,7 @@ const LeftPanel = createReactClass({
onSearchCleared: function(source) {
if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
},
@ -252,7 +252,7 @@ const LeftPanel = createReactClass({
if (!this.props.collapsed) {
exploreButton = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton>
<AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton>
</div>
);
}
@ -274,28 +274,15 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
let roomList = null;
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
roomList = <RoomList2
onKeyDown={this._onKeyDown}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ref={this.collectRoomList}
onFocus={this._onFocus}
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
return (
<div className={containerClasses}>

View file

@ -0,0 +1,201 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import TagPanel from "./TagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import SearchBox from "./SearchBox";
import RoomList2 from "../views/rooms/RoomList2";
import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseAvatar from '../views/avatars/BaseAvatar';
import UserMenuButton from "./UserMenuButton";
import RoomSearch from "./RoomSearch";
import AccessibleButton from "../views/elements/AccessibleButton";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps {
isMinimized: boolean;
}
interface IState {
searchFilter: string; // TODO: Move search into room list?
showBreadcrumbs: boolean;
}
export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel
// TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs
// TODO: a11y
// TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?)
constructor(props: IProps) {
super(props);
this.state = {
searchFilter: "",
showBreadcrumbs: BreadcrumbsStore.instance.visible,
};
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
public componentWillUnmount() {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
private onSearch = (term: string): void => {
this.setState({searchFilter: term});
};
private onExplore = () => {
dis.fire(Action.ViewRoomDirectory);
};
private onBreadcrumbsUpdate = () => {
const newVal = BreadcrumbsStore.instance.visible;
if (newVal !== this.state.showBreadcrumbs) {
this.setState({showBreadcrumbs: newVal});
}
};
private renderHeader(): React.ReactNode {
// TODO: Update when profile info changes
// TODO: Presence
// TODO: Breadcrumbs toggle
// TODO: Menu button
const avatarSize = 32;
// TODO: Don't do this profile lookup in render()
const client = MatrixClientPeg.get();
let displayName = client.getUserId();
let avatarUrl: string = null;
const myUser = client.getUser(client.getUserId());
if (myUser) {
displayName = myUser.rawDisplayName;
avatarUrl = myUser.avatarUrl;
}
let breadcrumbs;
if (this.state.showBreadcrumbs) {
breadcrumbs = (
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
</div>
);
}
let name = <span className="mx_LeftPanel2_userName">{displayName}</span>;
let buttons = (
<span className="mx_LeftPanel2_headerButtons">
<UserMenuButton />
</span>
);
if (this.props.isMinimized) {
name = null;
buttons = null;
}
return (
<div className="mx_LeftPanel2_userHeader">
<div className="mx_LeftPanel2_headerRow">
<span className="mx_LeftPanel2_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_LeftPanel2_userAvatar"
/>
</span>
{name}
{buttons}
</div>
{breadcrumbs}
</div>
);
}
private renderSearchExplore(): React.ReactNode {
// TODO: Collapsed support
return (
<div className="mx_LeftPanel2_filterContainer">
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
<AccessibleButton
tabIndex={-1}
className='mx_LeftPanel2_exploreButton'
onClick={this.onExplore}
alt={_t("Explore rooms")}
/>
</div>
);
}
public render(): React.ReactNode {
const tagPanel = (
<div className="mx_LeftPanel2_tagPanelContainer">
<TagPanel/>
</div>
);
// TODO: Improve props for RoomList2
const roomList = <RoomList2
onKeyDown={() => {/*TODO*/}}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
isMinimized={this.props.isMinimized}
/>;
// TODO: Conference handling / calls
const containerClasses = classNames({
"mx_LeftPanel2": true,
"mx_LeftPanel2_minimized": this.props.isMinimized,
});
return (
<div className={containerClasses}>
{tagPanel}
<aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div className="mx_LeftPanel2_actualRoomListContainer">
{roomList}
</div>
</aside>
</div>
);
}
}

View file

@ -51,6 +51,8 @@ import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -358,7 +360,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// refocusing during a paste event will make the
// paste end up in the newly focused element,
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
}
};
@ -450,9 +452,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// composer, so CTRL+` it is
if (ctrlCmdOnly) {
dis.dispatch({
action: 'toggle_top_left_menu',
});
dis.fire(Action.ToggleUserMenu);
handled = true;
}
break;
@ -508,7 +508,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
ev.stopPropagation();
// we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer
@ -667,6 +667,20 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
let leftPanel = (
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
);
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
// TODO: Supply props like collapsed and disabled to LeftPanel2
leftPanel = (
<LeftPanel2 isMinimized={this.props.collapseLhs || false} />
);
}
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
@ -680,11 +694,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
{ leftPanel }
<ResizeHandle />
{ pageElement }
</div>

View file

@ -72,6 +72,7 @@ import {
hideToast as hideAnalyticsToast
} from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
/** constants for MatrixChat.state.view */
export enum Views {
@ -347,7 +348,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Analytics.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
this.focusComposer = false;
}
}
@ -604,9 +605,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewIndexedRoom(payload.roomIndex);
break;
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
{initialTabId: tabPayload.initialTabId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true
);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@ -620,7 +624,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
break;
}
case 'view_room_directory': {
case Action.ViewRoomDirectory: {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
'mx_RoomDirectory_dialogWrapper', false, true);
@ -1363,7 +1367,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast();
}
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
this.setState({
ready: true,
});
@ -1607,9 +1611,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
action: 'require_registration',
});
} else if (screen === 'directory') {
dis.dispatch({
action: 'view_room_directory',
});
dis.fire(Action.ViewRoomDirectory);
} else if (screen === 'groups') {
dis.dispatch({
action: 'view_my_groups',

View file

@ -0,0 +1,172 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import { createRef } from "react";
import classNames from "classnames";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads";
import { throttle } from 'lodash';
import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean;
}
interface IState {
query: string;
focused: boolean;
}
export default class RoomSearch extends React.PureComponent<IProps, IState> {
private dispatcherRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
query: "",
focused: false,
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}
public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload) => {
if (payload.action === 'view_room' && payload.clear_search) {
this.clearInput();
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
this.inputRef.current.focus();
}
};
private clearInput = () => {
if (!this.inputRef.current) return;
this.inputRef.current.value = "";
this.onChange();
};
private openSearch = () => {
defaultDispatcher.dispatch({action: "show_left_panel"});
};
private onChange = () => {
if (!this.inputRef.current) return;
this.setState({query: this.inputRef.current.value});
this.onSearchUpdated();
};
// it wants this at the top of the file, but we know better
// tslint:disable-next-line
private onSearchUpdated = throttle(
() => {
// We can't use the state variable because it can lag behind the input.
// The lag is most obvious when deleting/clearing text with the keyboard.
this.props.onQueryUpdate(this.inputRef.current.value);
}, 200, {trailing: true, leading: true},
);
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({focused: true});
ev.target.select();
};
private onBlur = () => {
this.setState({focused: false});
};
private onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.key === Key.ESCAPE) {
this.clearInput();
defaultDispatcher.fire(Action.FocusComposer);
}
};
public render(): React.ReactNode {
const classes = classNames({
'mx_RoomSearch': true,
'mx_RoomSearch_expanded': this.state.query || this.state.focused,
'mx_RoomSearch_minimized': this.props.isMinimized,
});
const inputClasses = classNames({
'mx_RoomSearch_input': true,
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
});
let icon = (
<div className='mx_RoomSearch_icon'/>
);
let input = (
<input
type="text"
ref={this.inputRef}
className={inputClasses}
value={this.state.query}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={_t("Search")}
autoComplete="off"
/>
);
let clearButton = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_clearButton'
onClick={this.clearInput}
/>
);
if (this.props.isMinimized) {
icon = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_icon'
onClick={this.openSearch}
/>
);
input = null;
clearButton = null;
}
return (
<div className={classes}>
{icon}
{input}
{clearButton}
</div>
);
}
}

View file

@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -127,12 +128,12 @@ export default createReactClass({
_onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
_onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {

View file

@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
const DEBUG = false;
let debuglog = function() {};
@ -1162,7 +1163,7 @@ export default createReactClass({
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
this.setState({ draggingFile: false });
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onDragLeaveOrEnd: function(ev) {
@ -1368,7 +1369,7 @@ export default createReactClass({
event: null,
});
}
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onLeaveClick: function() {
@ -1457,9 +1458,7 @@ export default createReactClass({
// using /leave rather than /join. In the short term though, we
// just ignore them.
// https://github.com/vector-im/vector-web/issues/1134
dis.dispatch({
action: 'view_room_directory',
});
dis.fire(Action.ViewRoomDirectory);
},
onSearchClick: function() {
@ -1479,7 +1478,7 @@ export default createReactClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
// jump up to wherever our read marker is

View file

@ -27,25 +27,20 @@ import { ReactNode } from "react";
* Represents a tab for the TabbedView.
*/
export class Tab {
public label: string;
public icon: string;
public body: React.ReactNode;
/**
* Creates a new tab.
* @param {string} tabLabel The untranslated tab label.
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
* @param {React.ReactNode} tabJsx The JSX for the tab container.
* @param {string} id The tab's ID.
* @param {string} label The untranslated tab label.
* @param {string} icon The class for the tab icon. This should be a simple mask.
* @param {React.ReactNode} body The JSX for the tab container.
*/
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
this.label = tabLabel;
this.icon = tabIconClass;
this.body = tabJsx;
constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) {
}
}
interface IProps {
tabs: Tab[];
initialTabId?: string;
}
interface IState {
@ -53,16 +48,17 @@ interface IState {
}
export default class TabbedView extends React.Component<IProps, IState> {
static propTypes = {
// The tabs to show
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
};
constructor(props: IProps) {
super(props);
let activeTabIndex = 0;
if (props.initialTabId) {
const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId);
if (tabIndex >= 0) activeTabIndex = tabIndex;
}
this.state = {
activeTabIndex: 0,
activeTabIndex,
};
}

View file

@ -798,6 +798,9 @@ const TimelinePanel = createReactClass({
readMarkerVisible: false,
});
}
// Send the updated read marker (along with read receipt) to the server
this.sendReadReceipt();
},

View file

@ -24,6 +24,7 @@ import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {Action} from "../../dispatcher/actions";
const AVATAR_SIZE = 28;
@ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component {
onAction = (payload) => {
// For accessibility
if (payload.action === "toggle_top_left_menu") {
if (payload.action === Action.ToggleUserMenu) {
if (this._buttonRef) this._buttonRef.click();
}
};

View file

@ -0,0 +1,270 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import {User} from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { createRef } from "react";
import { _t } from "../../languageHandler";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
interface IProps {
}
interface IState {
user: User;
menuDisplayed: boolean;
isDarkTheme: boolean;
}
export default class UserMenuButton extends React.Component<IProps, IState> {
private dispatcherRef: string;
private themeWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
menuDisplayed: false,
user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()),
isDarkTheme: this.isUserOnDarkTheme(),
};
}
private get displayName(): string {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
} else if (this.state.user) {
return this.state.user.displayName;
} else {
return MatrixClientPeg.get().getUserId();
}
}
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
}
public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
}
private isUserOnDarkTheme(): boolean {
const theme = SettingsStore.getValue("theme");
if (theme.startsWith("custom-")) {
return getCustomTheme(theme.substring("custom-".length)).is_dark;
}
return theme === "dark";
}
private onThemeChanged = () => {
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
};
private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested
// For accessibility
if (this.buttonRef.current) this.buttonRef.current.click();
};
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({menuDisplayed: true});
};
private onCloseMenu = () => {
this.setState({menuDisplayed: false});
};
private onSwitchThemeClick = () => {
// Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
const newTheme = this.state.isDarkTheme ? "light" : "dark";
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
};
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
ev.preventDefault();
ev.stopPropagation();
const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
defaultDispatcher.dispatch(payload);
this.setState({menuDisplayed: false}); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// TODO: Archived room view (deferred)
console.log("TODO: Show archived rooms");
};
private onProvideFeedback = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
this.setState({menuDisplayed: false}); // also close the menu
};
private onSignOutClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
this.setState({menuDisplayed: false}); // also close the menu
};
public render() {
let contextMenu;
if (this.state.menuDisplayed) {
let hostingLink;
const signupLink = getHostingLink("user-context-menu");
if (signupLink) {
hostingLink = (
<div className="mx_UserMenuButton_contextMenu_header">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => (
<a
href={signupLink}
target="_blank"
rel="noreferrer noopener"
tabIndex={-1}
>{sub}</a>
),
},
)}
</div>
);
}
const elementRect = this.buttonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu}
>
<div className="mx_IconizedContextMenu mx_UserMenuButton_contextMenu">
<div className="mx_UserMenuButton_contextMenu_header">
<div className="mx_UserMenuButton_contextMenu_name">
<span className="mx_UserMenuButton_contextMenu_displayName">
{this.displayName}
</span>
<span className="mx_UserMenuButton_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
<div
className="mx_UserMenuButton_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
>
<img
src={require("../../../res/img/feather-customised/sun.svg")}
alt={_t("Switch theme")}
width={16}
/>
</div>
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<img src={require("../../../res/img/feather-customised/notifications.svg")} width={16} />
<span>{_t("Notification settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<img src={require("../../../res/img/feather-customised/lock.svg")} width={16} />
<span>{_t("Security & privacy")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<img src={require("../../../res/img/feather-customised/settings.svg")} width={16} />
<span>{_t("All settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onShowArchived}>
<img src={require("../../../res/img/feather-customised/archive.svg")} width={16} />
<span>{_t("Archived rooms")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onProvideFeedback}>
<img src={require("../../../res/img/feather-customised/message-circle.svg")} width={16} />
<span>{_t("Feedback")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={this.onSignOutClick}>
<img src={require("../../../res/img/feather-customised/sign-out.svg")} width={16} />
<span>{_t("Sign out")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuButton
className="mx_UserMenuButton"
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("Account settings")}
isExpanded={this.state.menuDisplayed}
>
<img src={require("../../../res/img/feather-customised/more-horizontal.svg")} alt="..." width={14} />
</ContextMenuButton>
{contextMenu}
</React.Fragment>
)
}
}

View file

@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
ROOM_GENERAL_TAB,
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_SECURITY_TAB,
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_ROLES_TAB,
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_RoomSettingsDialog_notificationsIcon",
<NotificationSettingsTab roomId={this.props.roomId} />,
@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component {
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
tabs.push(new Tab(
ROOM_BRIDGES_TAB,
_td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />,
@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component {
}
tabs.push(new Tab(
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,

View file

@ -33,9 +33,21 @@ import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
export const USER_VOICE_TAB = "USER_VOICE_TAB";
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
export const USER_LABS_TAB = "USER_LABS_TAB";
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
export const USER_HELP_TAB = "USER_HELP_TAB";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
initialTabId: PropTypes.string,
};
constructor() {
@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
USER_GENERAL_TAB,
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
USER_APPEARANCE_TAB,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />,
));
tabs.push(new Tab(
USER_PREFERENCES_TAB,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />,
));
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
tabs.push(new Tab(
USER_SECURITY_TAB,
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
USER_LABS_TAB,
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />,
@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component {
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
USER_MJOLNIR_TAB,
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_HELP_TAB,
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
onFinished={this.props.onFinished} title={_t("Settings")}>
<div className='ms_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} />
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
</div>
</BaseDialog>
);

View file

@ -15,9 +15,36 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import {Key} from '../../../Keyboard';
import classnames from 'classnames';
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>
/**
* children: React's magic prop. Represents all children given to the element.
* element: (optional) The base element type. "div" by default.
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>;
element?: string;
// The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options.
kind?: string;
// The ARIA role
role?: string;
// The tabIndex
tabIndex?: number;
disabled?: boolean;
className?: string;
onClick?(e?: ButtonEvent): void;
};
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
ref?: React.Ref<Element>;
}
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@ -27,11 +54,20 @@ import {Key} from '../../../Keyboard';
* @param {Object} props react element properties
* @returns {Object} rendered react
*/
export default function AccessibleButton(props) {
const {element, onClick, children, kind, disabled, ...restProps} = props;
export default function AccessibleButton({
element,
onClick,
children,
kind,
disabled,
inputRef,
className,
...restProps
}: IProps) {
const newProps: IAccessibleButtonProps = restProps;
if (!disabled) {
restProps.onClick = onClick;
newProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
@ -39,7 +75,7 @@ export default function AccessibleButton(props) {
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
restProps.onKeyDown = function(e) {
newProps.onKeyDown = (e) => {
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
@ -50,7 +86,7 @@ export default function AccessibleButton(props) {
e.preventDefault();
}
};
restProps.onKeyUp = function(e) {
newProps.onKeyUp = (e) => {
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
@ -64,53 +100,22 @@ export default function AccessibleButton(props) {
}
// Pass through the ref - used for keyboard shortcut access to some buttons
restProps.ref = restProps.inputRef;
delete restProps.inputRef;
newProps.ref = inputRef;
restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
if (kind) {
// We apply a hasKind class to maintain backwards compatibility with
// buttons which might not know about kind and break
restProps.className += " mx_AccessibleButton_hasKind mx_AccessibleButton_kind_" + kind;
}
if (disabled) {
restProps.className += " mx_AccessibleButton_disabled";
restProps["aria-disabled"] = true;
}
newProps.className = classnames(
"mx_AccessibleButton",
className,
{
"mx_AccessibleButton_hasKind": kind,
[`mx_AccessibleButton_kind_${kind}`]: kind,
"mx_AccessibleButton_disabled": disabled,
},
);
// React.createElement expects InputHTMLAttributes
return React.createElement(element, restProps, children);
}
/**
* children: React's magic prop. Represents all children given to the element.
* element: (optional) The base element type. "div" by default.
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
AccessibleButton.propTypes = {
children: PropTypes.node,
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
// The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options.
kind: PropTypes.string,
// The ARIA role
role: PropTypes.string,
// The tabIndex
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.bool,
};
AccessibleButton.defaultProps = {
element: 'div',
role: 'button',

View file

@ -39,6 +39,8 @@ import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType";
import {Capability} from "../../../widgets/WidgetApi";
import {sleep} from "../../../utils/promise";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -341,23 +343,37 @@ export default class AppTile extends React.Component {
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/
_endWidgetActions() {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
let terminationPromise;
if (this._hasCapability(Capability.ReceiveTerminate)) {
// Wait for widget to terminate within a timeout
const timeout = 2000;
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
} else {
terminationPromise = Promise.resolve();
}
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
return terminationPromise.finally(() => {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
});
}
/* If user has permission to modify widgets, delete the widget,
@ -381,12 +397,12 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
this._endWidgetActions();
WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.app.id,
).catch((e) => {
this._endWidgetActions().then(() => {
return WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.app.id,
);
}).catch((e) => {
console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -669,6 +685,17 @@ export default class AppTile extends React.Component {
}
_onPopoutWidgetClick() {
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
this._endWidgetActions().then(() => {
if (this._appFrame.current) {
// Reload iframe
this._appFrame.current.src = this._getRenderedUrl();
this.setState({});
}
});
}
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),

View file

@ -26,6 +26,7 @@ import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks
import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -290,7 +291,7 @@ export default class ReplyThread extends React.Component {
events,
}, this.loadNextEvent);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
render() {

View file

@ -18,11 +18,12 @@ import React from 'react';
import * as sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {Action} from "../../../dispatcher/actions";
const RoomDirectoryButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_room_directory"
<ActionButton action={Action.ViewRoomDirectory}
mouseOverAction={props.callout ? "callout_room_directory" : null}
label={_t("Room directory")}
iconPath={require("../../../../res/img/icons-directory.svg")}

View file

@ -16,63 +16,78 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
export default createReactClass({
displayName: 'SettingsFlag',
propTypes: {
name: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
roomId: PropTypes.string, // for per-room settings
label: PropTypes.string, // untranslated
onChange: PropTypes.func,
isExplicit: PropTypes.bool,
},
interface IProps {
// The setting must be a boolean
name: string;
level: string;
roomId?: string; // for per-room settings
label?: string; // untranslated
isExplicit?: boolean;
// XXX: once design replaces all toggles make this the default
useCheckbox?: boolean;
onChange?(checked: boolean): void;
}
getInitialState: function() {
return {
interface IState {
value: boolean;
}
export default class SettingsFlag extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
value: SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
),
};
},
onChange: function(checked) {
if (this.props.group && !checked) return;
}
}
private onChange = (checked: boolean): void => {
this.save(checked);
this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
},
}
save: function(val = undefined) {
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.onChange(e.target.checked);
}
private save = (val?: boolean): void => {
return SettingsStore.setValue(
this.props.name,
this.props.roomId,
this.props.level,
val !== undefined ? val : this.state.value,
);
},
}
render: function() {
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
let label = this.props.label;
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label);
return (
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{label}</span>
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
</div>
);
},
});
if (this.props.useCheckbox) {
return <StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={!canChange} >
{label}
</StyledCheckbox>;
} else {
return (
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{label}</span>
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
</div>
);
}
}
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classnames from 'classnames';
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
interface IState {
}
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',
}
public render() {
const { children, className, ...otherProps } = this.props;
return <label className={classnames('mx_RadioButton', className)}>
<input type='radio' {...otherProps} />
{/* Used to render the radio button circle */}
<div><div></div></div>
<span>{children}</span>
<div className="mx_RadioButton_spacer" />
</label>
}
}

View file

@ -16,13 +16,23 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import * as sdk from "../../../index";
interface IProps {
// Whether or not this toggle is in the 'on' position.
checked: boolean;
// Whether or not the user can interact with the switch
disabled: boolean;
// Called when the checked state changes. First argument will be the new state.
onChange(checked: boolean): void;
};
// Controlled Toggle Switch element, written with Accessibility in mind
const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
const _onClick = (e) => {
export default ({checked, disabled = false, onChange, ...props}: IProps) => {
const _onClick = () => {
if (disabled) return;
onChange(!checked);
};
@ -46,16 +56,3 @@ const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
</AccessibleButton>
);
};
ToggleSwitch.propTypes = {
// Whether or not this toggle is in the 'on' position.
checked: PropTypes.bool.isRequired,
// Whether or not the user can interact with the switch
disabled: PropTypes.bool,
// Called when the checked state changes. First argument will be the new state.
onChange: PropTypes.func.isRequired,
};
export default ToggleSwitch;

View file

@ -359,6 +359,8 @@ export default class BasicMessageEditor extends React.Component {
}
_onSelectionChange = () => {
const {isEmpty} = this.props.model;
this._refreshLastCaretIfNeeded();
const selection = document.getSelection();
if (this._hasTextSelected && selection.isCollapsed) {
@ -366,7 +368,7 @@ export default class BasicMessageEditor extends React.Component {
if (this._formatBarRef) {
this._formatBarRef.hide();
}
} else if (!selection.isCollapsed) {
} else if (!selection.isCollapsed && !isEmpty) {
this._hasTextSelected = true;
if (this._formatBarRef) {
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();

View file

@ -31,6 +31,7 @@ import {EventStatus} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer";
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
function _isReply(mxEvent) {
const relatesTo = mxEvent.getContent()["m.relates_to"];
@ -157,7 +158,7 @@ export default class EditMessageComposer extends React.Component {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
}
@ -165,7 +166,7 @@ export default class EditMessageComposer extends React.Component {
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
_isContentModified(newContent) {
@ -195,7 +196,7 @@ export default class EditMessageComposer extends React.Component {
// close the event editing and focus composer
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
};
_cancelPreviousPendingEdit() {

View file

@ -0,0 +1,293 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import classNames from "classnames";
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
import { Room } from "matrix-js-sdk/src/models/room";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import * as RoomNotifs from '../../../RoomNotifs';
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
import * as Unread from '../../../Unread';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { EventEmitter } from "events";
import { arrayDiff } from "../../../utils/arrays";
import { IDestroyable } from "../../../utils/IDestroyable";
export const NOTIFICATION_STATE_UPDATE = "update";
export enum NotificationColor {
// Inverted (None -> Red) because we do integer comparisons on this
None, // nothing special
Bold, // no badge, show as unread
Grey, // unread notified messages
Red, // unread pings
}
export interface INotificationState extends EventEmitter {
symbol?: string;
count: number;
color: NotificationColor;
}
interface IProps {
notification: INotificationState;
/**
* If true, the badge will conditionally display a badge without count for the user.
*/
allowNoCount: boolean;
}
interface IState {
}
export default class NotificationBadge extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
if (prevProps.notification) {
prevProps.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
}
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
}
private onNotificationUpdate = () => {
this.forceUpdate(); // notification state changed - update
};
public render(): React.ReactElement {
// Don't show a badge if we don't need to
if (this.props.notification.color <= NotificationColor.Bold) return null;
const hasNotif = this.props.notification.color >= NotificationColor.Red;
const hasCount = this.props.notification.color >= NotificationColor.Grey;
const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount");
let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
if (isEmptyBadge) symbol = "";
const classes = classNames({
'mx_NotificationBadge': true,
'mx_NotificationBadge_visible': hasCount,
'mx_NotificationBadge_highlighted': hasNotif,
'mx_NotificationBadge_dot': isEmptyBadge,
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
'mx_NotificationBadge_3char': symbol.length > 2,
});
return (
<div className={classes}>
<span className="mx_NotificationBadge_count">{symbol}</span>
</div>
);
}
}
export class RoomNotificationState extends EventEmitter implements IDestroyable {
private _symbol: string;
private _count: number;
private _color: NotificationColor;
constructor(private room: Room) {
super();
this.room.on("Room.receipt", this.handleRoomEventUpdate);
this.room.on("Room.timeline", this.handleRoomEventUpdate);
this.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
this.updateNotificationState();
}
public get symbol(): string {
return this._symbol;
}
public get count(): number {
return this._count;
}
public get color(): NotificationColor {
return this._color;
}
private get roomIsInvite(): boolean {
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
}
public destroy(): void {
this.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
}
}
private handleRoomEventUpdate = (event: MatrixEvent) => {
const roomId = event.getRoomId();
if (roomId !== this.room.roomId) return; // ignore - not for us
this.updateNotificationState();
};
private updateNotificationState() {
const before = {count: this.count, symbol: this.symbol, color: this.color};
if (this.roomIsInvite) {
this._color = NotificationColor.Red;
this._symbol = "!";
this._count = 1; // not used, technically
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red
// count. If that count is broken for some reason, assume zero. This avoids us showing
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
// Note: we only set the symbol if we have an actual count. We don't want to show
// zero on badges.
if (redNotifs > 0) {
this._color = NotificationColor.Red;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else if (greyNotifs > 0) {
this._color = NotificationColor.Grey;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);
if (hasUnread) {
this._color = NotificationColor.Bold;
} else {
this._color = NotificationColor.None;
}
// no symbol or count for this state
this._count = 0;
this._symbol = null;
}
}
// finally, publish an update if needed
const after = {count: this.count, symbol: this.symbol, color: this.color};
if (JSON.stringify(before) !== JSON.stringify(after)) {
this.emit(NOTIFICATION_STATE_UPDATE);
}
}
}
export class ListNotificationState extends EventEmitter implements IDestroyable {
private _count: number;
private _color: NotificationColor;
private rooms: Room[] = [];
private states: { [roomId: string]: RoomNotificationState } = {};
constructor(private byTileCount = false) {
super();
}
public get symbol(): string {
return null; // This notification state doesn't support symbols
}
public get count(): number {
return this._count;
}
public get color(): NotificationColor {
return this._color;
}
public setRooms(rooms: Room[]) {
// If we're only concerned about the tile count, don't bother setting up listeners.
if (this.byTileCount) {
this.rooms = rooms;
this.calculateTotalState();
return;
}
const oldRooms = this.rooms;
const diff = arrayDiff(oldRooms, rooms);
this.rooms = rooms;
for (const oldRoom of diff.removed) {
const state = this.states[oldRoom.roomId];
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.destroy();
}
for (const newRoom of diff.added) {
const state = new RoomNotificationState(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
if (this.states[newRoom.roomId]) {
// "Should never happen" disclaimer.
console.warn("Overwriting notification state for room:", newRoom.roomId);
this.states[newRoom.roomId].destroy();
}
this.states[newRoom.roomId] = state;
}
this.calculateTotalState();
}
public destroy() {
for (const state of Object.values(this.states)) {
state.destroy();
}
this.states = {};
}
private onRoomNotificationStateUpdate = () => {
this.calculateTotalState();
};
private calculateTotalState() {
const before = {count: this.count, symbol: this.symbol, color: this.color};
if (this.byTileCount) {
this._color = NotificationColor.Red;
this._count = this.rooms.length;
} else {
this._count = 0;
this._color = NotificationColor.None;
for (const state of Object.values(this.states)) {
this._count += state.count;
this._color = Math.max(this.color, state.color);
}
}
// finally, publish an update if needed
const after = {count: this.count, symbol: this.symbol, color: this.color};
if (JSON.stringify(before) !== JSON.stringify(after)) {
this.emit(NOTIFICATION_STATE_UPDATE);
}
}
}

View file

@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor";
import * as sdk from "../../../index";
import {toRem} from "../../../utils/units";
import {toPx} from "../../../utils/units";
let bounce = false;
try {
@ -149,7 +149,7 @@ export default createReactClass({
// start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px",
left: toRem(oldInfo.left) });
left: toPx(oldInfo.left) });
const reorderTransitionOpts = {
duration: 100,
@ -182,7 +182,7 @@ export default createReactClass({
}
const style = {
left: toRem(this.props.leftOffset),
left: toPx(this.props.leftOffset),
top: '0px',
visibility: this.props.hidden ? 'hidden' : 'visible',
};

View file

@ -0,0 +1,125 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition, TransitionGroup } from "react-transition-group";
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps {
}
interface IState {
// Both of these control the animation for the breadcrumbs. For details on the
// actual animation, see the CSS.
//
// doAnimation is to lie to the CSSTransition component (see onBreadcrumbsUpdate
// for info). skipFirst is used to try and reduce jerky animation - also see the
// breadcrumb update function for info on that.
doAnimation: boolean;
skipFirst: boolean;
}
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> {
private isMounted = true;
constructor(props: IProps) {
super(props);
this.state = {
doAnimation: true, // technically we want animation on mount, but it won't be perfect
skipFirst: false, // render the thing, as boring as it is
};
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
public componentWillUnmount() {
this.isMounted = false;
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
private onBreadcrumbsUpdate = () => {
if (!this.isMounted) return;
// We need to trick the CSSTransition component into updating, which means we need to
// tell it to not animate, then to animate a moment later. This causes two updates
// which means two renders. The skipFirst change is so that our don't-animate state
// doesn't show the breadcrumb we're about to reveal as it causes a visual jump/jerk.
// The second update, on the next available tick, causes the "enter" animation to start
// again and this time we want to show the newest breadcrumb because it'll be hidden
// off screen for the animation.
this.setState({doAnimation: false, skipFirst: true});
setTimeout(() => this.setState({doAnimation: true, skipFirst: false}), 0);
};
private viewRoom = (room: Room, index: number) => {
Analytics.trackEvent("Breadcrumbs", "click_node", index);
defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
};
public render(): React.ReactElement {
// TODO: Decorate crumbs with icons
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
return (
<AccessibleButton
className="mx_RoomBreadcrumbs2_crumb"
key={r.roomId}
onClick={() => this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})}
>
<RoomAvatar room={r} width={32} height={32}/>
</AccessibleButton>
)
});
if (tiles.length > 0) {
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition
appear={true} in={this.state.doAnimation} timeout={640}
classNames='mx_RoomBreadcrumbs2'
>
<div className='mx_RoomBreadcrumbs2'>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
</div>
</CSSTransition>
);
} else {
return (
<div className='mx_RoomBreadcrumbs2'>
<div className="mx_RoomBreadcrumbs2_placeholder">
{_t("No recently visited rooms")}
</div>
</div>
);
}
}
}

View file

@ -18,18 +18,17 @@ limitations under the License.
import * as React from "react";
import { _t, _td } from "../../../languageHandler";
import { Layout } from '../../../resizer/distributors/roomsublist2';
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { Dispatcher } from "flux";
import dis from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
/*******************************************************************
* CAUTION *
@ -46,10 +45,12 @@ interface IProps {
resizeNotifier: ResizeNotifier;
collapsed: boolean;
searchFilter: string;
isMinimized: boolean;
}
interface IState {
sublists: ITagMap;
layouts: Map<TagID, ListLayout>;
}
const TAG_ORDER: TagID[] = [
@ -96,7 +97,7 @@ const TAG_AESTHETICS: {
defaultHidden: false,
},
[DefaultTagID.DM]: {
sectionLabel: _td("Direct Messages"),
sectionLabel: _td("People"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Start chat"),
@ -127,19 +128,15 @@ const TAG_AESTHETICS: {
};
export default class RoomList2 extends React.Component<IProps, IState> {
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
private sublistSizes: { [tagId: string]: number } = {};
private sublistCollapseStates: { [tagId: string]: boolean } = {};
private unfilteredLayout: Layout;
private filteredLayout: Layout;
private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) {
super(props);
this.state = {sublists: {}};
this.loadSublistSizes();
this.prepareLayouts();
this.state = {
sublists: {},
layouts: new Map<TagID, ListLayout>(),
};
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
@ -158,49 +155,16 @@ export default class RoomList2 extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
console.log("new lists", store.orderedLists);
this.setState({sublists: store.orderedLists});
});
}
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
const newLists = store.orderedLists;
console.log("new lists", newLists);
private loadSublistSizes() {
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
}
private saveSublistSizes() {
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
}
private prepareLayouts() {
// TODO: Change layout engine for FTUE support
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
const sublist = this.sublistRefs[tagId];
if (sublist) sublist.current.setHeight(height);
// TODO: Check overflow (see old impl)
// Don't store a height for collapsed sublists
if (!this.sublistCollapseStates[tagId]) {
this.sublistSizes[tagId] = height;
this.saveSublistSizes();
const layoutMap = new Map<TagID, ListLayout>();
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
}, this.sublistSizes, this.sublistCollapseStates, {
allowWhitespace: false,
handleHeight: 1,
});
this.filteredLayout = new Layout((tagId: string, height: number) => {
const sublist = this.sublistRefs[tagId];
if (sublist) sublist.current.setHeight(height);
}, null, null, {
allowWhitespace: false,
handleHeight: 0,
this.setState({sublists: newLists, layouts: layoutMap});
});
}
@ -226,16 +190,20 @@ export default class RoomList2 extends React.Component<IProps, IState> {
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
components.push(<RoomSublist2
key={`sublist-${orderedTagId}`}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite}
/>);
components.push(
<RoomSublist2
key={`sublist-${orderedTagId}`}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized}
/>
);
}
return components;
@ -250,7 +218,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
onKeyDown={onKeyDownHandler}
className="mx_RoomList"
className="mx_RoomList2"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to

View file

@ -20,14 +20,18 @@ import * as React from "react";
import { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames';
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
import * as RoomNotifs from '../../../RoomNotifs';
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton";
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
import * as FormattingUtils from '../../../utils/FormattingUtils';
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import StyledCheckbox from "../elements/StyledCheckbox";
import StyledRadioButton from "../elements/StyledRadioButton";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
/*******************************************************************
* CAUTION *
@ -45,9 +49,10 @@ interface IProps {
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout: ListLayout;
isMinimized: boolean;
// TODO: Collapsed state
// TODO: Height
// TODO: Group invites
// TODO: Calls
// TODO: forceExpand?
@ -56,17 +61,22 @@ interface IProps {
}
interface IState {
notificationState: ListNotificationState;
menuDisplayed: boolean;
}
export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef();
private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
public setHeight(size: number) {
// TODO: Do a thing (maybe - height changes are different in FTUE)
}
constructor(props: IProps) {
super(props);
private hasTiles(): boolean {
return this.numTiles > 0;
this.state = {
notificationState: new ListNotificationState(this.props.isInvite),
menuDisplayed: false,
};
this.state.notificationState.setRooms(this.props.rooms);
}
private get numTiles(): number {
@ -74,43 +84,149 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
return (this.props.rooms || []).length;
}
public componentDidUpdate() {
this.state.notificationState.setRooms(this.props.rooms);
}
public componentWillUnmount() {
this.state.notificationState.destroy();
}
private onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
};
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
const direction = e.movementY < 0 ? -1 : +1;
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
this.props.layout.visibleTiles += tileDiff;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onShowAllClick = () => {
this.props.layout.visibleTiles = this.numTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({menuDisplayed: true});
};
private onCloseMenu = () => {
this.setState({menuDisplayed: false});
};
private onUnreadFirstChanged = async () => {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm);
};
private onTagSortChanged = async (sort: SortAlgorithm) => {
await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort);
};
private onMessagePreviewChanged = () => {
this.props.layout.showPreviews = !this.props.layout.showPreviews;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private renderTiles(): React.ReactElement[] {
const tiles: React.ReactElement[] = [];
if (this.props.rooms) {
for (const room of this.props.rooms) {
tiles.push(<RoomTile2 room={room} key={`room-${room.roomId}`}/>);
tiles.push(
<RoomTile2
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.props.layout.showPreviews}
isMinimized={this.props.isMinimized}
/>
);
}
}
return tiles;
}
private renderHeader(): React.ReactElement {
const notifications = !this.props.isInvite
? RoomNotifs.aggregateNotificationCount(this.props.rooms)
: {count: 0, highlight: true};
const notifCount = notifications.count;
const notifHighlight = notifications.highlight;
private renderMenu(): React.ReactElement {
let contextMenu = null;
if (this.state.menuDisplayed) {
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic;
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance;
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu}
>
<div className="mx_RoomSublist2_contextMenu">
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
<StyledRadioButton
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
checked={!isAlphabetical}
name={`mx_${this.props.layout.tagId}_sortBy`}
>
{_t("Activity")}
</StyledRadioButton>
<StyledRadioButton
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
checked={isAlphabetical}
name={`mx_${this.props.layout.tagId}_sortBy`}
>
{_t("A-Z")}
</StyledRadioButton>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
<StyledCheckbox
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Always show first")}
</StyledCheckbox>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
<StyledCheckbox
onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews}
>
{_t("Message preview")}
</StyledCheckbox>
</div>
</div>
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuButton
className="mx_RoomSublist2_menuButton"
onClick={this.onOpenMenuClick}
inputRef={this.menuButtonRef}
label={_t("List options")}
isExpanded={this.state.menuDisplayed}
/>
{contextMenu}
</React.Fragment>
);
}
private renderHeader(): React.ReactElement {
// TODO: Title on collapsed
// TODO: Incoming call box
let chevron = null;
if (this.hasTiles()) {
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': false, // isCollapsed
'mx_RoomSubList_chevronDown': true, // !isCollapsed
});
chevron = (<div className={chevronClasses}/>);
}
return (
<RovingTabIndexWrapper inputRef={this.headerButton}>
{({onFocus, isActive, ref}) => {
@ -118,68 +234,43 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const tabIndex = isActive ? 0 : -1;
// TODO: Collapsed state
let badge;
if (true) { // !isCollapsed
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': notifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (notifCount > 0) {
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
aria-label={_t("Jump to first unread room.")}
>
<div>
{FormattingUtils.formatCount(notifCount)}
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.hasTiles()) {
// Render the `!` badge for invites
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
aria-label={_t("Jump to first invite.")}
>
<div>
{FormattingUtils.formatCount(this.numTiles)}
</div>
</AccessibleButton>
);
}
}
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
let addRoomButton = null;
if (!!this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
<AccessibleButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
className="mx_RoomSublist2_auxButton"
aria-label={this.props.addRoomLabel || _t("Add room")}
/>
);
}
const classes = classNames({
'mx_RoomSublist2_headerContainer': true,
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
});
// TODO: a11y (see old component)
return (
<div className={"mx_RoomSubList_labelContainer"}>
<div className={classes}>
<AccessibleButton
inputRef={ref}
tabIndex={tabIndex}
className={"mx_RoomSubList_label"}
className={"mx_RoomSublist2_headerText"}
role="treeitem"
aria-level="1"
aria-level={1}
>
{chevron}
<span>{this.props.label}</span>
</AccessibleButton>
{badge}
{this.renderMenu()}
{addRoomButton}
<div className="mx_RoomSublist2_badgeContainer">
{badge}
</div>
</div>
);
}}
@ -195,19 +286,87 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const classes = classNames({
// TODO: Proper collapse support
'mx_RoomSubList': true,
'mx_RoomSubList_hidden': false, // len && isCollapsed
'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed
'mx_RoomSublist2': true,
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
'mx_RoomSublist2_minimized': this.props.isMinimized,
});
let content = null;
if (tiles.length > 0) {
const layout = this.props.layout; // to shorten calls
// TODO: Lazy list rendering
// TODO: Whatever scrolling magic needs to happen here
const nVisible = Math.floor(layout.visibleTiles);
const visibleTiles = tiles.slice(0, nVisible);
// If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present
let showMoreButton = null;
if (tiles.length > nVisible) {
// we have a cutoff condition - add the button to show all
const numMissing = tiles.length - visibleTiles.length;
let showMoreText = (
<span className='mx_RoomSublist2_showMoreButtonText'>
{_t("Show %(count)s more", {count: numMissing})}
</span>
);
if (this.props.isMinimized) showMoreText = null;
showMoreButton = (
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showMoreButton'>
<span className='mx_RoomSublist2_showMoreButtonChevron'>
{/* set by CSS masking */}
</span>
{showMoreText}
</div>
);
}
// Figure out if we need a handle
let handles = ['s'];
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
handles = []; // no handles, we're at a minimum
}
// We have to account for padding so we can accommodate a 'show more' button and
// the resize handle, which are pinned to the bottom of the container. This is the
// easiest way to have a resize handle below the button as otherwise we're writing
// our own resize handling and that doesn't sound fun.
//
// The layout class has some helpers for dealing with padding, as we don't want to
// apply it in all cases. If we apply it in all cases, the resizing feels like it
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
// only mathematically 7 possible).
const showMoreHeight = 32; // As defined by CSS
const resizeHandleHeight = 4; // As defined by CSS
// The padding is variable though, so figure out what we need padding for.
let padding = 0;
if (showMoreButton) padding += showMoreHeight;
if (handles.length > 0) padding += resizeHandleHeight;
const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding);
content = (
<IndicatorScrollbar className='mx_RoomSubList_scroll'>
{tiles}
</IndicatorScrollbar>
<ResizableBox
width={-1}
height={tilesPx}
axis="y"
minConstraints={[-1, minTilesPx]}
maxConstraints={[-1, maxTilesPx]}
resizeHandles={handles}
onResize={this.onResize}
className="mx_RoomSublist2_resizeBox"
>
{visibleTiles}
{showMoreButton}
</ResizableBox>
)
}

View file

@ -21,17 +21,16 @@ import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar";
import Tooltip from "../../views/elements/Tooltip";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import * as RoomNotifs from '../../../RoomNotifs';
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
import * as Unread from '../../../Unread';
import * as FormattingUtils from "../../../utils/FormattingUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge";
import { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
/*******************************************************************
* CAUTION *
@ -41,34 +40,26 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
* warning disappears. *
*******************************************************************/
enum NotificationColor {
// Inverted (None -> Red) because we do integer comparisons on this
None, // nothing special
Bold, // no badge, show as unread
Grey, // unread notified messages
Red, // unread pings
}
interface IProps {
room: Room;
showMessagePreview: boolean;
isMinimized: boolean;
// TODO: Allow falsifying counts (for invites and stuff)
// TODO: Transparency? Was this ever used?
// TODO: Incoming call boxes?
}
interface INotificationState {
symbol: string;
color: NotificationColor;
}
interface IState {
hover: boolean;
notificationState: INotificationState;
selected: boolean;
generalMenuDisplayed: boolean;
}
export default class RoomTile2 extends React.Component<IProps, IState> {
private roomTile = createRef();
private roomTileRef: React.RefObject<HTMLDivElement> = createRef();
private generalMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
// TODO: Custom status
// TODO: Lock icon
@ -86,86 +77,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.state = {
hover: false,
notificationState: this.getNotificationState(),
notificationState: new RoomNotificationState(this.props.room),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
generalMenuDisplayed: false,
};
this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
public componentWillUnmount() {
if (this.props.room) {
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
}
}
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
// this, but instead we're kinda forced to either duplicate the code or thread a variable
// through the code paths. This feels like the least evil option.
private get roomIsInvite(): boolean {
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
}
private handleRoomEventUpdate = (event: MatrixEvent) => {
const roomId = event.getRoomId();
// Sanity check: should never happen
if (roomId !== this.props.room.roomId) return;
this.updateNotificationState();
};
private updateNotificationState() {
this.setState({notificationState: this.getNotificationState()});
}
private getNotificationState(): INotificationState {
const state: INotificationState = {
color: NotificationColor.None,
symbol: null,
};
if (this.roomIsInvite) {
state.color = NotificationColor.Red;
state.symbol = "!";
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total');
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red
// count. If that count is broken for some reason, assume zero. This avoids us showing
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
// Note: we only set the symbol if we have an actual count. We don't want to show
// zero on badges.
if (redNotifs > 0) {
state.color = NotificationColor.Red;
state.symbol = FormattingUtils.formatCount(trueCount);
} else if (greyNotifs > 0) {
state.color = NotificationColor.Grey;
state.symbol = FormattingUtils.formatCount(trueCount);
} else {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room);
if (hasUnread) {
state.color = NotificationColor.Bold;
// no symbol for this state
}
}
}
return state;
}
private onTileMouseEnter = () => {
@ -186,63 +109,188 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
});
};
private onActiveRoomUpdate = (isActive: boolean) => {
this.setState({selected: isActive});
};
private onGeneralMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: true});
};
private onCloseGeneralMenu = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: false});
};
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.DM) {
// TODO: DM Flagging
} else {
// TODO: XOR favourites and low priority
}
};
private onLeaveRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
};
private onOpenRoomSettings = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
};
private renderGeneralMenu(): React.ReactElement {
if (this.props.isMinimized) return null; // no menu when minimized
let contextMenu = null;
if (this.state.generalMenuDisplayed) {
// The context menu appears within the list, so use the room tile as a reference point
const elementRect = this.roomTileRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height + 8}
onFinished={this.onCloseGeneralMenu}
>
<div
className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"
style={{width: elementRect.width}}
>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span>{_t("Favourite")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown" />
<span>{_t("Low Priority")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser" />
<span>{_t("Direct Chat")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span>{_t("Settings")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span>{_t("Leave Room")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuButton
className="mx_RoomTile2_menuButton"
onClick={this.onGeneralMenuOpenClick}
inputRef={this.generalMenuButtonRef}
label={_t("Room options")}
isExpanded={this.state.generalMenuDisplayed}
/>
{contextMenu}
</React.Fragment>
)
}
public render(): React.ReactElement {
// TODO: Collapsed state
// TODO: Invites
// TODO: a11y proper
// TODO: Render more than bare minimum
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
const isUnread = this.state.notificationState.color > NotificationColor.None;
const classes = classNames({
'mx_RoomTile': true,
// 'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': isUnread,
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
'mx_RoomTile_invited': this.roomIsInvite,
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_noBadges': !hasBadge,
// 'mx_RoomTile_transparent': this.props.transparent,
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected,
'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed,
'mx_RoomTile2_minimized': this.props.isMinimized,
});
const avatarClasses = classNames({
'mx_RoomTile_avatar': true,
});
let badge;
if (hasBadge) {
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
});
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>;
}
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
// TODO: the original RoomTile uses state for the room name. Do we need to?
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.roomIsInvite,
'mx_RoomTile_badgeShown': hasBadge,
});
// TODO: Support collapsed state properly
let tooltip = null;
if (false) { // isCollapsed
if (this.state.hover) {
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} />
// TODO: Tooltip?
let messagePreview = null;
if (this.props.showMessagePreview && !this.props.isMinimized) {
// The preview store heavily caches this info, so should be safe to hammer.
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
// Only show the preview if there is one to show.
if (text) {
messagePreview = (
<div className="mx_RoomTile2_messagePreview">
{text}
</div>
);
}
}
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
});
let nameContainer = (
<div className="mx_RoomTile2_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
{messagePreview}
</div>
);
if (this.props.isMinimized) nameContainer = null;
const avatarSize = 32;
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTile}>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
@ -254,20 +302,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onClick={this.onTileClick}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24}/>
</div>
<div className="mx_RoomTile2_avatarContainer">
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
</div>
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
</div>
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
{tooltip}
{this.renderGeneralMenu()}
</AccessibleButton>
}
</RovingTabIndexWrapper>

View file

@ -44,6 +44,7 @@ import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -364,7 +365,7 @@ export default class SendMessageComposer extends React.Component {
onAction = (payload) => {
switch (payload.action) {
case 'reply_to_event':
case 'focus_composer':
case Action.FocusComposer:
this._editorRef && this._editorRef.focus();
break;
case 'insert_mention':
@ -426,7 +427,9 @@ export default class SendMessageComposer extends React.Component {
_onPaste = (event) => {
const {clipboardData} = event;
if (clipboardData.files.length) {
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied.
if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
// This actually not so much for 'files' as such (at time of writing
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website

View file

@ -62,7 +62,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
super(props);
this.state = {
fontSize: SettingsStore.getValue("fontSize", null).toString(),
fontSize: (SettingsStore.getValue("baseFontSize", null) + FontWatcher.SIZE_DIFF).toString(),
...this.calculateThemeState(),
customThemeUrl: "",
customThemeMessage: {isError: false, text: ""},
@ -132,13 +132,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
private onFontSizeChanged = (size: number): void => {
this.setState({fontSize: size.toString()});
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, size - FontWatcher.SIZE_DIFF);
};
private onValidateFontSize = async ({value}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
const parsedSize = parseFloat(value);
const min = FontWatcher.MIN_SIZE;
const max = FontWatcher.MAX_SIZE;
const min = FontWatcher.MIN_SIZE + FontWatcher.SIZE_DIFF;
const max = FontWatcher.MAX_SIZE + FontWatcher.SIZE_DIFF;
if (isNaN(parsedSize)) {
return {valid: false, feedback: _t("Size must be a number")};
@ -151,7 +151,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
};
}
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value);
SettingsStore.setValue(
"baseFontSize",
null,
SettingLevel.DEVICE,
parseInt(value, 10) - FontWatcher.SIZE_DIFF
);
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
}
@ -275,7 +281,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
values={[13, 15, 16, 18, 20]}
value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this.onFontSizeChanged}
displayFunc={value => ""}
displayFunc={_ => ""}
disabled={this.state.useCustomFontSize}
/>
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
@ -284,9 +290,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked) => this.setState({useCustomFontSize: checked})}
useCheckbox={true}
/>
<Field
type="text"
type="number"
label={_t("Font size")}
autoComplete="off"
placeholder={this.state.fontSize.toString()}
@ -295,6 +302,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
onValidate={this.onValidateFontSize}
onChange={(value) => this.setState({fontSize: value.target.value})}
disabled={!this.state.useCustomFontSize}
className="mx_SettingsTab_customFontSizeField"
/>
</div>;
}

View file

@ -36,9 +36,15 @@ export enum Action {
/**
* Open the user settings. No additional payload information required.
* Optionally can include an OpenToTabPayload.
*/
ViewUserSettings = "view_user_settings",
/**
* Opens the room directory. No additional payload information required.
*/
ViewRoomDirectory = "view_room_directory",
/**
* Sets the current tooltip. Should be use with ViewTooltipPayload.
*/
@ -53,4 +59,14 @@ export enum Action {
* Provide status information for an ongoing update check. Should be used with a CheckUpdatesPayload.
*/
CheckUpdates = "check_updates",
/**
* Focuses the user's cursor to the composer. No additional payload information required.
*/
FocusComposer = "focus_composer",
/**
* Opens the user menu (previously known as the top left menu). No additional payload information required.
*/
ToggleUserMenu = "toggle_user_menu",
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface OpenToTabPayload extends ActionPayload {
action: Action.ViewUserSettings | string, // TODO: Add room settings action
/**
* The tab ID to open in the settings view to start, if possible.
*/
initialTabId?: string;
}

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// @ts-ignore - import * as EMOJIBASE actually breaks this
import EMOJIBASE from 'emojibase-data/en/compact.json';
export interface IEmoji {
@ -63,6 +62,8 @@ export const DATA_BY_CATEGORY = {
"flags": [],
};
const ZERO_WIDTH_JOINER = "\u200D";
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects
EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
@ -70,7 +71,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
DATA_BY_CATEGORY[categoryId].push(emoji);
}
// This is used as the string to match the query against when filtering emojis
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` +
`${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`.toLowerCase();
// Add mapping from unicode to Emoji object
// The 'unicode' field that we use in emojibase has either

View file

@ -2324,5 +2324,127 @@
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.": "<showSessionsText>Покажи сесиите</showSessionsText>, <sendAnywayText>изпрати така или иначе</sendAnywayText> или <cancelText>откажи</cancelText>.",
"Verify this login": "Потвърди тази сесия",
"Session verified": "Сесията беше потвърдена",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Промяната на паролата ще нулира всички ключове за шифроване от-край-до-край по всички ваши сесии, правейки шифрованата история на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете на стаите от друга сесия преди да промените паролата си."
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Промяната на паролата ще нулира всички ключове за шифроване от-край-до-край по всички ваши сесии, правейки шифрованата история на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете на стаите от друга сесия преди да промените паролата си.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Администраторът на сървъра е изключиш шифроване от край-до-край по подразбиране за лични стаи и за директни съобщения.",
"Emoji picker": "Избор на емоджи",
"People": "Хора",
"Show %(n)s more": "Покажи още %(n)s",
"Switch to light mode": "Смени на светъл режим",
"Switch to dark mode": "Смени на тъмен режим",
"Switch theme": "Смени темата",
"Security & privacy": "Сигурност и поверителност",
"All settings": "Всички настройки",
"Archived rooms": "Архивирани стаи",
"Feedback": "Обратна връзка",
"Account settings": "Настройки на профила",
"You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Излязохте от всички сесии и вече няма да получавате push уведомления. За да включите уведомленията пак, влезте наново от всяко устройство.",
"Syncing...": "Синхронизиране...",
"Signing In...": "Влизане...",
"If you've joined lots of rooms, this might take a while": "Това може да отнеме известно време, ако сте в много стаи",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Потвърдете идентичността на този вход чрез верифицирането му от някоя от другите ви сесии, давайки достъп до шифрованите съобщения.",
"This requires the latest Riot on your other devices:": "Това изисква най-новата версия на Riot на другите ви устройства:",
"or another cross-signing capable Matrix client": "или друг Matrix клиент поддържащ кръстосано-подписване",
"Use Recovery Passphrase or Key": "Използвай парола за възстановяване или ключ",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Сесията ви е потвърдена. Тя има достъп до шифрованите ви съобщения. Други потребители я виждат като доверена.",
"Your new session is now verified. Other users will see it as trusted.": "Новата ви сесия е потвърдена. Другите потребители ще я виждат като доверена.",
"Without completing security on this session, it wont have access to encrypted messages.": "Без да повишите сигурността на тази сесия, няма да имате достъп до шифрованите съобщения.",
"Go Back": "Върни се",
"Regain access to your account and recover encryption keys stored in this session. Without them, you wont be able to read all of your secure messages in any session.": "Възвърнете достъп до профила си и до ключовете за шифроване съхранени в тази сесия. Без тях няма да можете да четете всички защитени съобщения в коя да е сесия.",
"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.": "Внимание: личните ви данни (включително ключове за шифроване) все още се съхраняват в тази сесия. Изчистете ги, ако сте приключили със сесията или искате да влезете в друг профил.",
"Confirm encryption setup": "Потвърждение на настройки за шифроване",
"Click the button below to confirm setting up encryption.": "Кликнете бутона по-долу за да потвърдите настройването на шифроване.",
"Enter your account password to confirm the upgrade:": "Въведете паролата за профила си за да потвърдите обновлението:",
"Restore your key backup to upgrade your encryption": "Възстановете резервното копие на ключа за да обновите шифроването",
"Restore": "Възстанови",
"You'll need to authenticate with the server to confirm the upgrade.": "Ще трябва да се автентикирате пред сървъра за да потвърдите обновяването.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Обновете тази сесия, за да може да потвърждава други сесии, давайки им достъп до шифрованите съобщения и маркирайки ги като доверени за другите потребители.",
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Настройте парола за възстановяване за да защитите шифрованата информация и за да я възстановите, ако излезете от профила. По-добре е да използвате парола различна от тази за профила ви:",
"Enter a recovery passphrase": "Въведете парола за възстановяване",
"Great! This recovery passphrase looks strong enough.": "Чудесно! Тази парола за възстановяване изглежда достатъчно силна.",
"Back up encrypted message keys": "Правене на резервно копие на ключовете за шифроване",
"Use a different passphrase?": "Използвай друга парола?",
"Enter your recovery passphrase a second time to confirm it.": "Въведете паролата за възстановяване още веднъж за да потвърдите.",
"Confirm your recovery passphrase": "Потвърдете паролата за възстановяване",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Ключа за възстановяване е допълнителна гаранция - може да го използвате вместо паролата за възстановяване за да възстановите достъп до шифрованите съобщения.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Направете копие на сигурно място, например password manager или сейф.",
"Your recovery key": "Ключът ви за възстановяване",
"Copy": "Копирай",
"Unable to query secret storage status": "Неуспешно допитване за състоянието на секретното складиране",
"You can now verify your other devices, and other users to keep your chats safe.": "Вече може да потвърждавате другите си устройства и другите потребители, за да пазите чатовете си сигурни.",
"Upgrade your encryption": "Обновете шифроването",
"Confirm recovery passphrase": "Потвърдете паролата за възстановяване",
"Make a copy of your recovery key": "Направете копие на ключа за възстановяване",
"You're done!": "Готови сте!",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Ще съхраним шифровано копие на ключовете ви на сървъра. Защитете резервното копие с парола за възстановяване.",
"Please enter your recovery passphrase a second time to confirm.": "Въведете паролата за възстановяване отново за да потвърдите.",
"Repeat your recovery passphrase...": "Повторете паролата си за възстановяване...",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Ако не настройте защитено възстановяване на съобщения, няма да можете да възстановите историята на шифрованите си съобщения при излизане от профила или при използване на друга сесия.",
"Secure your backup with a recovery passphrase": "Защитете профила си с парола за възстановяване",
"Create key backup": "Създай резервно копие на ключовете",
"This session is encrypting history using the new recovery method.": "Тази сесия шифрова историята използвайки новия метод за възстановяване.",
"This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Тази сесия установи, че паролата за възстановяване и ключа за сигурни съобщения са били премахнати.",
"If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Ако сте направили това без да искате, може да настройте защитени съобщения за тази сесия, което ще зашифрова наново историята на съобщенията използвайки новия метод за възстановяване.",
"If disabled, messages from encrypted rooms won't appear in search results.": "Ако е изключено, съобщения от шифровани стаи няма да се показват в резултатите от търсения.",
"Disable": "Изключи",
"Not currently indexing messages for any room.": "В момента не се индексират съобщения в нито една стая.",
"Currently indexing: %(currentRoom)s": "В момента се индексира: %(currentRoom)s",
"Riot is securely caching encrypted messages locally for them to appear in search results:": "Riot кешира шифровани съобщения локално по сигурен начин, за да може те да се появяват в резултати от търсения:",
"Space used:": "Използвано пространство:",
"Indexed messages:": "Индексирани съобщения:",
"Indexed rooms:": "Индексирани стаи:",
"%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s от %(totalRooms)s",
"Message downloading sleep time(ms)": "Период на пауза между свалянията на съобщения (ms)",
"Navigation": "Навигация",
"Calls": "Обаждания",
"Room List": "Списък със стаи",
"Autocomplete": "Подсказване",
"Alt": "Alt",
"Alt Gr": "Alt Gr",
"Shift": "Shift",
"Super": "Super",
"Ctrl": "Ctrl",
"Toggle Bold": "Превключи удебеляването",
"Toggle Italics": "Превключи накланянето",
"Toggle Quote": "Превключи цитирането",
"New line": "Нов ред",
"Navigate recent messages to edit": "Движение из скорошни съобщения за редактиране",
"Jump to start/end of the composer": "Прескачане в началото/края на полето за писане",
"Navigate composer history": "Движение из историята на полето за писане",
"Cancel replying to a message": "Отказване на отговарянето на съобщение",
"Toggle microphone mute": "Превключване на заглушаването на микрофона",
"Toggle video on/off": "Превключване на видеото (вкл./изкл.)",
"Scroll up/down in the timeline": "Движение нагоре/надолу из съобщенията",
"Dismiss read marker and jump to bottom": "Игнориране на маркера за прочитане и отиване най-долу",
"Jump to oldest unread message": "Прескачане до най-старото непрочетено съобщение",
"Upload a file": "Качване на файл",
"Jump to room search": "Търсене на стаи",
"Navigate up/down in the room list": "Движение нагоре/надолу в списъка със стаи",
"Select room from the room list": "Избор на стая от списъка",
"Collapse room list section": "Свиване на раздел със стаи",
"Expand room list section": "Разширение на раздел със стаи",
"Clear room list filter field": "Изчистване на полето за филтрирането на списъка със стаи",
"Previous/next unread room or DM": "Предишна/следваща непрочетена стая или директно съобщение",
"Previous/next room or DM": "Предишна/следваща стая или директно съобщение",
"Toggle the top left menu": "Превключва основното меню (горе в ляво)",
"Close dialog or context menu": "Затваряне на прозорец или контекстно меню",
"Activate selected button": "Активиране на избрания бутон",
"Toggle right panel": "Превключване на десния панел",
"Toggle this dialog": "Превключване на този прозорец",
"Move autocomplete selection up/down": "Движение нагоре/надолу из подсказките",
"Cancel autocomplete": "Отказване на подсказките",
"Page Up": "Page Up",
"Page Down": "Page Down",
"Esc": "Esc",
"Enter": "Enter",
"Space": "Space",
"End": "End",
"sent an image.": "изпрати снимка.",
"You: %(message)s": "Вие: %(message)s",
"No recently visited rooms": "Няма наскоро-посетени стаи",
"Sort by": "Подреди по",
"Activity": "Активност",
"A-Z": "Азбучен ред",
"Unread rooms": "Непрочетени стаи",
"Show %(count)s more|other": "Покажи още %(count)s",
"Show %(count)s more|one": "Покажи още %(count)s"
}

View file

@ -412,7 +412,7 @@
"Seen by %(userName)s at %(dateTime)s": "Gesehen von %(userName)s um %(dateTime)s",
"Send anyway": "Trotzdem senden",
"Start authentication": "Authentifizierung beginnen",
"This room": "In diesem Raum",
"This room": "diesen Raum",
"unknown caller": "Unbekannter Anrufer",
"Unnamed Room": "Unbenannter Raum",
"Upload new:": "Neue(s) hochladen:",
@ -1569,7 +1569,7 @@
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Ihr Passwort wurde erfolgreich geändert. Sie erhalten keine Push-Benachrichtigungen zu anderen Sitzungen, bis Sie sich wieder bei diesen anmelden",
"Sessions": "Sitzungen",
"Notification sound": "Benachrichtigungston",
"Set a new custom sound": "Setze einen neuen benutzerdefinierten Sound",
"Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
"Browse": "Durchsuche",
"Direct Messages": "Direktnachrichten",
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Sie können <code>/help</code> benutzen, um verfügbare Befehle aufzulisten. Wollten Sie dies als Nachricht senden?",
@ -1584,8 +1584,8 @@
"To help us prevent this in future, please <a>send us logs</a>.": "Um uns zu helfen, dies in Zukunft zu vermeiden, <a>senden Sie uns bitte Logs</a>.",
"We recommend you go through the verification process for each session to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Wir empfehlen Ihnen, den Verifizierungsprozess für jede Sitzung zu durchlaufen, um zu bestätigen, dass sie ihrem rechtmäßigen Eigentümer gehören, aber Sie können die Nachricht auch ohne Verifizierung erneut senden, wenn Sie dies bevorzugen.",
"Notification settings": "Benachrichtigungseinstellungen",
"Help": "Hilfe",
"Filter": "Filter",
"Help": "Hilf uns",
"Filter": "Filtern",
"Filter rooms…": "Räume filtern…",
"You have %(count)s unread notifications in a prior version of this room.|one": "Sie haben %(count)s ungelesene Benachrichtigungen in einer früheren Version dieses Raumes.",
"Go Back": "Gehe zurück",
@ -1793,7 +1793,7 @@
"Use Single Sign On to continue": "Benutze „Single Sign-On“ (Einmalanmeldung) um fortzufahren",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse mit „Single Sign-On“, um deine Identität nachzuweisen.",
"Single Sign On": "Single Sign-On",
"Confirm adding email": "Bestätige das Hinzufügen der Email-Adresse",
"Confirm adding email": "Hinzufügen der E-Mail-Adresse bestätigen",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser Telefonnummer, indem du deine Identität mittels „Single Sign-On“ nachweist.",
"Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.",
"If you cancel now, you won't complete your operation.": "Wenn du jetzt abbrichst, wirst du deinen Vorgang nicht fertigstellen.",
@ -1802,7 +1802,7 @@
"Command failed": "Befehl fehlgeschlagen",
"Could not find user in room": "Der Benutzer konnte im Raum nicht gefunden werden",
"Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
"Confirm adding phone number": "Bestätige das Hinzufügen der Telefonnummer",
"Confirm adding phone number": "Hinzufügen der Telefonnummer bestätigen",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s erneuert eine Ausschluss-Regel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
"Not Trusted": "Nicht vertrauenswürdig",
@ -1985,7 +1985,7 @@
"This invite to %(roomName)s was sent to %(email)s": "Diese Einladung zu %(roomName)s wurde an %(email)s gesendet",
"Use an identity server in Settings to receive invites directly in Riot.": "Verknüpfe einen Identitätsserver in den Einstellungen um die Einladungen direkt in Riot zu erhalten.",
"Share this email in Settings to receive invites directly in Riot.": "Teile diese E-Mail-Adresse in den Einstellungen um Einladungen direkt in Riot zu erhalten.",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s kann keine Vorschau erzeugt werden. Möchtest du den Raum betreten?",
"%(roomName)s can't be previewed. Do you want to join it?": "Für %(roomName)s kann keine Vorschau erzeugt werden. Möchtest du den Raum betreten?",
"This room doesn't exist. Are you sure you're at the right place?": "Dieser Raum existiert nicht. Bist du sicher dass du hier richtig bist?",
"Try again later, or ask a room admin to check if you have access.": "Versuche es später erneut oder bitte einen Raum-Administrator deine Zutrittsrechte zu überprüfen.",
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "Beim Betreten des Raums ist ein Fehler aufgetreten %(errcode)s. Wenn du denkst dass diese Meldung nicht korrekt ist <issueLink>sende bitte einen Fehlerbericht</issueLink>.",
@ -2389,5 +2389,65 @@
"Create room": "Raum erstellen",
"Jump to oldest unread message": "Zur ältesten ungelesenen Nachricht springen",
"Upload a file": "Eine Datei hochladen",
"Dismiss read marker and jump to bottom": "Entferne Lesemarker und springe nach unten"
"Dismiss read marker and jump to bottom": "Entferne Lesemarker und springe nach unten",
"Room name or address": "Raumname oder -adresse",
"Joins room with given address": "Tritt dem Raum unter der angegebenen Adresse bei",
"Unrecognised room address:": "Unbekannte Raumadresse:",
"Help us improve Riot": "Hilf uns Riot zu verbessern",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Hilf uns Riot zu verbessern, indem du <UsageDataLink>anonyme Nutzungsdaten</UsageDataLink> schickst. Dies wird ein <PolicyLink>Cookie</PolicyLink> verwenden.",
"I want to help": "Ich möchte helfen",
"Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzerlimit erreicht.",
"Your homeserver has exceeded one of its resource limits.": "Dein Heimserver hat eine seiner Ressourcengrenzen erreicht.",
"Contact your <a>server admin</a>.": "Kontaktiere deinen <a>Heimserver Administrator</a>.",
"Ok": "Ok",
"Set password": "Setze Passwort",
"To return to your account in future you need to set a password": "Um dein Konto zukünftig wieder verwenden zu können, setze ein Passwort",
"Restart": "Neustarten",
"Upgrade your Riot": "Aktualisiere dein Riot",
"A new version of Riot is available!": "Eine neue Version von Riot ist verfügbar!",
"New version available. <a>Update now.</a>": "Neue Version verfügbar. <a>Jetzt aktualisieren.</a>",
"Please verify the room ID or address and try again.": "Bitte überprüfe die Raum-ID oder -adresse und versuche es erneut.",
"To link to this room, please add an address.": "Um den Raum zu verlinken, füge bitte eine Adresse hinzu.",
"Emoji picker": "Emoji Auswahl",
"Show %(n)s more": "%(n)s weitere anzeigen",
"Error creating address": "Fehler beim Anlegen der Adresse",
"There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Es gab einen Fehler beim Anlegen der Adresse. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.",
"You don't have permission to delete the address.": "Du hast nicht die Berechtigung die Adresse zu löschen.",
"Error removing address": "Fehler beim Löschen der Adresse",
"Categories": "Kategorien",
"Room address": "Raumadresse",
"Please provide a room address": "Bitte gib eine Raumadresse an",
"This address is available to use": "Diese Adresse ist verfügbar",
"This address is already in use": "Diese Adresse wird bereits verwendet",
"Address (optional)": "Adresse (optional)",
"delete the address.": "lösche die Adresse.",
"Use a different passphrase?": "Eine andere Passphrase verwenden?",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Dein Server-Administrator hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.",
"People": "Personen",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert diese nicht mehr oder es kam zu einem temporären Fehler.",
"Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.",
"You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Du hast für diese Sitzung zuvor eine neuere Version von Riot verwendet. Um diese Version mit Ende-zu-Ende-Verschlüsselung wieder zu benutzen, musst du dich erst ab- und dann wieder anmelden.",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Soll die Raum-Adresse %(alias)s gelöscht und %(name)s aus dem Raum-Verzeichnis entfernt werden?",
"Switch to light mode": "Zum Light Mode wechseln",
"Switch to dark mode": "Zum Dark Mode wechseln",
"Switch theme": "Design ändern",
"Security & privacy": "Sicherheit & Datenschutz",
"All settings": "Alle Einstellungen",
"Archived rooms": "Archivierte Räume",
"Feedback": "Feedback",
"Account settings": "Konto-Einstellungen",
"Room ID or address of ban list": "Raum-ID oder Adresse der Verbotsliste",
"sent an image.": "hat ein Bild gesendet.",
"You: %(message)s": "Du: %(message)s",
"No recently visited rooms": "Keine kürzlich besuchten Räume",
"Sort by": "Sortieren nach",
"Unread rooms": "Ungelesene Räume",
"Always show first": "Zeige immer zuerst",
"Show": "Zeige",
"Message preview": "Nachrichtenvorschau",
"List options": "Optionen anzeigen",
"Show %(count)s more|other": "Zeige %(count)s weitere",
"Show %(count)s more|one": "Zeige %(count)s weitere",
"Leave Room": "Verlasse Raum",
"Room options": "Raumoptionen"
}

View file

@ -246,6 +246,7 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
"sent an image.": "sent an image.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
@ -419,6 +420,7 @@
"Restart": "Restart",
"Upgrade your Riot": "Upgrade your Riot",
"A new version of Riot is available!": "A new version of Riot is available!",
"You: %(message)s": "You: %(message)s",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
@ -1069,6 +1071,7 @@
"Replying": "Replying",
"Room %(name)s": "Room %(name)s",
"Recent rooms": "Recent rooms",
"No recently visited rooms": "No recently visited rooms",
"No rooms to show": "No rooms to show",
"Unnamed room": "Unnamed room",
"World readable": "World readable",
@ -1090,6 +1093,7 @@
"Low priority": "Low priority",
"Historical": "Historical",
"System Alerts": "System Alerts",
"People": "People",
"This room": "This room",
"Joining room …": "Joining room …",
"Loading …": "Loading …",
@ -1133,9 +1137,17 @@
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
"Not now": "Not now",
"Don't ask me again": "Don't ask me again",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Sort by": "Sort by",
"Activity": "Activity",
"A-Z": "A-Z",
"Unread rooms": "Unread rooms",
"Always show first": "Always show first",
"Show": "Show",
"Message preview": "Message preview",
"List options": "List options",
"Add room": "Add room",
"Show %(count)s more|other": "Show %(count)s more",
"Show %(count)s more|one": "Show %(count)s more",
"Options": "Options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
@ -1143,6 +1155,11 @@
"%(count)s unread messages.|one": "1 unread message.",
"Unread mentions.": "Unread mentions.",
"Unread messages.": "Unread messages.",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Leave Room": "Leave Room",
"Room options": "Room options",
"Add a topic": "Add a topic",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
"This room has already been upgraded.": "This room has already been upgraded.",
@ -1818,9 +1835,6 @@
"Mentions only": "Mentions only",
"Leave": "Leave",
"Forget": "Forget",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
@ -1959,6 +1973,7 @@
"Explore": "Explore",
"Filter": "Filter",
"Filter rooms…": "Filter rooms…",
"Explore rooms": "Explore rooms",
"Failed to reject invitation": "Failed to reject invitation",
"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 room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@ -2004,7 +2019,6 @@
"Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms": "Explore rooms",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
@ -2016,6 +2030,8 @@
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed",
@ -2040,6 +2056,14 @@
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Switch to light mode": "Switch to light mode",
"Switch to dark mode": "Switch to dark mode",
"Switch theme": "Switch theme",
"Security & privacy": "Security & privacy",
"All settings": "All settings",
"Archived rooms": "Archived rooms",
"Feedback": "Feedback",
"Account settings": "Account settings",
"Could not load user profile": "Could not load user profile",
"Verify this login": "Verify this login",
"Recovery Key": "Recovery Key",

View file

@ -2455,5 +2455,7 @@
"Restart": "Restartigi",
"Upgrade your Riot": "Gradaltigi vian Rioton",
"A new version of Riot is available!": "Nova versio de Riot estas disponebla!",
"New version available. <a>Update now.</a>": "Nova versio estas disponebla. <a>Ĝisdatigu nun.</a>"
"New version available. <a>Update now.</a>": "Nova versio estas disponebla. <a>Ĝisdatigu nun.</a>",
"Emoji picker": "Elektilo de bildsignoj",
"Show %(n)s more": "Montri %(n)s pliajn"
}

View file

@ -1276,5 +1276,161 @@
"Keys restored": "Krüptimise võtmed on taastatud",
"Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s sessiooni dekrüptimine ei õnnestunud!",
"Successfully restored %(sessionCount)s keys": "%(sessionCount)s sessiooni võtme taastamine õnnestus",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Hoiatus</b>: sa peaksid võtmete varunduse seadistama vaid usaldusväärsest arvutist."
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Hoiatus</b>: sa peaksid võtmete varunduse seadistama vaid usaldusväärsest arvutist.",
"eg: @bot:* or example.org": "näiteks: @bot:* või example.org",
"Subscribed lists": "Tellitud loendid",
"Subscribe": "Telli",
"Start automatically after system login": "Käivita automaatselt peale arvutisse sisselogimist",
"Always show the window menu bar": "Näita alati aknas menüüriba",
"Show tray icon and minimize window to it on close": "Näita süsteemisalve ikooni ja Rioti'i akna sulgemisel minimeeri ta salve",
"Preferences": "Eelistused",
"Room list": "Jututubade loend",
"Timeline": "Ajajoon",
"Autocomplete delay (ms)": "Viivitus automaatsel sõnalõpetusel (ms)",
"Server Name": "Serveri nimi",
"Enter password": "Sisesta salasõna",
"Nice, strong password!": "Vahva, see on korralik salasõna!",
"Password is allowed, but unsafe": "Selline salasõna on küll lubatud, kuid üsna ebaturvaline",
"Keep going...": "Jätka...",
"The email field must not be blank.": "E-posti aadressi väli ei tohi olla tühi.",
"The username field must not be blank.": "Kasutajanime väli ei tohi olla tühi.",
"The phone number field must not be blank.": "Telefoninumbri väli ei tohi olla tühi.",
"The password field must not be blank.": "Salasõna väli ei tohi olla tühi.",
"Email": "E-posti aadress",
"Username": "Kasutajanimi",
"Phone": "Telefon",
"Not sure of your password? <a>Set a new one</a>": "Sa ei ole kindel oma salasõnas? <a>Tee uus salasõna</a>",
"Sign in with": "Logi sisse oma kasutajaga",
"No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Ühtegi isikutuvastusserverit pole seadistatud ning sul ei ole võimalik lisada oma e-posti aadressi hilisemaks võimalikuks salasõna muutmiseks.",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Kui sa ei sisesta oma e-posti aadressi, siis sa ei saa hiljem oma salasõnamuuta. Kas sa kindlasti soovid seda?",
"Use an email address to recover your account": "Kasuta e-posti aadressi ligipääsu taastamiseks oma kontole",
"Enter email address (required on this homeserver)": "Sisesta e-posti aadress (nõutav selles koduserveris)",
"Doesn't look like a valid email address": "Ei tundu olema korralik e-posti aadress",
"Passwords don't match": "Salasõnad ei klapi",
"Enter phone number (required on this homeserver)": "Sisesta telefoninumber (nõutav selles koduserveris)",
"Doesn't look like a valid phone number": "Ei tundu olema korralik telefoninumber",
"Use lowercase letters, numbers, dashes and underscores only": "Palun kasuta vaid väiketähti, numbreid, sidekriipsu ja alakriipsu",
"Enter username": "Sisesta kasutajanimi",
"Email (optional)": "E-posti aadress (kui soovid)",
"Phone (optional)": "Telefoninumber (kui soovid)",
"Create your Matrix account on %(serverName)s": "Loo oma Matrixi konto %(serverName)s serveris",
"Create your Matrix account on <underlinedServerName />": "Loo oma Matrixi konto <underlinedServerName /> serveris",
"Register": "Registreeru",
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Seadista e-posti aadress, mis võimaldaks sul vajadusel taastada ligipääsu oma kontole. Lisaks võid kasutada e-posti aadressi või telefoninumbrit, et need inimesed, kes sind tunnevad, saaks sind leida.",
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Seadista e-posti aadress, mis võimaldaks sul vajadusel taastada ligipääsu oma kontole. Lisaks võid kasutada e-posti aadressi, et need inimesed, kes sind tunnevad, saaks sind leida.",
"Enter your custom homeserver URL <a>What does this mean?</a>": "Sisesta oma koduserveri aadress <a>Mida see tähendab?</a>",
"Homeserver URL": "Koduserveri aadress",
"Enter your custom identity server URL <a>What does this mean?</a>": "Sisesta kohandatud isikutuvastusserver aadress <a>Mida see tähendab?</a>",
"Identity Server URL": "Isikutuvastusserveri aadress",
"Other servers": "Muud serverid",
"Free": "Tasuta teenus",
"Join millions for free on the largest public server": "Liitu tasuta nende miljonitega, kas kasutavad suurimat avalikku Matrix'i serverit",
"Premium": "Tasuline eriteenus",
"Premium hosting for organisations <a>Learn more</a>": "Tasuline Matrix'i majutusteenus organisatsioonidele <a>Loe lisateavet</a>",
"Find other public servers or use a custom server": "Otsi muid avalikke Matrix'i servereid või kasuta enda määratud serverit",
"Sign in to your Matrix account on %(serverName)s": "Logi sisse on Matrix'i kontole %(serverName)s serveris",
"Sign in to your Matrix account on <underlinedServerName />": "Logi sisse on Matrix'i kontole <underlinedServerName /> serveris",
"Sorry, your browser is <b>not</b> able to run Riot.": "Vabandust, aga Riot <b>ei toimi</b> sinu brauseris.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot kasutab mitmeid uusi brauseri-põhiseid tehnoloogiaid ning mitmed neist kas pole veel olemas või on lahendatud sinu brauseris katselisena.",
"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!": "Sinu praeguse brauseriga meie rakenduse välimus ja toimivus võivad olla täitsa valed ning mõni funktsionaalsus ei pruugi toimida üldse. Kui soovid katsetada, siis loomulikult võid jätkata, kuid erinevate tekkivate vigadega pead ise hakkama saama!",
"Fetching third party location failed": "Kolmanda osapoole asukoha tuvastamine ei õnnestunud",
"Unable to look up room ID from server": "Jututoa tunnuse otsimine serverist ei õnnestunud",
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.": "<showSessionsText>Näita sessioone</showSessionsText>, <sendAnywayText>saada ikkagi</sendAnywayText> või <cancelText>tühista</cancelText>.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Sinu sõnumit ei saadetud, kuna see koduserver on ületanud on ületanud ressursipiirangu. Teenuse kasutamiseks palun <a>võta ühendust serveri haldajaga</a>.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Saada kõik uuesti</resendText> või <cancelText>tühista kõigi saatmine</cancelText>. Samuti võid sa valida saatmiseks või saatmise tühistamiseks üksikuid sõnumeid.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Saada sõnum uuesti</resendText> või <cancelText>tühista sõnumi saatmine</cancelText>.",
"Connectivity to the server has been lost.": "Ühendus sinu serveriga on katkenud.",
"Sent messages will be stored until your connection has returned.": "Saadetud sõnumid salvestatakse seniks, kuni võrguühendus on taastunud.",
"Active call": "Käsilolev kõne",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "Siin ei leidu kedagi teist! Kas sa tahaksid <inviteText>kutsuda kedagi jututuppa</inviteText> või <nowarnText>lõpetada selle tühja jututoa hoiatuse kuvamise</nowarnText>?",
"You have %(count)s unread notifications in a prior version of this room.|other": "Sinul on selle jututoa varasemas versioonis %(count)s lugemata teavitust.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Sinul on selle jututoa varasemas versioonis %(count)s lugemata teavitus.",
"Fill screen": "Täida ekraan",
"No identity server is configured: add one in server settings to reset your password.": "Ühtegi isikutuvastusserverit pole seadistatud: salasõna taastamiseks määra see serveri sätetes.",
"Sign in instead": "Pigem logi sisse",
"A verification email will be sent to your inbox to confirm setting your new password.": "Kontrollimaks, et just sina sina ise sisestasid uue salasõna, siis saadame sulle kinnituskirja.",
"Send Reset Email": "Saada salasõna taastamise e-kiri",
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Saatsime kirja %(emailAddress)s aadressile. Kui sa oled klõpsinud kirjas olnud linki, siis jätka alljärgnevaga.",
"I have verified my email address": "Ma olen teinud läbi oma e-posti aadressi kontrolli",
"Your password has been reset.": "Sinu salasõna on muudetud.",
"Dismiss read marker and jump to bottom": "Ära arvesta loetud sõnumite järjehoidjat ning mine kõige lõppu",
"Jump to oldest unread message": "Mine vanima lugemata sõnumi juurde",
"Upload a file": "Lae fail üles",
"Read Marker lifetime (ms)": "Lugemise markeri iga (ms)",
"Read Marker off-screen lifetime (ms)": "Lugemise markeri iga, kui Riot pole fookuses (ms)",
"Unignore": "Lõpeta eiramine",
"<not supported>": "<ei ole toetatud>",
"Import E2E room keys": "Impordi E2E läbiva krüptimise võtmed jututubade jaoks",
"Cryptography": "Krüptimine",
"Session ID:": "Sessiooni tunnus:",
"Session key:": "Sessiooni võti:",
"Bulk options": "Masstoimingute seadistused",
"Accept all %(invitedRooms)s invites": "Võta vastu kõik %(invitedRooms)s kutsed",
"Reject all %(invitedRooms)s invites": "Lükka tagasi kõik %(invitedRooms)s kutsed",
"Key backup": "Võtmete varundus",
"Cross-signing": "Risttunnustamine",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Hoiatus</b>: Jututoa versiooni uuendamine <i>ei koli jututoa liikmeid automaatselt uude jututoa olekusse.</i> Vanas jututoa versioonis saab olema viide uuele versioonile ning kõik liikmed peavad seda viidet klõpsama.",
"Uploaded sound": "Üleslaetud heli",
"Sounds": "Helid",
"Notification sound": "Teavitusheli",
"Reset": "Taasta algolek",
"Set a new custom sound": "Seadista uus kohandatud heli",
"Jump to message": "Mine sõnumi juurde",
"were invited %(count)s times|other": "said kutse %(count)s korda",
"were invited %(count)s times|one": "said kutse",
"was invited %(count)s times|other": "sai kutse %(count)s korda",
"was invited %(count)s times|one": "sai kutse",
"%(severalUsers)schanged their name %(count)s times|other": "Mitu kasutajat %(severalUsers)s muutsid oma nime %(count)s korda",
"%(severalUsers)schanged their name %(count)s times|one": "Mitu kasutajat %(severalUsers)s muutsid oma nime",
"%(oneUser)schanged their name %(count)s times|other": "Kasutaja %(oneUser)s muutis oma nime %(count)s korda",
"You cannot place VoIP calls in this browser.": "Selle brauseriga ei saa VoIP kõnet teha.",
"You cannot place a call with yourself.": "Sa ei saa iseendale helistada.",
"You do not have permission to start a conference call in this room": "Sul ei ole piisavalt õigusi, et selles jututoas alustada konverentsikõnet",
"%(severalUsers)schanged their avatar %(count)s times|other": "Mitu kasutajat %(severalUsers)s muutsid oma tunnuspilti %(count)s korda",
"%(severalUsers)schanged their avatar %(count)s times|one": "Mitu kasutajat %(severalUsers)s muutsid oma tunnuspilti",
"%(oneUser)schanged their avatar %(count)s times|other": "Kasutaja %(oneUser)s muutis oma tunnuspilti %(count)s korda",
"%(oneUser)schanged their avatar %(count)s times|one": "Kasutaja %(oneUser)s muutis oma tunnuspilti",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Palu oma koduserveri haldajat (<code>%(homeserverDomain)s</code>), et ta seadistaks kõnede kindlamaks toimimiseks TURN serveri.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatiivina võid sa kasutada avalikku serverit <code>turn.matrix.org</code>, kuid see ei pruugi olla piisavalt töökindel ning sa jagaad ka oma IP-aadressi selle serveriga. Täpsemalt saad seda määrata seadistustes.",
"Try using turn.matrix.org": "Proovi kasutada turn.matrix.org serverit",
"OK": "Sobib",
"Unable to capture screen": "Ekraanipildi hõive ei õnnestunud",
"Existing Call": "Käimasolev kõne",
"You are already in a call.": "Sinul on juba kõne pooleli.",
"VoIP is unsupported": "VoIP ei ole toetatud",
"Call in Progress": "Kõne on pooleli",
"A call is currently being placed!": "Kõne algatamine on just käimas!",
"A call is already in progress!": "Kõne on juba pooleli!",
"Permission Required": "Vaja on täiendavaid õigusi",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Hetkel ei ole võimalik vastata failiga. Kas sa sooviksid seda faili lihtsalt üles laadida?",
"Continue": "Jätka",
"The file '%(fileName)s' failed to upload.": "Faili '%(fileName)s' üleslaadimine ei õnnestunud.",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Faili '%(fileName)s' suurus ületab serveris seadistatud üleslaadimise piiri",
"Upload Failed": "Üleslaadimine ei õnnestunud",
"Server may be unavailable, overloaded, or you hit a bug.": "Server kas pole võrgus või on üle koormatud, aga võib-olla oled komistanud süsteemivea otsa.",
"The server does not support the room version specified.": "See server ei toeta antud jututoa versiooni.",
"Failure to create room": "Jututoa loomine ei õnnestunud",
"If you cancel now, you won't complete verifying the other user.": "Kui sa katkestad nüüd, siis sul jääb teise kasutaja verifitseerimine lõpetamata.",
"If you cancel now, you won't complete verifying your other session.": "Kui sa katkestad nüüd, siis sul jääb oma muu sessiooni verifitseerimine lõpetamata.",
"If you cancel now, you won't complete your operation.": "Kui sa katkestad nüüd, siis sul jääb pooleliolev tegevus lõpetamata.",
"Cancel entering passphrase?": "Kas katkestame paroolifraasi sisestamise?",
"Enter passphrase": "Sisesta paroolifraas",
"Setting up keys": "Võtamevõtmed kasutusele",
"Room name or address": "Jututoa nimi või aadress",
"Unable to enable Notifications": "Teavituste kasutusele võtmine ei õnnestunud",
"This email address was not found": "Seda e-posti aadressi ei leidunud",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Sinu e-posti aadress ei tundu olema selles koduserveris seotud Matrixi kasutajatunnusega.",
"Add User": "Lisa kasutaja",
"Enter a server name": "Sisesta serveri nimi",
"Looks good": "Tundub õige",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "E-posti teel kutse saatmiseks kasuta isikutuvastusserverit. Võid kasutada <default>vaikimisi serverit (%(defaultIdentityServerName)s)</default> või määrata muud serverid <settings>seadistustes</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Kasutajatele e-posti teel kutse saatmiseks pruugi isikutuvastusserverit. Täpsemalt saad seda <settings>hallata seadistustes</settings>.",
"The following users may not exist": "Järgnevaid kasutajaid ei pruugi olla olemas",
"Invite anyway and never warn me again": "Kutsu siiski ja ära hoiata mind enam",
"Invite anyway": "Kutsu siiski",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Palun kirjelda seda, mis läks valesti ja loo GitHub'is veateade.",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Vigadega seotud logid sisaldavad rakenduse teavet, sealhulgas sinu kasutajanime, külastatud jututubade kasutajatunnuseid või aliasi ning teiste kasutajate kasutajanimesid. Logides ei ole saadetud sõnumite sisu.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Enne logide saatmist sa peaksid <a>GitHub'is looma veateate</a> ja kirjeldama seal tekkinud probleemi.",
"GitHub issue": "Veateade GitHub'is",
"Notes": "Märkused"
}

View file

@ -2425,5 +2425,66 @@
"Click the button below to confirm setting up encryption.": "Sakatu azpiko botoia zifratze-ezarpena berresteko.",
"Dismiss read marker and jump to bottom": "Baztertu irakurtze-marka eta jauzi beheraino",
"Jump to oldest unread message": "Jauzi irakurri gabeko mezu zaharrenera",
"Upload a file": "Igo fitxategia"
"Upload a file": "Igo fitxategia",
"Room name or address": "Gelaren izena edo helbidea",
"Joins room with given address": "Emandako helbidea duen gelara elkartzen da",
"Unrecognised room address:": "Gela helbide ezezaguna:",
"sent an image.": "irudi bat bidali du.",
"Help us improve Riot": "Lagundu gaitzazu Riot hobetzen",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Bidali <UsageDataLink>erabilera datu anonimoak</UsageDataLink> Riot hobetzen laguntzeko. Honek <PolicyLink>cookie</PolicyLink> bat darabil.",
"I want to help": "Lagundu nahi dut",
"Your homeserver has exceeded its user limit.": "Zure hasiera-zerbitzariak erabiltzaile muga gainditu du.",
"Your homeserver has exceeded one of its resource limits.": "Zure hasiera-zerbitzariak bere baliabide mugetako bat gainditu du.",
"Contact your <a>server admin</a>.": "Jarri kontaktuan <a>zerbitzariaren administratzailearekin</a>.",
"Ok": "Ados",
"Set password": "Ezarri pasahitza",
"To return to your account in future you need to set a password": "Zure kontura etorkizunean itzultzeko pasahitza ezarri behar duzu",
"Restart": "Berrasi",
"Upgrade your Riot": "Eguneratu zure Riot",
"A new version of Riot is available!": "Riot bertsio berria eskuragarri dago!",
"You: %(message)s": "Zu: %(message)s",
"New version available. <a>Update now.</a>": "Bertsio berri eskuragarri. <a>Eguneratu orain.</a>",
"Please verify the room ID or address and try again.": "Egiaztatu gelaren ID-a edo helbidea eta saiatu berriro.",
"Room ID or address of ban list": "Debeku zerrendaren gelaren IDa edo helbidea",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Zure zerbitzariko administratzaileak muturretik muturrerako zifratzea desgaitu du lehenetsita gela probatuetan eta mezu zuzenetan.",
"To link to this room, please add an address.": "Gela hau estekatzeko, gehitu helbide bat.",
"Emoji picker": "Emoji hautatzailea",
"No recently visited rooms": "Ez dago azkenaldian bisitatutako gelarik",
"People": "Jendea",
"Sort by": "Ordenatu honela",
"Activity": "Jarduera",
"A-Z": "A-Z",
"Unread rooms": "Irakurri gabeko gelak",
"Always show first": "Erakutsi lehenbizi",
"Show": "Erakutsi",
"Message preview": "Mezu-aurrebista",
"List options": "Zerrenda-aukerak",
"Show %(count)s more|other": "Erakutsi %(count)s gehiago",
"Show %(count)s more|one": "Erakutsi %(count)s gehiago",
"Leave Room": "Atera gelatik",
"Room options": "Gelaren aukerak",
"Error creating address": "Errorea helbidea sortzean",
"There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Errorea gertatu da helbidea sortzean. Agian ez du zerbitzariak onartzen edo behin behineko arazo bat egon da.",
"You don't have permission to delete the address.": "Ez duzu helbidea ezabatzeko baimenik.",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Errorea gertatu da helbidea kentzean. Agian ez dago jada edo behin behineko arazo bat egon da.",
"Error removing address": "Errorea helbidea kentzean",
"Categories": "Kategoriak",
"Room address": "Gelaren helbidea",
"Please provide a room address": "Eman gelaren helbidea",
"This address is available to use": "Gelaren helbide hau erabilgarri dago",
"This address is already in use": "Gelaren helbide hau erabilita dago",
"Set a room address to easily share your room with other people.": "Ezarri gelaren helbide bat zure gela besteekin erraz partekatzeko.",
"You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Riot bertsio berriago bat erabili duzu saio honekin. Berriro bertsio hau muturretik muturrerako zifratzearekin erabiltzeko, saioa amaitu eta berriro hasi beharko duzu.",
"Address (optional)": "Helbidea (aukerakoa)",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Ezabatu %(alias)s gelaren helbidea eta kendu %(name)s direktoriotik?",
"delete the address.": "ezabatu helbidea.",
"Switch to light mode": "Aldatu modu argira",
"Switch to dark mode": "Aldatu modu ilunera",
"Switch theme": "Aldatu azala",
"Security & privacy": "Segurtasuna eta pribatutasuna",
"All settings": "Ezarpen guztiak",
"Archived rooms": "Artxibatutako gelak",
"Feedback": "Iruzkinak",
"Account settings": "Kontuaren ezarpenak",
"Use a different passphrase?": "Erabili pasa-esaldi desberdin bat?"
}

View file

@ -2481,5 +2481,32 @@
"Restart": "Redémarrer",
"Upgrade your Riot": "Mettre à niveau votre Riot",
"A new version of Riot is available!": "Une nouvelle version de Riot est disponible !",
"New version available. <a>Update now.</a>": "Nouvelle version disponible. <a>Faire la mise à niveau maintenant.</a>"
"New version available. <a>Update now.</a>": "Nouvelle version disponible. <a>Faire la mise à niveau maintenant.</a>",
"Emoji picker": "Sélecteur démojis",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Ladministrateur de votre serveur a désactivé le chiffrement de bout en bout par défaut dans les salons privés et les messages directs.",
"Show %(n)s more": "En montrer %(n)s de plus",
"People": "Personnes",
"Switch to light mode": "Passer au mode clair",
"Switch to dark mode": "Passer au mode sombre",
"Switch theme": "Changer le thème",
"Security & privacy": "Sécurité & vie privée",
"All settings": "Tous les paramètres",
"Archived rooms": "Salons archivés",
"Feedback": "Commentaire",
"Account settings": "Paramètres du compte",
"sent an image.": "a envoyé une image.",
"You: %(message)s": "Vous : %(message)s",
"No recently visited rooms": "Aucun salon visité récemment",
"Sort by": "Trier par",
"Unread rooms": "Salons non lus",
"Always show first": "Toujours afficher en premier",
"Show": "Afficher",
"Message preview": "Aperçu de message",
"List options": "Options de liste",
"Show %(count)s more|other": "En afficher %(count)s de plus",
"Show %(count)s more|one": "En afficher %(count)s de plus",
"Leave Room": "Quitter le salon",
"Room options": "Options du salon",
"Activity": "Activité",
"A-Z": "A-Z"
}

View file

@ -1253,5 +1253,412 @@
"Add theme": "Engadir decorado",
"Theme": "Decorado",
"Language and region": "Idioma e rexión",
"Your theme": "O teu decorado"
"Your theme": "O teu decorado",
"Use the improved room list (in development - refresh to apply changes)": "Usar a lista mellorada de salas (en desenvolvemento - actualiza para aplicar)",
"Support adding custom themes": "Permitir engadir decorados personalizados",
"Use IRC layout": "Usar disposición IRC",
"Font size": "Tamaño da letra",
"Custom font size": "Tamaño letra personalizado",
"Enable Emoji suggestions while typing": "Activar suxestión de Emoji ao escribir",
"Show a placeholder for removed messages": "Resaltar o lugar das mensaxes eliminadas",
"Show avatar changes": "Mostrar cambios de avatar",
"Show display name changes": "Mostrar cambios do nome mostrado",
"Show read receipts sent by other users": "Mostrar resgardo de lectura enviados por outras usuarias",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Mostrar recordatorio para activar Recuperación Segura de Mensaxes en salas cifradas",
"Show avatars in user and room mentions": "Mostrar avatares nas mencións de salas e usuarias",
"Enable big emoji in chat": "Activar Emojis grandes na conversa",
"Send typing notifications": "Enviar notificación de escritura",
"Show typing notifications": "Mostrar notificacións de escritura",
"Allow Peer-to-Peer for 1:1 calls": "Permitir Par-a-Par para chamadas 1:1",
"Never send encrypted messages to unverified sessions from this session": "Non enviar nunca desde esta sesión mensaxes cifradas a sesións non verificadas",
"Never send encrypted messages to unverified sessions in this room from this session": "Non enviar mensaxes cifradas desde esta sesión a sesións non verificadas nesta sala",
"Prompt before sending invites to potentially invalid matrix IDs": "Avisar antes de enviar convites a IDs de Matrix potencialmente incorrectos",
"Show developer tools": "Mostrar ferramentas de desenvolvemento",
"Order rooms by name": "Ordenar salas por nome",
"Show rooms with unread notifications first": "Mostrar primeiro as salas que teñen notificacións sen ler",
"Show shortcuts to recently viewed rooms above the room list": "Mostrar atallos a salas vistas recentemente enriba da lista de salas",
"Show hidden events in timeline": "Mostrar na cronoloxía eventos ocultos",
"Low bandwidth mode": "Modo de ancho de banda reducido",
"Straight rows of keys are easy to guess": "Palabras de letras contiguas son doadas de adiviñar",
"Short keyboard patterns are easy to guess": "Patróns curtos de teclas son doados de adiviñar",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Permitir o servidor de apoio para chamadas turn.matrix.org cando o servidor propio non ofreza un (o teu IP compartirase durante a chamada)",
"Send read receipts for messages (requires compatible homeserver to disable)": "Enviar resgardos de lectura para as mensaxes (require servidor compatible para desactivar)",
"Show previews/thumbnails for images": "Mostrar miniaturas/vista previa das imaxes",
"Enable message search in encrypted rooms": "Activar a busca de mensaxes en salas cifradas",
"Keep recovery passphrase in memory for this session": "Manter a frase de paso de recuperación en memoria para esta sesión",
"How fast should messages be downloaded.": "Velocidade á que deberían descargarse as mensaxes.",
"Manually verify all remote sessions": "Verificar manualmente todas as sesións remotas",
"IRC display name width": "Ancho do nome mostrado de IRC",
"Messages containing my username": "Mensaxes que conteñen o meu nome de usuaria",
"Messages containing @room": "Mensaxes que conteñen @room",
"Encrypted messages in one-to-one chats": "Mensaxes cifradas en conversas 1:1",
"Encrypted messages in group chats": "Mensaxes cifradas en convesas en grupo",
"When rooms are upgraded": "Cando se actualizan as salas",
"My Ban List": "Listaxe de bloqueo",
"This is your list of users/servers you have blocked - don't leave the room!": "Esta é a listaxe de usuarias/servidores que ti bloqueaches - non deixes a sala!",
"The other party cancelled the verification.": "A outra parte cancelou a verificación.",
"Verified!": "Verificada!",
"You've successfully verified this user.": "Verificaches esta usuaria.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "As mensaxes seguras con esta usuaria están cifradas extremo-a-extremo e non son lexibles por terceiras.",
"Got It": "Vale",
"Verify this session by completing one of the following:": "Verifica esta sesión completando un dos seguintes:",
"Scan this unique code": "Escanea este código único",
"or": "ou",
"Compare unique emoji": "Compara os emoji",
"Compare a unique set of emoji if you don't have a camera on either device": "Compara o conxunto único de emoticonas se non tes cámara no outro dispositivo",
"Start": "Comezar",
"Confirm the emoji below are displayed on both sessions, in the same order:": "Confirma que as emoticonas se mostran nas dúas sesións, na mesma orde:",
"Verify this user by confirming the following emoji appear on their screen.": "Verifica a usuaria confirmando que as emoticonas aparecen na súa pantalla.",
"Verify this session by confirming the following number appears on its screen.": "Verifica esta sesión confirmando que o seguinte número aparece na súa pantalla.",
"Verify this user by confirming the following number appears on their screen.": "Verifica esta usuaria confirmando que o seguinte número aparece na súa pantalla.",
"Unable to find a supported verification method.": "Non se atopa un método de verificación válido.",
"Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Agardando pola outra sesión, %(deviceName)s %(deviceId)s, para verificar…",
"Waiting for your other session to verify…": "Agardando pola túa outra sesión para verificar…",
"Waiting for %(displayName)s to verify…": "Agardando por %(displayName)s para verificar…",
"Cancelling…": "Cancelando…",
"They match": "Concordan",
"They don't match": "Non concordan",
"To be secure, do this in person or use a trusted way to communicate.": "Para estar seguro, fai esto en persoa ou utiliza un xeito seguro para comunicarte.",
"Dog": "Can",
"Cat": "Gato",
"Lion": "León",
"Horse": "Cabalo",
"Unicorn": "Unicorno",
"Pig": "Porco",
"Elephant": "Elefante",
"Rabbit": "Coello",
"Panda": "Panda",
"Rooster": "Galo",
"Penguin": "Pingüino",
"Turtle": "Tartaruga",
"Fish": "Peixe",
"Octopus": "Polbo",
"Butterfly": "Bolboreta",
"Flower": "Flor",
"Tree": "Árbore",
"Cactus": "Cactus",
"Mushroom": "Cogomelo",
"Globe": "Globo",
"Moon": "Lúa",
"Cloud": "Nube",
"Fire": "Lume",
"Banana": "Plátano",
"Apple": "Mazá",
"Strawberry": "Amorodo",
"Corn": "Millo",
"Pizza": "Pizza",
"Cake": "Biscoito",
"Heart": "Corazón",
"Smiley": "Sorriso",
"Robot": "Robot",
"Hat": "Sombreiro",
"Glasses": "Gafas",
"Spanner": "Ferramenta",
"Santa": "Nöel",
"Thumbs up": "Oká",
"Umbrella": "Paraugas",
"Hourglass": "Reloxo area",
"Clock": "Reloxo",
"Gift": "Agasallo",
"Light bulb": "Lámpada",
"Book": "Libro",
"Pencil": "Lápis",
"Paperclip": "Prendedor",
"Scissors": "Tesoiras",
"Lock": "Cadeado",
"Key": "Chave",
"Hammer": "Martelo",
"Telephone": "Teléfono",
"Flag": "Bandeira",
"Train": "Tren",
"Bicycle": "Bicicleta",
"Aeroplane": "Aeroplano",
"Rocket": "Foguete",
"Trophy": "Trofeo",
"Ball": "Bola",
"Guitar": "Guitarra",
"Trumpet": "Trompeta",
"Bell": "Campá",
"Anchor": "Áncora",
"Headphones": "Auriculares",
"Folder": "Cartafol",
"Pin": "Pin",
"From %(deviceName)s (%(deviceId)s)": "Desde %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Rexeitar (%(counter)s)",
"Accept <policyLink /> to continue:": "Acepta <policyLink /> para continuar:",
"Upload": "Subir",
"This bridge was provisioned by <user />.": "Esta ponte está proporcionada por <user />.",
"This bridge is managed by <user />.": "Esta ponte está xestionada por <user />.",
"Workspace: %(networkName)s": "Espazo de traballo: %(networkName)s",
"Channel: %(channelName)s": "Canal: %(channelName)s",
"Show less": "Mostrar menos",
"Show more": "Mostrar máis",
"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.": "Ao cambiar o contrasinal vas restablecer todas as chaves de cifrado extremo-a-extremo en tódalas sesións, facendo que o historial de conversa cifrado non sexa lexible, a menos que primeiro exportes todas as chaves das salas e as importes posteriormente. No futuro melloraremos o procedemento.",
"Your homeserver does not support cross-signing.": "O teu servidor non soporta a sinatura cruzada.",
"Cross-signing and secret storage are enabled.": "A sinatura cruzada e o almacenaxe segredo está activados.",
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "A túa conta ten unha identidade de sinatura cruzada no almacenaxe segredo, pero aínda non confiaches nela nesta sesión.",
"Cross-signing and secret storage are not yet set up.": "A sinatura cruzada e almacenaxe segredo aínda non se configuraron.",
"Reset cross-signing and secret storage": "Restablecer sinatura cruzada e almacenaxe segredo",
"Bootstrap cross-signing and secret storage": "Configurar sinatura cruzada e almacenaxe segredo",
"well formed": "ben formado",
"unexpected type": "tipo non agardado",
"Cross-signing public keys:": "Chaves públicas da sinatura cruzada:",
"in memory": "en memoria",
"not found": "non atopado",
"Cross-signing private keys:": "Chaves privadas da sinatura cruzada:",
"in secret storage": "no almacenaxe segredo",
"Self signing private key:": "Auto asinado da chave privada:",
"cached locally": "na caché local",
"not found locally": "non se atopa localmente",
"User signing private key:": "Chave privada de sinatura da usuaria:",
"Session backup key:": "Chave de apoio da sesión:",
"Secret storage public key:": "Chave pública da almacenaxe segreda:",
"in account data": "nos datos da conta",
"Homeserver feature support:": "Soporte de funcións do servidor:",
"exists": "existe",
"Your homeserver does not support session management.": "O teu servidor non soporta a xestión da sesión.",
"Unable to load session list": "Non se puido cargar a lista de sesións",
"Confirm deleting these sessions": "Confirma o borrado destas sesións",
"Click the button below to confirm deleting these sessions.|other": "Preme no botón inferior para confirmar o borrado das sesións.",
"Click the button below to confirm deleting these sessions.|one": "Preme no botón inferior para confirmar o borrado da sesión.",
"Delete sessions|other": "Borrar sesións",
"Delete sessions|one": "Borrar sesión",
"Delete %(count)s sessions|other": "Borrar %(count)s sesións",
"Delete %(count)s sessions|one": "Borrar %(count)s sesión",
"ID": "ID",
"Public Name": "Nome público",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verificar individualmente cada sesión utilizada pola usuaria para marcala como confiable, non confiando en dispositivos con sinatura cruzada.",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Gardar de xeito seguro na caché mensaxes cifradas para que aparezan nos resultados de busca, usando ",
" to store messages from ": " para gardar mensaxes de ",
"rooms.": "salas.",
"Manage": "Xestionar",
"Securely cache encrypted messages locally for them to appear in search results.": "Gardar de xeito seguro mensaxes cifradas na caché local para que aparezan nos resultados de buscas.",
"Enable": "Activar",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Falta un compoñente de Riot requerido para almacenar localmente mensaxes cifradas na caché. Se queres experimentar con esta función, compila unha versión personalizada de Riot Desktop <nativeLink>cos compoñentes de busca engadidos</nativeLink>.",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot non pode gardar de xeito seguro localmente as mensaxes cifradas se se executa nun navegador. Usa <riotLink>Riot Desktop</riotLink> para que as mensaxes cifradas aparezan nas buscas.",
"Connecting to integration manager...": "Conectando co xestor de integración...",
"Cannot connect to integration manager": "Non se puido conectar co xestor de intregración",
"The integration manager is offline or it cannot reach your homeserver.": "O xestor de integración non está en liña ou non é accesible desde o teu servidor.",
"Delete Backup": "Borrar copia de apoio",
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Estás seguro? Perderás as mensaxes cifradas se non tes unha copia de apoio das chaves de cifrado.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "As mensaxes cifradas están seguras con cifrado de extremo-a-extremo. Só ti e o correpondente(s) tedes as chaves para ler as mensaxes.",
"Unable to load key backup status": "Non se puido cargar o estado das chaves de apoio",
"Restore from Backup": "Restaurar desde copia de apoio",
"This session is backing up your keys. ": "Esta sesión está gardando as túas chaves. ",
"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.": "Esta sesión <b>non está facendo copia das chaves</b>, pero tes unha copia de apoio existente que podes restablecer e engadir para seguir adiante.",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Conecta esta sesión ao gardado das chaves antes de desconectarte para evitar perder calquera chave que só puidese estar nesta sesión.",
"Connect this session to Key Backup": "Conecta esta sesión a Copia de Apoio de chaves",
"not stored": "non gardado",
"Backing up %(sessionsRemaining)s keys...": "Copiando %(sessionsRemaining)s chaves...",
"All keys backed up": "Copiaronse todas as chaves",
"Backup has a <validity>valid</validity> signature from this user": "A copia ten unha sinatura <validity>válida</validity> desta usuaria",
"Backup has a <validity>invalid</validity> signature from this user": "A copia ten una sinatura <validity>non válida</validity> desta usuaria",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "A copia ten unha sinatura dunha usuaria <verify>descoñecida</verify> con ID %(deviceId)s",
"Backup has a signature from <verify>unknown</verify> session with ID %(deviceId)s": "A copia ten unha sinatura dunha sesión <verify>descoñecida</verify> con ID %(deviceId)s",
"Backup has a <validity>valid</validity> signature from this session": "A copia ten unha sinatura <validity>válida</validity> desde esta sesión",
"Backup has an <validity>invalid</validity> signature from this session": "A copia ten unha sinatura <validity>non válida</validity> desde esta sesión",
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> session <device></device>": "A copia ten unha sinatura <validity>válida</validity> desde a sesión <verify>verificada</verify> en <device></device>",
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "A copia ten unha sinatura <validity>válida</validity> desde a sesión <verify>non verificada</verify> en <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "A copia ten unha sinatura <validity>non válida</validity> desde a sesión <verify>verificada</verify> en <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "A copia ten unha sinatura <validity>non válida</validity> desde a sesión <verify>non verificada</verify> en <device></device>",
"Backup is not signed by any of your sessions": "A copia non está asinada por ningunha das túas sesións",
"This backup is trusted because it has been restored on this session": "Esta copia é de confianza porque foi restaurada nesta sesión",
"Backup version: ": "Versión da copia: ",
"Algorithm: ": "Algoritmo: ",
"Backup key stored: ": "Chave de apoio gardada: ",
"Your keys are <b>not being backed up from this session</b>.": "As túas chaves <b>non están a ser copiadas desde esta sesión</b>.",
"Back up your keys before signing out to avoid losing them.": "Fai unha copia de apoio das chaves antes de desconectarte para evitar perdelas.",
"Start using Key Backup": "Fai unha Copia de apoio das chaves",
"Clear notifications": "Eliminar notificacións",
"Add an email address to configure email notifications": "Engade un enderezo de email para configurar as notificacións por email",
"Enable desktop notifications for this session": "Activa as notificacións de escritorio para esta sesión",
"Enable audible notifications for this session": "Activa as notificacións por son para esta sesión",
"<a>Upgrade</a> to your own domain": "<a>Mellora</a> e usa un dominio propio",
"Display Name": "Nome mostrado",
"Profile picture": "Imaxe de perfil",
"Identity Server URL must be HTTPS": "O URL do servidor de identidade debe comezar HTTPS",
"Not a valid Identity Server (status code %(code)s)": "Servidor de Identidade non válido (código de estado %(code)s)",
"Could not connect to Identity Server": "Non hai conexión co Servidor de Identidade",
"Checking server": "Comprobando servidor",
"Change identity server": "Cambiar de servidor de identidade",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Desconectar do servidor de identidade <current /> e conectar con <new />?",
"Terms of service not accepted or the identity server is invalid.": "Non se aceptaron os Termos do servizo ou o servidor de identidade non é válido.",
"The identity server you have chosen does not have any terms of service.": "O servidor de identidade escollido non ten establecidos termos do servizo.",
"Disconnect identity server": "Desconectar servidor de identidade",
"Disconnect from the identity server <idserver />?": "Desconectar do servidor de identidade <idserver />?",
"Disconnect": "Desconectar",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Deberías <b>eliminar os datos personais</b> do servidor de identidade <idserver /> antes de desconectar. Desgraciadamente, o servidor <idserver /> non está en liña e no se pode acceder.",
"You should:": "Deberías:",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "comprobar os engadidos do navegador por algún está bloqueando o servidor de identidade (como Privacy Badger)",
"contact the administrators of identity server <idserver />": "contactar coa administración do servidor de identidade <idserver />",
"wait and try again later": "agardar e probar máis tarde",
"Disconnect anyway": "Desconectar igualmente",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Aínda estás <b>compartindo datos personais</b> no servidor de identidade <idserver />.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Recomendámosche que elimines os teus enderezos de email e números de teléfono do servidor de identidade antes de desconectar del.",
"Go back": "Atrás",
"Identity Server (%(server)s)": "Servidor de Identidade (%(server)s)",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Neste intre usas <server></server> para atopar e ser atopado polos contactos existentes que coñeces. Aquí abaixo podes cambiar de servidor de identidade.",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Se non queres usar <server /> para atopar e ser atopado polos contactos existentes que coñeces, escribe embaixo outro servidor de identidade.",
"Identity Server": "Servidor de Identidade",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Non estás a usar un servidor de identidade. Para atopar e ser atopado polos contactos existentes que coñeces, engade un embaixo.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Ao desconectar do teu servidor de identidade non te poderán atopar as outras usuarias e non poderás convidar a outras polo seu email ou teléfono.",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Usar un servidor de identidade é optativo. Se escolles non usar un, non poderás ser atopado por outras usuarias e non poderás convidar a outras polo seu email ou teléfono.",
"Do not use an identity server": "Non usar un servidor de identidade",
"Enter a new identity server": "Escribe o novo servidor de identidade",
"Change": "Cambiar",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integración <b>(%(serverName)s)</b> para xestionar bots, widgets e paquetes de pegatinas.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integracións para xestionar bots, widgets e paquetes de pegatinas.",
"Manage integrations": "Xestionar integracións",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Os xestores de integracións reciben datos de configuración, e poden modificar os widgets, enviar convites das salas, e establecer roles no teu nome.",
"New version available. <a>Update now.</a>": "Nova versión dispoñible. <a>Actualiza.</a>",
"Size must be a number": "O tamaño ten que ser un número",
"Custom font size can only be between %(min)s pt and %(max)s pt": "O tamaño da fonte só pode estar entre %(min)s pt e %(max)s pt",
"Use between %(min)s pt and %(max)s pt": "Usa entre %(min)s pt e %(max)s pt",
"Appearance": "Aparencia",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Cambiouse o contrasinal. Non recibirás notificacións push noutras sesións ata que desconectes e voltes a conectar nelas",
"Email addresses": "Enderezos de email",
"Phone numbers": "Número de teléfono",
"Set a new account password...": "Establecer novo contrasinal da conta...",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Acepta os Termos do Servizo do servidor (%(serverName)s) para permitir que te atopen polo enderezo de email ou número de teléfono.",
"Account management": "Xestión da conta",
"Deactivating your account is a permanent action - be careful!": "A desactivación da conta será permanente - ten coidado!",
"Credits": "Créditos",
"Chat with Riot Bot": "Chat co Bot Riot",
"Bug reporting": "Informar de fallos",
"Clear cache and reload": "Baleirar caché e recargar",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Para informar dun asunto relacionado coa seguridade de Matrix, le a <a>Política de Revelación de Privacidade</a> de Matrix.org.",
"FAQ": "PMF",
"Keyboard Shortcuts": "Atallos de teclado",
"Versions": "Versións",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliza a túa experiencia con características experimentais. <a>Coñecer máis</a>.",
"Ignored/Blocked": "Ignorado/Bloqueado",
"Error adding ignored user/server": "Fallo ao engadir a ignorado usuaria/servidor",
"Something went wrong. Please try again or view your console for hints.": "Algo fallou. Inténtao outra vez o mira na consola para ter algunha pista.",
"Error subscribing to list": "Fallo ao subscribirse a lista",
"Please verify the room ID or address and try again.": "Comproba o ID da sala ou enderezo e proba outra vez.",
"Error removing ignored user/server": "Fallo ao eliminar a usuaria/servidor de ignorados",
"Error unsubscribing from list": "Fallo ao retirar a susbscrición a lista",
"Please try again or view your console for hints.": "Inténtao outra vez ou mira na consola para ter algunha pista.",
"None": "Nada",
"Ban list rules - %(roomName)s": "Regras de bloqueo - %(roomName)s",
"Server rules": "Regras do servidor",
"User rules": "Regras da usuaria",
"You have not ignored anyone.": "Non ignoraches a ninguén.",
"You are currently ignoring:": "Estás a ignorar a:",
"You are not subscribed to any lists": "Non estás subscrita a ningunha lista",
"Unsubscribe": "Baixa na subscrición",
"View rules": "Ver regras",
"You are currently subscribed to:": "Estas subscrito a:",
"Ignored users": "Usuarias ignoradas",
"⚠ These settings are meant for advanced users.": "⚠ Estos axustes van dirixidos a usuarias avanzadas.",
"Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Engade aquí usuarias e servidores que desexas ignorar. Usa asterisco que Riot usará como comodín. Exemplo, <code>@bot*</code> ignorará todas as usuarias de calquera servidor que teñan 'bot' no nome.",
"Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorar a persoas faise a través de listaxes de bloqueo que conteñen regras. Subscribíndote a unha listaxe de bloqueo fará que esas usuarias/servidores sexan inaccesibles para ti.",
"Personal ban list": "Lista personal de bloqueo",
"Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "A túa listaxe personal de bloqueo acolle as usuarias/servidores que personalmente non desexas ver. Tras ignorar a túa primeira usuaria/servidor, unha nova sala chamada 'Listaxe de bloqueos' aparecerá na listaxe de salas - non saias desta sala para que o bloqueo siga surtindo efecto.",
"Server or user ID to ignore": "ID de usuaria ou servidor a ignorar",
"eg: @bot:* or example.org": "ex: @bot:* ou exemplo.org",
"Subscribed lists": "Listaxes subscritas",
"If this isn't what you want, please use a different tool to ignore users.": "Se esto non é o que queres, usa unha ferramenta diferente para ignorar usuarias.",
"Room ID or address of ban list": "ID da sala ou enderezo da listaxe de bloqueo",
"Subscribe": "Subscribir",
"Always show the window menu bar": "Mostrar sempre a barra de menú da ventá",
"Show tray icon and minimize window to it on close": "Mostrar icona na bandexa do sistema e minizar nela ao pechar",
"Preferences": "Preferencias",
"Room list": "Listaxe de Salas",
"Composer": "Editor",
"Timeline": "Cronoloxía",
"Autocomplete delay (ms)": "Retraso no autocompletado (ms)",
"Read Marker lifetime (ms)": "Duración do marcador de lectura (ms)",
"Read Marker off-screen lifetime (ms)": "Duración do marcador de lectura fóra de pantall (ms)",
"Session ID:": "ID da sesión:",
"Session key:": "Chave da sesión:",
"Bulk options": "Opcións agrupadas",
"Accept all %(invitedRooms)s invites": "Aceptar os %(invitedRooms)s convites",
"Key backup": "Copia da Chave",
"Message search": "Buscar mensaxe",
"Cross-signing": "Sinatura cruzada",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "A administración do servidor desactivou por omisión o cifrado extremo-a-extremo en salas privadas e Mensaxes Directas.",
"A session's public name is visible to people you communicate with": "Un nome público de sesión é visible para a xente coa que te comunicas",
"Missing media permissions, click the button below to request.": "Falta permiso acceso multimedia, preme o botón para solicitalo.",
"Request media permissions": "Solicitar permiso a multimedia",
"Voice & Video": "Voz e Vídeo",
"Upgrade this room to the recommended room version": "Actualiza esta sala á versión recomendada",
"this room": "esta sala",
"View older messages in %(roomName)s.": "Ver mensaxes antigas en %(roomName)s.",
"Room information": "Información da sala",
"Internal room ID:": "ID interno da sala:",
"Room version": "Versión da sala",
"Room version:": "Versión da sala:",
"Developer options": "Opcións desenvolvemento",
"Open Devtools": "Open Devtools",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "Esta sala está enviando mensaxes ás seguintes plataformas. <a>Coñece máis.</a>",
"sent an image.": "enviou unha imaxe.",
"You: %(message)s": "Ti: %(message)s",
"This room isnt bridging messages to any platforms. <a>Learn more.</a>": "Esta sala non está enviando mensaxes a outras plataformas. <a>Saber máis.</a>",
"Bridges": "Pontes",
"Uploaded sound": "Audio subido",
"Sounds": "Audios",
"Notification sound": "Ton de notificación",
"Reset": "Restablecer",
"Set a new custom sound": "Establecer novo ton personalizado",
"Browse": "Buscar",
"Change room avatar": "Cambiar avatar da sala",
"Change main address for the room": "Cambiar enderezo principal da sala",
"Change history visibility": "Cambiar visibilidade do historial",
"Change permissions": "Cambiar permisos",
"Change topic": "Cambiar tema",
"Upgrade the room": "Actualizar a sala",
"Enable room encryption": "Activar cifrado da sala",
"Modify widgets": "Modificar widgets",
"Error changing power level requirement": "Erro ao cambiar o requerimento de nivel de responsabilidade",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Algo fallou ao cambiar os requerimentos de nivel de responsabilidade na sala. Asegúrate de ter os permisos suficientes e volve a intentalo.",
"Error changing power level": "Erro ao cambiar nivel de responsabilidade",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Algo fallou ao cambiar o nivel de responsabilidade da usuaria. Asegúrate de ter permiso suficiente e inténtao outra vez.",
"Default role": "Rol por omsión",
"Send messages": "Enviar mensaxes",
"Invite users": "Convidar usuarias",
"Change settings": "Cambiar axustes",
"Kick users": "Expulsar usuarias",
"Ban users": "Bloquear usuarias",
"Remove messages": "Eliminar mensaxes",
"Notify everyone": "Notificar a todas",
"Send %(eventType)s events": "Enviar %(eventType)s eventos",
"Select the roles required to change various parts of the room": "Escolle os roles requeridos para cambiar determinadas partes da sala",
"Enable encryption?": "Activar cifrado?",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Unha vez activado, non se pode desactivar o cifrado da sala. As mensaxes enviadas nunha sala cifrada non poder ser vistas polo servidor, só polas participantes da sala. Ao activar o cifrado poderías causar que bots e pontes deixen de funcionar correctamente. <a>Coñece máis sobre o cifrado.</a>",
"To link to this room, please add an address.": "Para ligar a esta sala, engade un enderezo.",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Os cambios sobre quen pode ler o historial só se aplicarán as mensaxes futuras nesta sala. A visibilidade do historial precedente non cambiará.",
"Encryption": "Cifrado",
"Once enabled, encryption cannot be disabled.": "Unha vez activado, non se pode desactivar.",
"Encrypted": "Cifrado",
"Unable to revoke sharing for email address": "Non se puido revogar a compartición para o enderezo de correo",
"Unable to share email address": "Non se puido compartir co enderezo de email",
"Your email address hasn't been verified yet": "O teu enderezo de email aínda non foi verificado",
"Click the link in the email you received to verify and then click continue again.": "Preme na ligazón do email recibido para verificalo e após preme en continuar outra vez.",
"Verify the link in your inbox": "Verifica a ligazón na túa caixa de correo",
"Complete": "Completar",
"Revoke": "Revogar",
"Share": "Compartir",
"Discovery options will appear once you have added an email above.": "As opcións de descubrimento aparecerán após ti engadas un email.",
"Unable to revoke sharing for phone number": "Non se puido revogar a compartición do número de teléfono",
"Unable to share phone number": "Non se puido compartir o número de teléfono",
"Unable to verify phone number.": "Non se puido verificar o número de teléfono.",
"Please enter verification code sent via text.": "Escribe o código de verificación enviado no SMS.",
"Verification code": "Código de verificación",
"Discovery options will appear once you have added a phone number above.": "As opción para atoparte aparecerán cando engadas un número de teléfono.",
"Remove %(email)s?": "Eliminar %(email)s?",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Enviamosche un email para verificar o enderezo. Segue as instrucións incluídas e após preme no botón inferior.",
"Email Address": "Enderezo de Email",
"Remove %(phone)s?": "Eliminar %(phone)s?",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Enviamosche un SMS ao +%(msisdn)s. Escribe o código de verificación que contén.",
"Phone Number": "Número de teléfono",
"This user has not verified all of their sessions.": "Esta usuaria non verificou ningunha das súas sesións.",
"You have not verified this user.": "Non verificaches esta usuaria.",
"You have verified this user. This user has verified all of their sessions.": "Verificaches esta usuaria. A usuaria verificou todas as súas sesións.",
"Someone is using an unknown session": "Alguén está a usar unha sesión descoñecida",
"This room is end-to-end encrypted": "Esta sala está cifrada extremo-a-extremo",
"Everyone in this room is verified": "Todas nesta sala están verificadas",
"Edit message": "Editar mensaxe",
"Mod": "Mod",
"Your key share request has been sent - please check your other sessions for key share requests.": "Enviouse a solicitude de compartir chave - comproba as túas outras sesións para solicitudes de compartir chave."
}

View file

@ -2415,7 +2415,7 @@
"Waiting for your other session to verify…": "A másik munkameneted ellenőrzésére várunk…",
"You've successfully verified your device!": "Sikeresen ellenőrizted az eszközödet!",
"Message deleted": "Üzenet törölve",
"Message deleted by %(name)s": "Üzenetet törölte: %(name)s",
"Message deleted by %(name)s": "%(name)s törölte az üzenetet",
"QR Code": "QR kód",
"To continue, use Single Sign On to prove your identity.": "A folytatáshoz a személyazonosságod megerősítéséhez használd az egyszeri bejelentkezést.",
"Confirm to continue": "Erősítsd meg a továbblépéshez",
@ -2470,5 +2470,29 @@
"Address (optional)": "Cím (nem kötelező)",
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Törlöd a szoba címét: %(alias)s és eltávolítod a könyvtárból ezt: %(name)s?",
"delete the address.": "cím törlése.",
"Use a different passphrase?": "Másik jelmondat használata?"
"Use a different passphrase?": "Másik jelmondat használata?",
"sent an image.": "kép elküldve.",
"You: %(message)s": "Te: %(message)s",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "A szerver adminisztrátorod alapesetben kikapcsolta a végpontok közötti titkosítást a közvetlen beszélgetésekben.",
"Emoji picker": "Emodzsi választó",
"No recently visited rooms": "Nincsenek nemrégiben meglátogatott szobák",
"People": "Emberek",
"Sort by": "Rendezve:",
"Unread rooms": "Olvasatlan szobák",
"Always show first": "Mindig az elsőt mutatja",
"Show": "Mutat",
"Message preview": "Üzenet előnézet",
"List options": "Lista beállítások",
"Show %(count)s more|other": "Még %(count)s megjelenítése",
"Show %(count)s more|one": "Még %(count)s megjelenítése",
"Leave Room": "Szoba elhagyása",
"Room options": "Szoba beállítások",
"Switch to light mode": "Világos módra váltás",
"Switch to dark mode": "Sötét módra váltás",
"Switch theme": "Kinézet váltása",
"Security & privacy": "Biztonság és adatvédelem",
"All settings": "Minden beállítás",
"Archived rooms": "Archivált szobák",
"Feedback": "Visszajelzés",
"Account settings": "Fiók beállítása"
}

View file

@ -2476,5 +2476,31 @@
"Restart": "Riavvia",
"Upgrade your Riot": "Aggiorna Riot",
"A new version of Riot is available!": "È disponibile una nuova versione di Riot!",
"New version available. <a>Update now.</a>": "Nuova versione disponibile. <a>Aggiorna ora.</a>"
"New version available. <a>Update now.</a>": "Nuova versione disponibile. <a>Aggiorna ora.</a>",
"Emoji picker": "Selettore emoji",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "L'amministratore del server ha disattivato la crittografia end-to-end in modo predefinito nelle stanze private e nei messaggi diretti.",
"People": "Persone",
"Show %(n)s more": "Mostra altri %(n)s",
"Switch to light mode": "Passa alla modalità chiara",
"Switch to dark mode": "Passa alla modalità scura",
"Switch theme": "Cambia tema",
"Security & privacy": "Sicurezza e privacy",
"All settings": "Tutte le impostazioni",
"Archived rooms": "Stanze archiviate",
"Feedback": "Feedback",
"Account settings": "Impostazioni account",
"sent an image.": "ha inviato un'immagine.",
"You: %(message)s": "Tu: %(message)s",
"No recently visited rooms": "Nessuna stanza visitata di recente",
"Sort by": "Ordina per",
"Unread rooms": "Stanze non lette",
"Show": "Mostra",
"Message preview": "Anteprima messaggio",
"List options": "Opzioni lista",
"Show %(count)s more|other": "Mostra altri %(count)s",
"Show %(count)s more|one": "Mostra %(count)s altro",
"Leave Room": "Lascia stanza",
"Room options": "Opzioni stanza",
"Activity": "Attività",
"A-Z": "A-Z"
}

View file

@ -1399,5 +1399,6 @@
"Use bots, bridges, widgets and sticker packs": "ボット、ブリッジ、ウィジェット、ステッカーパックを使用",
"Service": "サービス",
"Summary": "概要",
"Document": "ドキュメント"
"Document": "ドキュメント",
"Appearance": "外観"
}

View file

@ -6,7 +6,7 @@
"The version of Riot.im": "Riot.im versija",
"Your language of choice": "Jūsų pasirinkta kalba",
"Which officially provided instance you are using, if any": "Kurią oficialiai teikiamą instanciją naudojate, jei tokių yra",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Nepriklausomai nuo to ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą ar ne",
"Your homeserver's URL": "Jūsų serverio URL",
"Your identity server's URL": "Jūsų tapatybės serverio URL",
"Analytics": "Statistika",
@ -419,7 +419,7 @@
"New passwords must match each other.": "Nauji slaptažodžiai privalo sutapti.",
"I have verified my email address": "Aš patvirtinau savo el. pašto adresą",
"Return to login screen": "Grįžti į prisijungimą",
"Send Reset Email": "Siųsti atstatymo el. laišką",
"Send Reset Email": "Siųsti nustatymo iš naujo el. laišką",
"Incorrect username and/or password.": "Neteisingas vartotojo vardas ir/arba slaptažodis.",
"Please note you are logging into the %(hs)s server, not matrix.org.": "Atkreipkite dėmesį, kad jūs jungiatės prie %(hs)s serverio, o ne matrix.org.",
"Failed to fetch avatar URL": "Nepavyko gauti pseudoportreto URL",
@ -549,7 +549,7 @@
"Token incorrect": "Neteisingas prieigos raktas",
"Sign in with": "Prisijungti naudojant",
"Sign in": "Prisijungti",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Jeigu nenurodysite savo el. pašto adreso, negalėsite atstatyti savo slaptažodį. Ar esate tikri?",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Jeigu nenurodysite savo el. pašto adreso, negalėsite iš naujo nustatyti savo slaptažodžio. Ar esate tikri?",
"Please <a>contact your service administrator</a> to continue using the service.": "Norėdami toliau naudotis šia paslauga, <a>susisiekite su savo paslaugos administratoriumi</a>.",
"Create new room": "Sukurti naują kambarį",
"No results": "Jokių rezultatų",
@ -600,7 +600,7 @@
"This homeserver has exceeded one of its resource limits.": "Šis serveris viršijo vieno iš savo išteklių limitą.",
"Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie serverio. Bandoma iš naujo...",
"Enable widget screenshots on supported widgets": "Įjungti valdiklių ekrano kopijas palaikomuose valdikliuose",
"Export E2E room keys": "Eksportuoti E2E kambario raktus",
"Export E2E room keys": "Eksportuoti E2E kambarių raktus",
"Last seen": "Paskutinį kartą matytas",
"Unignore": "Nebeignoruoti",
"and %(count)s others...|other": "ir %(count)s kitų...",
@ -712,7 +712,7 @@
"This doesn't appear to be a valid email address": "Tai nepanašu į teisingą el. pašto adresą",
"Unable to add email address": "Nepavyko pridėti el. pašto adreso",
"Unable to verify email address.": "Nepavyko patvirtinti el. pašto adreso.",
"This will allow you to reset your password and receive notifications.": "Tai jums leis atstatyti savo slaptažodį ir gauti pranešimus.",
"This will allow you to reset your password and receive notifications.": "Tai jums leis iš naujo nustatyti slaptažodį ir gauti pranešimus.",
"Skip": "Praleisti",
"Username not available": "Vartotojo vardas negalimas",
"Username invalid: %(errMessage)s": "Neteisingas vartotojo vardas: %(errMessage)s",
@ -750,15 +750,15 @@
"Retry": "Bandyti dar kartą",
"Add Email Address": "Pridėti el. pašto adresą",
"Add Phone Number": "Pridėti telefono numerį",
"Whether or not you're logged in (we don't record your username)": "Nepriklausomai nuo to ar jūs prisijungę (mes neįrašome jūsų vartotojo vardo)",
"Whether or not you're logged in (we don't record your username)": "Ar jūs prisijungę ar ne (mes neįrašome jūsų vartotojo vardo)",
"Chat with Riot Bot": "Kalbėtis su Riot Botu",
"Sign In": "Prisijungti",
"Explore rooms": "Žvalgyti kambarius",
"Your Riot is misconfigured": "Jūsų Riot yra neteisingai sukonfigūruotas",
"Sign in to your Matrix account on %(serverName)s": "Prisijunkite prie savo Matrix paskyros %(serverName)s serveryje",
"Sign in to your Matrix account on <underlinedServerName />": "Prisijunkite prie savo paskyros <underlinedServerName /> serveryje",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Nepriklausomai nuo to ar jūs naudojate 'duonos trupinių' funkciją (pseudoportretai virš kambarių sąrašo)",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur šis puslapis įtraukia identifikuojamą informaciją, kaip kambarys, vartotojas ar grupės ID, tie duomenys yra pašalinami prieš siunčiant į serverį.",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ar jūs naudojate 'duonos trupinių' funkciją ar ne (pseudoportretai virš kambarių sąrašo)",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ten, kur šis puslapis įtraukia identifikuojamą informaciją, kaip kambarys, vartotojas ar grupės ID, tie duomenys yra pašalinami prieš siunčiant į serverį.",
"The remote side failed to pick up": "Nuotolinėi pusėi nepavyko atsiliepti",
"Call failed due to misconfigured server": "Skambutis nepavyko dėl neteisingai sukonfigūruoto serverio",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Paprašykite savo serverio administratoriaus (<code>%(homeserverDomain)s</code>) sukonfiguruoti TURN serverį, kad skambučiai veiktų patikimai.",
@ -903,7 +903,7 @@
"General failure": "Bendras triktis",
"Messages containing my username": "Žinutės, kuriose yra mano vartotojo vardas",
"Set a new account password...": "Nustatyti naują paskyros slaptažodį...",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jei jūs pateikėte klaidą per GitHub, derinimo žurnalai (debug logs) gali padėti mums surasti problemą. Derinimo žurnaluose yra programos naudojimo duomenys, įskaitant jūsų vartotojo vardą, ID ar kitus kambarių arba grupių, kuriuose jūs lankėtės, pavadinimus ir kitų vartotojų vardus. Juose nėra žinučių.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jei jūs pateikėte pranešimą apie klaidą per GitHub, derinimo žurnalai (debug logs) gali padėti mums surasti problemą. Derinimo žurnaluose yra programos naudojimo duomenys, įskaitant jūsų vartotojo vardą, ID ar kitus kambarių arba grupių, kuriuose jūs lankėtės, pavadinimus ir kitų vartotojų vardus. Juose nėra žinučių.",
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (galia %(powerLevelNumber)s)",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Derinimo žurnaluose yra programos naudojimo duomenys, įskaitant jūsų vartotojo vardą, ID ar kitus kambarių arba grupių, kuriuose jūs lankėtės, pavadinimus ir kitų vartotojų vardus. Juose nėra žinučių.",
"If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.": "Jei jūs negalite kažkieno surasti, paklauskite jų vartotojo vardo, pasidalinkite savo vartotojo vardu (%(userId)s) arba <a>profilio nuoroda</a>.",
@ -916,8 +916,8 @@
"Confirm": "Patvirtinti",
"Create your Matrix account on %(serverName)s": "Sukurkite savo Matrix paskyrą %(serverName)s serveryje",
"Create your Matrix account on <underlinedServerName />": "Sukurkite savo Matrix paskyrą <underlinedServerName /> serveryje",
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Nustatykite el. paštą paskyros susigrąžinimui. Naudokite el. paštą ar tel. nr. norėdami pasirinktinai būti aptinkami esamų kontaktų.",
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Nustatykite el. paštą paskyros susigrąžinimui. Naudokite el. paštą norėdami pasirinktinai būti aptinkami esamų kontaktų.",
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Nustatykite el. paštą paskyros atgavimui. Naudokite el. paštą ar tel. nr. norėdami pasirinktinai būti aptinkami esamų kontaktų.",
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Nustatykite el. paštą paskyros atgavimui. Naudokite el. paštą norėdami pasirinktinai būti aptinkami esamų kontaktų.",
"Sign in instead": "Prisijungti",
"A verification email will be sent to your inbox to confirm setting your new password.": "Patvirtinimo laiškas bus išsiųstas į jūsų pašto dėžutę tam, kad patvirtintumėte naujo slaptažodžio nustatymą.",
"Set a new password": "Nustatykite naują slaptažodį",
@ -1017,10 +1017,10 @@
"Show read receipts sent by other users": "Rodyti kitų vartotojų siųstus perskaitymo kvitus",
"Order rooms by name": "Rūšiuoti kambarius pagal pavadinimą",
"The other party cancelled the verification.": "Kita šalis atšaukė patvirtinimą.",
"Public Name": "Viešas Vardas",
"Public Name": "Viešas pavadinimas",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Užšifruotos žinutės yra apsaugotos visapusiu šifravimu. Tik jūs ir gavėjas(-ai) turi raktus šioms žinutėms perskaityti.",
"Back up your keys before signing out to avoid losing them.": "Prieš atsijungdami sukurkite atsarginę savo raktų kopiją, kad išvengtumėte jų praradimo.",
"Start using Key Backup": "Pradėti naudoti Atsarginę Raktų Kopiją",
"Start using Key Backup": "Pradėti naudoti atsarginę raktų kopiją",
"Display Name": "Rodomas Vardas",
"Please verify the room ID or alias and try again.": "Prašome patikrinti kambario ID arba slapyvardį ir bandyti dar kartą.",
"Room %(name)s": "Kambarys %(name)s",
@ -1064,7 +1064,7 @@
"Server Name": "Serverio Pavadinimas",
"Other servers": "Kiti serveriai",
"Add room": "Sukurti kambarį",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Slaptažodžio keitimas ištrins visų jūsų seansų šifravimo raktus, todėl nebebus galima perskaityti užšifruotos pokalbių istorijos. Sukurkite atsarginę raktų kopiją arba eksportuokite savo kambarių raktus iš kito seanso prieš atstatydami slaptažodį.",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Slaptažodžio keitimas iš naujo nustatys visų jūsų seansų šifravimo raktus, todėl užšifruota pokalbių istorija taps neperskaitoma. Sukurkite atsarginę raktų kopiją arba eksportuokite savo kambarių raktus iš kito seanso prieš iš naujo nustatydami slaptažodį.",
"Set a display name:": "Nustatyti rodomą vardą:",
"Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Apsaugokite savo šifravimo raktus slaptafraze. Maksimaliam saugumui užtikrinti ji turi skirtis nuo jūsų paskyros slaptažodžio:",
"Enter a passphrase": "Įveskite slaptafrazę",
@ -1088,7 +1088,7 @@
"Back up encrypted message keys": "Padaryti atsargines šifruotų žinučių raktų kopijas",
"Set up with a recovery key": "Nustatyti su atgavimo raktu",
"Enter your recovery passphrase a second time to confirm it.": "Įveskite atgavimo slaptafrazę antrą kartą, kad ją patvirtintumėte.",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Jūsų atgavimo raktas yra atsarginė saugumo priemonė - jūs galite jį naudoti prieigos prie jūsų šifruotų žinučių atgavimui, jei pamiršite savo atgavimo slaptafrazę.",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Jūsų atgavimo raktas yra atsarginė saugumo priemonė - jūs galite jį naudoti prieigos prie jūsų šifruotų žinučių atkūrimui, jei pamiršite savo atgavimo slaptafrazę.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Laikykite šio rakto kopiją saugioje vietoje, pavyzdžiui slaptažodžių tvarkyklėje arba seife.",
"Your recovery key": "Jūsų atgavimo raktas",
"Copy": "Kopijuoti",
@ -1137,12 +1137,12 @@
"<b>Warning</b>: You should only do this on a trusted computer.": "<b>Įspėjimas</b>: Tai atlikite tik saugiame kompiuteryje.",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Pasiekite savo saugių žinučių istoriją ir kryžminio pasirašymo tapatybę, naudojamą kitų seansų patvirtinimui, įvesdami savo atgavimo slaptafrazę.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "Jei pamiršote savo atgavimo slaptafrazę jūs galite <button1>naudoti savo atgavimo raktą</button1> arba <button2>nustatyti naujus atgavimo nustatymus</button2>.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "Jei pamiršote savo atgavimo slaptafrazę jūs galite <button1>naudoti savo atgavimo raktą</button1> arba <button2>nustatyti naujus atgavimo nustatymus</button2>",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "Jei pamiršote savo atgavimo slaptafrazę jūs galite <button1>naudoti savo atgavimo raktą</button1> arba <button2>nustatyti naujas atgavimo parinktis</button2>",
"Confirm your identity by entering your account password below.": "Patvirtinkite savo tapatybę žemiau įvesdami savo paskyros slaptažodį.",
"Use an email address to recover your account": "Naudokite el. pašto adresą, kad prireikus galėtumėte atgauti paskyrą",
"Passwords don't match": "Slaptažodžiai nesutampa",
"Use lowercase letters, numbers, dashes and underscores only": "Naudokite tik mažąsias raides, brūkšnelius ir pabraukimus",
"Great! This recovery passphrase looks strong enough.": "Puiku! Ši slaptafrazė atrodo pakankamai stipri.",
"Great! This recovery passphrase looks strong enough.": "Puiku! Ši atgavimo slaptafrazė atrodo pakankamai stipri.",
"That matches!": "Tai sutampa!",
"That doesn't match.": "Tai nesutampa.",
"Confirm your recovery passphrase": "Patvirtinti atgavimo slaptafrazę",
@ -1154,7 +1154,7 @@
"You can now verify your other devices, and other users to keep your chats safe.": "Jūs dabar galite patvirtinti kitus savo įrenginius ir kitus vartotojus, kad jūsų pokalbiai būtų saugūs.",
"Confirm recovery passphrase": "Patvirtinkite atgavimo slaptafrazę",
"You're done!": "Atlikta!",
"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.": "Pakeitus slaptažodį šiuo metu, visuose seansuose bus anuliuoti visapusio šifravimo raktai, tad šifruotų pokalbių istorija taps neperskaitoma, nebent jūs eksportuosite savo kambarių raktus ir po to importuosite juos atgal. Ateityje ši funkcija bus pataisyta.",
"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.": "Pakeitus slaptažodį šiuo metu, visuose seansuose bus iš naujo nustatyti visapusio šifravimo raktai, tad šifruotų pokalbių istorija taps neperskaitoma, nebent jūs eksportuosite savo kambarių raktus ir po to importuosite juos atgal. Ateityje ši funkcija bus pataisyta.",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Jūsų slaptažodis buvo sėkmingai pakeistas. Jūs kituose seansuose negausite pranešimų, kol iš naujo prie jų neprisijungsite",
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "El. laiškas buvo išsiųstas į %(emailAddress)s. Kai paspausite jame esančią nuorodą, tada spauskite žemiau.",
"Your password has been reset.": "Jūsų slaptažodis buvo iš naujo nustatytas.",
@ -1254,12 +1254,12 @@
"Report Content": "Pranešti",
"Nice, strong password!": "Puiku, stiprus slaptažodis!",
"Old cryptography data detected": "Aptikti seni kriptografijos duomenys",
"Verify this login": "Patvirtinti šį prisijungimą",
"Verify this login": "Patvirtinkite šį prisijungimą",
"Registration has been disabled on this homeserver.": "Registracija šiame serveryje išjungta.",
"You can now close this window or <a>log in</a> to your new account.": "Jūs galite uždaryti šį langą arba <a>prisijungti</a> į savo naują paskyrą.",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Patvirtinkite savo tapatybę verifikuodami šį prisijungimą viename iš kitų jūsų seansų, suteikdami jam prieigą prie šifruotų žinučių.",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Identifikuokite save patvirtindami šį prisijungimą viename iš kitų jūsų seansų ir suteikdami jam prieigą prie šifruotų žinučių.",
"This requires the latest Riot on your other devices:": "Tam reikia naujausios Riot versijos kituose jūsų įrenginiuose:",
"or another cross-signing capable Matrix client": "arba kitą kryžminį pasirašymą palaikantį Matrix klientą",
"or another cross-signing capable Matrix client": "arba kito kryžminį pasirašymą palaikančio Matrix kliento",
"Use Recovery Passphrase or Key": "Naudoti atgavimo slaptafrazę arba raktą",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Atnaujinkite šį seansą, kad jam būtų leista patvirtinti kitus seansus, suteikiant jiems prieigą prie šifruotų žinučių ir juos pažymint kaip patikimus kitiems vartotojams.",
"Use Single Sign On to continue": "Norėdami tęsti naudokite Vieno Prisijungimo sistemą",
@ -1285,8 +1285,8 @@
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Jūs vis dar <b>dalijatės savo asmeniniais duomenimis</b> tapatybės serveryje <idserver />.",
"Identity Server (%(server)s)": "Tapatybės serveris (%(server)s)",
"Enter a new identity server": "Pridėkite naują tapatybės serverį",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Naudokite integracijų valdiklį <b>(%(serverName)s)</b> botų, valdiklių ir lipdukų valdymui.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Naudokite integracijų valdiklį botų, valdiklių ir lipdukų valdymui.",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Naudokite integracijų tvarkytuvą <b>(%(serverName)s)</b> botų, valdiklių ir lipdukų valdymui.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Naudokite integracijų tvarkytuvą botų, valdiklių ir lipdukų valdymui.",
"Manage integrations": "Valdyti integracijas",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų valdikliai 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.",
@ -1309,19 +1309,19 @@
"Your theme": "Jūsų tema",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Valdiklio ištrinimas pašalina jį visiems kambaryje esantiems vartotojams. Ar tikrai norite ištrinti šį valdiklį?",
"Enable 'Manage Integrations' in Settings to do this.": "Įjunkite 'Valdyti integracijas' nustatymuose, kad tai atliktumėte.",
"Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Jūsų Riot neleidžia jums naudoti integracijų valdiklio tam atlikti. Susisiekite su administratoriumi.",
"Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Jūsų Riot neleidžia jums naudoti integracijų tvarkytuvo tam atlikti. Susisiekite su administratoriumi.",
"Enter phone number (required on this homeserver)": "Įveskite telefono numerį (privaloma šiame serveryje)",
"Doesn't look like a valid phone number": "Tai nepanašu į veikiantį telefono numerį",
"Invalid homeserver discovery response": "Klaidingas serverio radimo atsakas",
"Invalid identity server discovery response": "Klaidingas tapatybės serverio radimo atsakas",
"The phone number entered looks invalid": "Įvestas telefono numeris atrodo klaidingas",
"Double check that your server supports the room version chosen and try again.": "Dar kartą įsitikinkite, kad jūsų serveris palaiko pasirinktą kambario versiją ir bandykite iš naujo.",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Nesvarbu, ar naudojate „Riot“ įrenginyje, kuriame pagrindinis įvesties mechanizmas yra lietimas",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Ar naudojate Riot įrenginyje, kuriame pagrindinis įvesties mechanizmas yra lietimas",
"Session already verified!": "Seansas jau patvirtintas!",
"WARNING: Session already verified, but keys do NOT MATCH!": "ĮSPĖJIMAS: Seansas jau patvirtintas, bet raktai NESUTAMPA!",
"Enable cross-signing to verify per-user instead of per-session": "Įjunkite kryžminį pasirašymą, kad patvirtintumėte vartotoją, o ne seansą",
"Enable Emoji suggestions while typing": "Įjungti jaustukų pasiūlymus rašant",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Rodyti priminimą įjungti saugų žinučių atgavimą šifruotuose kambariuose",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Rodyti priminimą įjungti saugių žinučių atgavimą šifruotuose kambariuose",
"Enable automatic language detection for syntax highlighting": "Įjungti automatinį kalbos aptikimą sintaksės paryškinimui",
"Enable big emoji in chat": "Įjungti didelius jaustukus pokalbiuose",
"Enable Community Filter Panel": "Įjungti bendruomenės filtrų skydelį",
@ -1409,24 +1409,24 @@
"Accept <policyLink /> to continue:": "Sutikite su <policyLink />, kad tęstumėte:",
"Upload": "Įkelti",
"Your homeserver does not support cross-signing.": "Jūsų serveris nepalaiko kryžminio pasirašymo.",
"Cross-signing and secret storage are enabled.": "Kryžminis pasirašymas ir slapta saugykla yra įjungta.",
"Cross-signing and secret storage are enabled.": "Kryžminis pasirašymas ir slapta saugykla yra įjungti.",
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Jūsų paskyra slaptoje saugykloje turi kryžminio pasirašymo tapatybę, bet šis seansas dar ja nepasitiki.",
"Cross-signing and secret storage are not yet set up.": "Kryžminis pasirašymas ir slapta saugykla dar nėra nustatyti.",
"Reset cross-signing and secret storage": "Atstatyti kryžminį pasirašymą ir slaptą saugyklą",
"Reset cross-signing and secret storage": "Iš naujo nustatyti kryžminį pasirašymą ir slaptą saugyklą",
"Bootstrap cross-signing and secret storage": "Prikabinti kryžminį pasirašymą ir slaptą saugyklą",
"Cross-signing public keys:": "Kryžminio pasirašymo vieši raktai:",
"Cross-signing private keys:": "Kryžminio pasirašymo privatūs raktai:",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individualiai patikrinkite kiekvieną vartotojo naudojamą seansą, kad pažymėtumėte jį kaip patikimą, nepasitikint kryžminiu pasirašymu patvirtintais įrenginiais.",
"Enable": "Įjungti",
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> session <device></device>": "Atsarginė kopija turi <validity>galiojantį</validity> parašą iš <verify>patikrinto</verify> seanso <device></device>",
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "Atsarginė kopija turi <validity>galiojantį</validity> parašą iš <verify>nepatikrinto</verify> seanso <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "Atsarginė kopija turi <validity>negaliojantį</validity> parašą iš <verify>patikrinto</verify> seanso <device></device>",
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "Atsarginė kopija turi <validity>negaliojantį</validity> parašą iš <verify>nepatikrinto</verify> seanso <device></device>",
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> session <device></device>": "Atsarginė kopija turi <validity>galiojantį</validity> <verify>patvirtinto</verify> seanso <device></device> parašą",
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "Atsarginė kopija turi <validity>galiojantį</validity> <verify>nepatvirtinto</verify> seanso <device></device> parašą",
"Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "Atsarginė kopija turi <validity>negaliojantį</validity> <verify>patvirtinto</verify> seanso <device></device> parašą",
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "Atsarginė kopija turi <validity>negaliojantį</validity> <verify>nepatvirtinto</verify> seanso <device></device> parašą",
"Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Atsarginė rakto kopija saugoma slaptoje saugykloje, bet ši funkcija nėra įjungta šiame seanse. Įjunkite kryžminį pasirašymą Laboratorijose, kad galėtumėte keisti atsarginės rakto kopijos būseną.",
"Enable desktop notifications for this session": "Įjungti darbalaukio pranešimus šiam seansui",
"Enable audible notifications for this session": "Įjungti garsinius pranešimus šiam seansui",
"wait and try again later": "palaukite ir bandykite vėliau dar kartą",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jei jūs nenorite naudoti <server /> radimui ir tam, kad būtumėte randamas esamų, jums žinomų kontaktų, žemiau įveskite kitą tapatybės serverį.",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jei jūs nenorite naudoti <server /> serverio radimui ir tam, kad būtumėte randamas esamų, jums žinomų kontaktų, žemiau įveskite kitą tapatybės serverį.",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Tapatybės serverio naudojimas yra pasirinktinis. Jei jūs pasirinksite jo nenaudoti, jūs nebūsite randamas kitų vartotojų ir neturėsite galimybės pakviesti kitų nurodydamas el. paštą ar telefoną.",
"Do not use an identity server": "Nenaudoti tapatybės serverio",
"Cross-signing": "Kryžminis pasirašymas",
@ -1462,7 +1462,7 @@
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Pasiekite savo saugių žinučių istoriją ir kryžminio pasirašymo tapatybę, naudojamą kitų seansų patvirtinimui, įvesdami savo atgavimo raktą.",
"Session verified": "Seansas patvirtintas",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Neįmanoma prisijungti prie serverio per HTTP, kai naršyklės juostoje yra HTTPS URL. Naudokite HTTPS arba <a>įjunkite nesaugias rašmenas</a>.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Jūsų naujas seansas dabar yra patvirtintas. Jis turi prieigą prie jūsų šifruotų žinučių ir kiti vartotojai matys jį kaip patikimą.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Jūsų naujas seansas buvo patvirtintas. Jis turi prieigą prie jūsų šifruotų žinučių ir kiti vartotojai matys jį kaip patikimą.",
"Your new session is now verified. Other users will see it as trusted.": "Jūsų naujas seansas dabar yra patvirtintas. Kiti vartotojai matys jį kaip patikimą.",
"NOT verified": "Nepatvirtinta",
"verified": "patvirtinta",
@ -1484,9 +1484,9 @@
"Send typing notifications": "Siųsti spausdinimo pranešimus",
"Automatically replace plain text Emoji": "Automatiškai pakeisti paprasto teksto jaustukus",
"Mirror local video feed": "Atkartoti lokalų video tiekimą",
"Allow Peer-to-Peer for 1:1 calls": "Leisti \"Peer-to-Peer\" 1:1 skambučiams",
"Allow Peer-to-Peer for 1:1 calls": "Leisti tiesioginį \"Peer-to-Peer\" sujungimą 1:1 skambučiams",
"Prompt before sending invites to potentially invalid matrix IDs": "Klausti prieš siunčiant pakvietimus galimai netinkamiems matrix ID",
"Show rooms with unread notifications first": "Pirmiausia rodyti kambarius su neskaitytais pranešimais",
"Show rooms with unread notifications first": "Pirmiausia rodyti kambarius su neperskaitytais pranešimais",
"Show shortcuts to recently viewed rooms above the room list": "Rodyti neseniai peržiūrėtų kambarių nuorodas virš kambarių sąrašo",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Leisti atsarginį skambučių pagalbos serverį turn.matrix.org, kai jūsų serveris to neteikia (jūsų IP adresas bus bendrintas pokalbio metu)",
"Show previews/thumbnails for images": "Rodyti vaizdų peržiūras/miniatiūras",
@ -1525,7 +1525,7 @@
"Jump to start/end of the composer": "Peršokti į rašymo pradžią/pabaigą",
"Navigate composer history": "Naršyti rašymo istoriją",
"Navigate up/down in the room list": "Naršyti aukštyn/žemyn kambarių saraše",
"Select room from the room list": "Pasirinkite kambarį iš kambarių sąrašo",
"Select room from the room list": "Pasirinkti kambarį iš kambarių sąrašo",
"Collapse room list section": "Sutraukti kambarių sąrašo skyrių",
"Expand room list section": "Išplėsti kambarių sąrašo skyrių",
"Clear room list filter field": "Išvalyti kambarių sąrašo filtro lauką",
@ -1535,12 +1535,12 @@
"Can't leave Server Notices room": "Negalima išeiti iš Serverio Pranešimų kambario",
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Šis kambarys yra naudojamas svarbioms žinutėms iš serverio, todėl jūs negalite jo palikti.",
"Terms and Conditions": "Taisyklės ir Sąlygos",
"Self-verification request": "Savipatvirtinimo užklausa",
"Self-verification request": "Savarankiško patvirtinimo užklausa",
"Logout": "Atsijungti",
"Reject & Ignore user": "Atmesti ir ignoruoti vartotoją",
"Reject invitation": "Atmesti pakvietimą",
"Unable to reject invite": "Nepavyko atmesti pakvietimo",
"Whether you're using Riot as an installed Progressive Web App": "Nesvarbu, ar naudojate Riot kaip įdiegtą progresyviąją žiniatinklio programą",
"Whether you're using Riot as an installed Progressive Web App": "Ar naudojate Riot kaip įdiegtą progresyviąją žiniatinklio programą",
"Your user agent": "Jūsų vartotojo agentas",
"The information being sent to us to help make Riot better includes:": "Informacija, siunčiama mums siekiant pagerinti Riot, yra:",
"Invite only": "Tik pakviestiems",
@ -1557,7 +1557,7 @@
"Ask this user to verify their session, or manually verify it below.": "Paprašykite šio vartotojo patvirtinti savo seansą arba patvirtinkite jį rankiniu būdu žemiau.",
"Encryption upgrade available": "Galimas šifravimo atnaujinimas",
"Verify this user by confirming the following number appears on their screen.": "Patvirtinkite šį vartotoją įsitikindami, kad jo ekrane rodomas toliau esantis skaičius.",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Atsarginė kopija turi <verify>nežinomo</verify> vartotojo parašą su %(deviceId)s ID",
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Atsarginė kopija turi <verify>nežinomo</verify> vartotojo parašą su ID %(deviceId)s",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Tvarkykite savo seansų pavadinimus ir iš jų atsijunkite žemiau, arba <a>patvirtinkite juos savo Vartotojo Profilyje</a>.",
"Please enter verification code sent via text.": "Įveskite patvirtinimo kodą išsiųstą teksto žinute.",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Teksto žinutė buvo išsiųsta numeriu +%(msisdn)s. Įveskite joje esantį patvirtinimo kodą.",
@ -1586,7 +1586,166 @@
"View": "Žiūrėti",
"Confirm encryption setup": "Patvirtinti šifravimo sąranką",
"Click the button below to confirm setting up encryption.": "Paspauskite mygtuką žemiau, kad patvirtintumėte šifravimo nustatymą.",
"Restore your key backup to upgrade your encryption": "Atstatykite savo raktų atsarginę kopiją, kad atnaujintumėte šifravimą",
"Restore your key backup to upgrade your encryption": "Atkurkite savo atsarginę raktų kopiją, kad atnaujintumėte šifravimą",
"Upgrade your encryption": "Atnaujinkite savo šifravimą",
"Failed to set topic": "Nepavyko nustatyti temos"
"Failed to set topic": "Nepavyko nustatyti temos",
"Show typing notifications": "Rodyti spausdinimo pranešimus",
"Show hidden events in timeline": "Rodyti paslėptus įvykius laiko juostoje",
"Session backup key:": "Seanso atsarginės kopijos raktas:",
"Unable to load key backup status": "Nepavyko įkelti atsarginės raktų kopijos būklės",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Prieš atsijungdami prijunkite šį seansą prie atsarginės raktų kopijos, kad neprarastumėte raktų, kurie gali būti tik šiame seanse.",
"Connect this session to Key Backup": "Prijungtii šį seansą prie atsarginės raktų kopijos",
"Backup key stored: ": "Atsarginės kopijos raktas saugomas: ",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Tam, kad galėtumėte rasti ir tam, kad būtumėte randamas esamų, jums žinomų kontaktų, jūs šiuo metu naudojate <server></server> tapatybės serverį. Jį pakeisti galite žemiau.",
"Identity Server": "Tapatybės serveris",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Šiuo metu jūs nenaudojate tapatybės serverio. Tam, kad galėtumėte rasti ir tam, kad būtumėte randamas esamų, jums žinomų kontaktų, pridėkite jį žemiau.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Atsijungimas nuo tapatybės serverio reikš, kad jūs nebebūsite randamas kitų vartotojų ir jūs nebegalėsite pakviesti kitų, naudodami jų el. paštą arba telefoną.",
"Appearance": "Išvaizda",
"Deactivate account": "Deaktyvuoti paskyrą",
"Identity Server is": "Tapatybės serveris yra",
"Timeline": "Laiko juosta",
"Key backup": "Atsarginė raktų kopija",
"Where youre logged in": "Kur esate prisijungę",
"A session's public name is visible to people you communicate with": "Seanso viešas pavadinimas yra matomas žmonėms su kuriais jūs bendraujate",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Pabandykite slinkti aukštyn laiko juostoje, kad sužinotumėte, ar yra ankstesnių.",
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tai visam laikui padarys jūsų paskyrą nebetinkama naudoti. Jūs nebegalėsite prisijungti ir niekas nebegalės iš naujo užregistruoti to pačio vartotojo ID. Jūsų paskyra išeis iš visų kambarių, kuriuose ji dalyvauja ir pašalins jūsų paskyros detales iš jūsų tapatybės serverio. <b>Šis veiksmas neatšaukiamas.</b>",
"Unable to validate homeserver/identity server": "Neįmanoma patvirtinti serverio/tapatybės serverio",
"No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Nėra sukonfigūruota jokio tapatybės serverio, tad jūs negalite pridėti el. pašto adreso, tam, kad galėtumėte iš naujo nustatyti savo slaptažodį ateityje.",
"Enter your custom identity server URL <a>What does this mean?</a>": "Įveskite savo pasirinktinio tapatybės serverio URL <a>Ką tai reiškia?</a>",
"Identity Server URL": "Tapatybės serverio URL",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Bandyta įkelti konkrečią vietą šio kambario laiko juostoje, bet jūs neturite leidimo peržiūrėti tos žinutės.",
"Failed to load timeline position": "Nepavyko įkelti laiko juostos pozicijos",
"Your Matrix account on %(serverName)s": "Jūsų Matrix paskyra %(serverName)s serveryje",
"Your Matrix account on <underlinedServerName />": "Jūsų Matrix paskyra <underlinedServerName /> serveryje",
"No identity server is configured: add one in server settings to reset your password.": "Nesukonfigūruotas joks tapatybės serveris: pridėkite jį serverio nustatymuose, kad iš naujo nustatytumėte slaptažodį.",
"Identity server URL does not appear to be a valid identity server": "Tapatybės serverio URL neatrodo kaip tinkamas tapatybės serveris",
"Scroll up/down in the timeline": "Slinkti aukštyn/žemyn laiko juostoje",
"Show developer tools": "Rodyti vystytojo įrankius",
"Low bandwidth mode": "Žemo duomenų pralaidumo režimas",
"Send read receipts for messages (requires compatible homeserver to disable)": "Siųsti žinučių perskaitymo kvitus (norint išjungti reikalingas suderinamas serveris)",
"Keep recovery passphrase in memory for this session": "Šiam seansui atgavimo slaptafrazę laikyti atmintyje",
"How fast should messages be downloaded.": "Kaip greitai žinutės turi būti parsiųstos.",
"Manually verify all remote sessions": "Rankiniu būdu patvirtinti visus nuotolinius seansus",
"well formed": "gerai suformuotas",
"unexpected type": "netikėto tipo",
"in memory": "atmintyje",
"not found": "nerasta",
"in secret storage": "slaptoje saugykloje",
"Self signing private key:": "Savarankiško pasirašymo privatus raktas:",
"cached locally": "lokaliame podėlyje",
"not found locally": "lokaliai nerasta",
"User signing private key:": "Vartotojo pasirašymo privatus raktas:",
"Secret storage public key:": "Slaptos saugyklos viešas raktas:",
"in account data": "paskyros duomenyse",
"Homeserver feature support:": "Serverio funkcijų palaikymas:",
"exists": "yra",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot negali saugiai podėlyje talpinti užšifruotų žinučių, kol veikia interneto naršyklėje. Naudokite <riotLink>Riot Desktop</riotLink>, kad užšifruotos žinutės būtų rodomos paieškos rezultatuose.",
"This session is backing up your keys. ": "Šis seansas kuria atsargines jūsų raktų kopijas. ",
"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.": "Šis seansas <b>nekuria atsarginių raktų kopijų</b>, bet jūs jau turite atsarginę kopiją iš kurios galite atkurti ir pridėti.",
"All keys backed up": "Atsarginė kopija sukurta visiems raktams",
"Backup has a <validity>valid</validity> signature from this user": "Atsarginė kopija turi <validity>galiojantį</validity> šio vartotojo parašą",
"Backup has a <validity>invalid</validity> signature from this user": "Atsarginė kopija turi <validity>negaliojantį</validity> šio vartotojo parašą",
"Backup has a signature from <verify>unknown</verify> session with ID %(deviceId)s": "Atsarginė kopija turi <verify>nežinomo</verify> seanso parašą su ID %(deviceId)s",
"Backup has a <validity>valid</validity> signature from this session": "Atsarginė kopija turi <validity>galiojantį</validity> šio seanso parašą",
"Backup has an <validity>invalid</validity> signature from this session": "Atsarginė kopija turi <validity>negaliojantį</validity> šio seanso parašą",
"This backup is trusted because it has been restored on this session": "Ši atsarginė kopija yra patikima, nes buvo atkurta šiame seanse",
"Message search": "Žinučių paieška",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Šio seanso duomenų išvalymas yra negrįžtamas. Šifruotos žinutės bus prarastos, nebent buvo sukurta jų raktų atsarginė kopija.",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Trūksta kai kurių seanso duomenų, įskaitant šifruotų žinučių raktus. Atsijunkite ir prisijunkite, kad tai išspręstumėte, atkurdami raktus iš atsarginės kopijos.",
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Nepavyko pasiekti slaptos saugyklos. Patikrinkite, ar įvedėte teisingą atgavimo slaptafrazę.",
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Nepavyko pasiekti slaptos saugyklos. Patikrinkite, ar įvedėte teisingą atgavimo raktą.",
"Restoring keys from backup": "Raktų atkūrimas iš atsarginės kopijos",
"Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Atsarginės kopijos nepavyko iššifruoti naudojant šį atgavimo raktą: patikrinkite, ar įvedėte teisingą atgavimo raktą.",
"Unable to query secret storage status": "Slaptos saugyklos būsenos užklausa neįmanoma",
"Unable to set up secret storage": "Neįmanoma nustatyti slaptos saugyklos",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Užšifruotą jūsų raktų kopiją saugosime savo serveryje. Apsaugokite savo atsarginę kopiją naudodami atgavimo slaptafrazę.",
"Your keys are being backed up (the first backup could take a few minutes).": "Kuriama jūsų raktų atsarginė kopija (pirmas atsarginės kopijos sukūrimas gali užtrukti kelias minutes).",
"Create key backup": "Sukurti atsarginę raktų kopiją",
"Unable to create key backup": "Nepavyko sukurti atsarginės raktų kopijos",
"Your homeserver does not support session management.": "Jūsų serveris nepalaiko seansų tvarkymo.",
"Your homeserver has exceeded its user limit.": "Jūsų serveris pasiekė savo vartotojų limitą.",
"Your homeserver has exceeded one of its resource limits.": "Jūsų serveris pasiekė vieną iš savo resursų limitų.",
"Never send encrypted messages to unverified sessions from this session": "Niekada iš šio seanso nesiųsti šifruotų žinučių nepatvirtintiems seansams",
"Never send encrypted messages to unverified sessions in this room from this session": "Niekada iš šio seanso nesiųsti šifruotų žinučių šiame kambaryje nepatvirtintiems seansams",
"Unable to load session list": "Neįmanoma įkelti seansų sąrašo",
"Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Patvirtinkite šių seansų ištrinimą, naudodamiesi Vienu Prisijungimu, kad įrodytumėte savo tapatybę.",
"Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Patvirtinkite šio seanso ištrinimą, naudodamiesi Vienu Prisijungimu, kad įrodytumėte savo tapatybę.",
"Click the button below to confirm deleting these sessions.|other": "Paspauskite žemiau esantį mygtuką, kad patvirtintumėte šių seansų ištrynimą.",
"Click the button below to confirm deleting these sessions.|one": "Paspauskite žemiau esantį mygtuką, kad patvirtintumėte šio seanso ištrynimą.",
"Connecting to integration manager...": "Jungiamasi prie integracijų tvarkytuvo...",
"Cannot connect to integration manager": "Neįmanoma prisijungti prie integracijų tvarkytuvo",
"The integration manager is offline or it cannot reach your homeserver.": "Integracijų tvarkytuvas yra išjungtas arba negali pasiekti jūsų serverio.",
"Disconnect": "Atsijungti",
"Disconnect anyway": "Vis tiek atsijungti",
"Credits": "Padėka",
"For help with using Riot, click <a>here</a>.": "Norėdami gauti pagalbos naudojant Riot, paspauskite <a>čia</a>.",
"For help with using Riot, click <a>here</a> or start a chat with our bot using the button below.": "Norėdami gauti pagalbos naudojant Riot, paspauskite <a>čia</a> arba pradėkite pokalbį su mūsų botu pasinaudoję žemiau esančiu mygtuku.",
"Bug reporting": "Pranešti apie klaidą",
"Clear cache and reload": "Išvalyti podėlį ir perkrauti",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Norėdami pranešti apie su Matrix susijusią saugos problemą, perskaitykite Matrix.org <a>Saugumo Atskleidimo Poliiką</a>.",
"FAQ": "DUK",
"Keyboard Shortcuts": "Spartieji klavišai",
"Versions": "Versijos",
"Homeserver is": "Serveris yra",
"Import E2E room keys": "Importuoti E2E kambarių raktus",
"Session ID:": "Seanso ID:",
"Session key:": "Seanso raktas:",
"Riot collects anonymous analytics to allow us to improve the application.": "Riot renka anoniminę analizę, kad galėtume patobulinti programą.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatumas mums yra svarbus, todėl mes nerenkame jokių asmeninių ar identifikuojamų duomenų savo analitikai.",
"Learn more about how we use analytics.": "Sužinokite daugiau apie tai, kaip mes naudojame analitiką.",
"Reset": "Iš naujo nustatyti",
"Failed to connect to integration manager": "Nepavyko prisijungti prie integracijų tvarkytuvo",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Naudojimasis šiuo valdikliu gali pasidalinti duomenimis <helpIcon /> su %(widgetDomain)s ir jūsų integracijų tvarkytuvu.",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Prašome <newIssueLink>sukurti naują problemą</newIssueLink> GitHub'e, kad mes galėtume ištirti šią klaidą.",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Pasakyite mums kas nutiko, arba, dar geriau, sukurkite GitHub problemą su jos apibūdinimu.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Prieš pateikiant žurnalus jūs turite <a>sukurti GitHub problemą</a>, kad apibūdintumėte savo problemą.",
"GitHub issue": "GitHub problema",
"Notes": "Pastabos",
"Integrations are disabled": "Integracijos yra išjungtos",
"Integrations not allowed": "Integracijos neleidžiamos",
"Integration Manager": "Integracijų tvarkytuvas",
"This looks like a valid recovery key!": "Tai panašu į galiojantį atgavimo raktą!",
"Not a valid recovery key": "Negaliojantis atgavimo raktas",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "Jei pamiršote savo atgavimo raktą, galite <button>nustatyti naujas atgavimo parinktis</button>.",
"Recovery key mismatch": "Atgavimo rakto neatitikimas",
"Incorrect recovery passphrase": "Neteisinga atgavimo slaptafrazė",
"Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Nepavyko iššifruoti atsarginės kopijos naudojant šią atgavimo slaptafrazę: patikrinkite, ar įvedėte teisingą atgavimo slaptafrazę.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Prieikite prie savo saugių žinučių istorijos ir nustatykite saugių žinučių siuntimą, įvesdami atgavimo slaptafrazę.",
"Access your secure message history and set up secure messaging by entering your recovery key.": "Prieikite prie savo saugių žinučių istorijos ir nustatykite saugių žinučių siuntimą, įvesdami atgavimo raktą.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "Jei pamiršote savo atgavimo raktą, galite <button>nustatyti naujas atgavimo parinktis</button>",
"Regain access to your account and recover encryption keys stored in this session. Without them, you wont be able to read all of your secure messages in any session.": "Susigrąžinkite prieigą prie savo paskyros ir atgaukite šifravimo raktus, saugomus šiame seanse. Be jų jūs negalėsite perskaityti visų savo saugių žinučių bet kuriame seanse.",
"Restore": "Atkurti",
"Use a different passphrase?": "Naudoti kitą slaptafrazę?",
"Repeat your recovery passphrase...": "Pakartokite savo atgavimo slaptafrazę...",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Nenustatę saugių žinučių atgavimo, negalėsite atkurti užšifruotų žinučių istorijos, jei atsijungsite ar naudosite kitą seansą.",
"Set up Secure Message Recovery": "Nustatyti saugių žinučių atgavimą",
"Secure your backup with a recovery passphrase": "Apsaugokite savo atsarginę kopiją atgavimo slaptafraze",
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Nenustatę saugių žinučių atgavimo, atsijungdami jūs prarasite savo saugių žinučių istoriją.",
"New Recovery Method": "Naujas atgavimo metodas",
"A new recovery passphrase and key for Secure Messages have been detected.": "Buvo aptikta nauja atgavimo slaptafrazė ir saugių žinučių raktas.",
"This session is encrypting history using the new recovery method.": "Šis seansas šifruoja istoriją naudodamas naują atgavimo metodą.",
"Recovery Method Removed": "Atgavimo metodas pašalintas",
"This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Ši seansas aptiko, kad buvo pašalinta jūsų atgavimo slaptafrazė ir saugių žinučių raktas.",
"If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Jei tai padarėte netyčia, šiame seanse galite nustatyti saugias žinutes, kurios pakartotinai užšifruos šio seanso žinučių istoriją nauju atgavimo metodu.",
"Toggle Bold": "Perjungti paryškinimą",
"Toggle Italics": "Perjungti kursyvą",
"Toggle Quote": "Perjungti citatą",
"New line": "Nauja eilutė",
"Navigate recent messages to edit": "Naršyti naujausius pranešimus redagavimui",
"Cancel replying to a message": "Atšaukti atsakymą į žinutę",
"Toggle microphone mute": "Perjungti mikrofono nutildymą",
"Toggle video on/off": "Perjungti vaizdo įjungimą/išjungimą",
"Dismiss read marker and jump to bottom": "Atsisakyti skaitymo žymeklio ir nušokti į apačią",
"Jump to oldest unread message": "Nušokti iki seniausios neperskaitytos žinutės",
"Upload a file": "Įkelti failą",
"Jump to room search": "Nušokti į kambarių paiešką",
"Previous/next unread room or DM": "Ankstesnis/sekantis neperskaitytas kambarys arba tiesioginė žinutė",
"Previous/next room or DM": "Ankstesnis/sekantis kambarys arba tiesioginė žinutė",
"Toggle the top left menu": "Perjungti viršutinį kairės pusės meniu",
"Close dialog or context menu": "Uždaryti dialogą arba kontekstinį meniu",
"Activate selected button": "Aktyvuoti pasirinktą mygtuką",
"Toggle right panel": "Perjungti dešinį skydelį",
"Toggle this dialog": "Perjungti šį dialogą",
"Move autocomplete selection up/down": "Perkelti automatinio užbaigimo pasirinkimą aukštyn/žemyn",
"Cancel autocomplete": "Atšaukti automatinį užbaigimą"
}

View file

@ -1344,5 +1344,15 @@
"Repeat your recovery passphrase...": "Gjenta gjenopprettingspassfrasen din …",
"Other users may not trust it": "Andre brukere kan kanskje mistro den",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reagerte med %(content)s</reactedWith>",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagerte med %(shortName)s</reactedWith>"
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagerte med %(shortName)s</reactedWith>",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s kan ikke forhåndsvises. Vil du bli med i den?",
"Messages in this room are end-to-end encrypted.": "Meldinger i dette rommet er start-til-slutt-kryptert.",
"Messages in this room are not end-to-end encrypted.": "Meldinger i dette rommet er ikke start-til-slutt-kryptert.",
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Meldinger i dette rommet er start-til-slutt-kryptert. Lær mer og verifiser denne brukeren i brukerprofilen deres.",
"Use a different passphrase?": "Vil du bruke en annen passfrase?",
"Emoji picker": "Emojivelger",
"Jump to read receipt": "Hopp til lesekvitteringen",
"Mention": "Nevn",
"Community Name": "Samfunnets navn",
"Dismiss read marker and jump to bottom": "Avføy lesekvitteringen og hopp ned til bunnen"
}

View file

@ -350,7 +350,7 @@
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Zo te zien is uw e-mailadres op deze thuisserver niet aan een Matrix-ID gekoppeld.",
"You seem to be in a call, are you sure you want to quit?": "Het ziet er naar uit dat u in gesprek bent, weet u zeker dat u wilt afsluiten?",
"You seem to be uploading files, are you sure you want to quit?": "Het ziet er naar uit dat u bestanden aan het uploaden bent, weet u zeker dat u wilt afsluiten?",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "U kunt deze veranderingen niet ongedaan maken aangezien u de gebruiker tot hetzelfde niveau als uzelf promoveert.",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "U zult deze veranderingen niet terug kunnen draaien, daar u de gebruiker tot uw eigen niveau promoveert.",
"This server does not support authentication with a phone number.": "Deze server biedt geen ondersteuning voor authenticatie met een telefoonnummer.",
"An error occurred: %(error_string)s": "Er is een fout opgetreden: %(error_string)s",
"Make Moderator": "Benoemen tot moderator",
@ -382,7 +382,7 @@
"Failed to invite": "Uitnodigen is mislukt",
"Failed to invite the following users to the %(roomName)s room:": "Kon de volgende gebruikers niet uitnodigen voor gesprek %(roomName)s:",
"Confirm Removal": "Verwijdering bevestigen",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Weet u zeker dat u deze gebeurtenis wilt verwijderen? Wees u er wel van bewust dat als u een gespreksnaam of onderwerpswijziging verwijdert, u de verandering mogelijk ongedaan maakt.",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Weet u zeker dat u deze gebeurtenis wilt verwijderen? Besef wel dat het verwijderen van een van een gespreksnaams- of onderwerpswijziging die wijziging mogelijk teniet doet.",
"Unknown error": "Onbekende fout",
"Incorrect password": "Onjuist wachtwoord",
"To continue, please enter your password.": "Voer uw wachtwoord in om verder te gaan.",
@ -501,7 +501,7 @@
"Unban this user?": "Deze gebruiker ontbannen?",
"Ban this user?": "Deze gebruiker verbannen?",
"Mirror local video feed": "Lokale videoaanvoer ook elders opslaan (spiegelen)",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "U kunt deze actie niet ongedaan maken omdat u zichzelf degradeert. Als u de laatste bevoorrechte gebruiker in het gesprek bent, is het onmogelijk deze rechten terug te krijgen.",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Zelfdegradatie is onomkeerbaar. Als u de laatste bevoorrechte gebruiker in het gesprek bent zullen deze rechten voorgoed verloren gaan.",
"Unignore": "Niet meer negeren",
"Ignore": "Negeren",
"Jump to read receipt": "Naar het laatst gelezen bericht gaan",
@ -952,7 +952,7 @@
"User %(userId)s is already in the room": "De gebruiker %(userId)s is al aanwezig",
"User %(user_id)s does not exist": "Er bestaat geen gebruiker %(user_id)s",
"User %(user_id)s may or may not exist": "Er bestaat mogelijk geen gebruiker %(user_id)s",
"The user must be unbanned before they can be invited.": "De gebruiker kan niet uitgenodigd worden voordat diens ban ongedaan is gemaakt.",
"The user must be unbanned before they can be invited.": "De gebruiker kan niet uitgenodigd worden voordat diens ban teniet is gedaan.",
"Unknown server error": "Onbekende serverfout",
"Use a few words, avoid common phrases": "Gebruik enkele woorden - maar geen bekende uitdrukkingen",
"No need for symbols, digits, or uppercase letters": "Hoofdletters, cijfers of speciale tekens hoeven niet, mogen wel",
@ -1107,7 +1107,7 @@
"Language and region": "Taal en regio",
"Theme": "Thema",
"Account management": "Accountbeheer",
"Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account kan niet ongedaan gemaakt worden!",
"Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account is onherroepelijk!",
"General": "Algemeen",
"Legal": "Wettelijk",
"Credits": "Met dank aan",
@ -1356,7 +1356,7 @@
"Adds a custom widget by URL to the room": "Voegt met een URL een aangepaste widget toe aan het gesprek",
"Please supply a https:// or http:// widget URL": "Voer een https://- of http://-widget-URL in",
"You cannot modify widgets in this room.": "U kunt de widgets in dit gesprek niet aanpassen.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging voor %(targetDisplayName)s om toe te treden tot het gesprek ingetrokken.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging aan %(targetDisplayName)s toe te treden tot het gesprek ingetrokken.",
"Upgrade this room to the recommended room version": "Werk dit gesprek bij tot de aanbevolen versie",
"This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Dit gesprek draait op groepsgespreksversie <roomVersion />, die door deze thuisserver als <i>onstabiel</i> is gemarkeerd.",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Bijwerken zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een bijgewerkte versie starten.",
@ -1477,7 +1477,7 @@
"Cannot reach homeserver": "Kan thuisserver niet bereiken",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Zorg dat u een stabiele internetverbinding heeft, of neem contact op met de systeembeheerder",
"Your Riot is misconfigured": "Uw Riot is onjuist geconfigureerd",
"Ask your Riot admin to check <a>your config</a> for incorrect or duplicate entries.": "Vraag uw Riot-beheerder om <a>uw configuratie</a> na te kijken op onjuiste of duplicate items.",
"Ask your Riot admin to check <a>your config</a> for incorrect or duplicate entries.": "Vraag uw Riot-beheerder <a>uw configuratie</a> na te kijken op onjuiste of dubbele items.",
"Unexpected error resolving identity server configuration": "Onverwachte fout bij het oplossen van de identiteitsserverconfiguratie",
"Use lowercase letters, numbers, dashes and underscores only": "Gebruik enkel letters, cijfers, streepjes en underscores",
"Cannot reach identity server": "Kan identiteitsserver niet bereiken",
@ -1572,8 +1572,8 @@
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Gebruik een identiteitsserver om uit te nodigen via e-mail. Klik op Doorgaan om de standaardidentiteitsserver (%(defaultIdentityServerName)s) te gebruiken, of beheer de server in de instellingen.",
"Use an identity server to invite by email. Manage in Settings.": "Gebruik een identiteitsserver om uit te nodigen via e-mail. Beheer de server in de instellingen.",
"Multiple integration managers": "Meerdere integratiebeheerders",
"Send read receipts for messages (requires compatible homeserver to disable)": "Verstuur leesbevestigingen voor berichten (vereist compatibele thuisserver om uit te schakelen)",
"Accept <policyLink /> to continue:": "Aanvaard <policyLink /> om verder te gaan:",
"Send read receipts for messages (requires compatible homeserver to disable)": "Verstuur leesbevestigingen voor berichten (uitschakelen vereist een compatibele thuisserver)",
"Accept <policyLink /> to continue:": "Aanvaard <policyLink /> om door te gaan:",
"ID": "ID",
"Public Name": "Openbare naam",
"Change identity server": "Identiteitsserver wisselen",
@ -1599,11 +1599,11 @@
"No recent messages by %(user)s found": "Geen recente berichten door %(user)s gevonden",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Probeer omhoog te scrollen in de tijdslijn om te kijken of er eerdere zijn.",
"Remove recent messages by %(user)s": "Recente berichten door %(user)s verwijderen",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten door %(user)s te verwijderen. Dit kan niet ongedaan worden gemaakt. Wilt u doorgaan?",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Bij een groot aantal berichten kan dit even duren. Herlaad uw cliënt niet gedurende deze tijd.",
"Remove %(count)s messages|other": "%(count)s berichten verwijderen",
"Deactivate user?": "Gebruiker deactiveren?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deze gebruiker deactiveren zal hem/haar afmelden en verhinderen dat hij/zij zich weer aanmeldt. Bovendien zal hij/zij alle gesprekken waaraan hij/zij deelneemt verlaten. Deze actie kan niet ongedaan worden gemaakt. Weet u zeker dat u deze gebruiker wilt deactiveren?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deze gebruiker deactiveren zal hem/haar afmelden en verhinderen dat hij/zij zich weer aanmeldt. Bovendien zal hij/zij alle gesprekken waaraan hij/zij deelneemt verlaten. Deze actie is onherroepelijk. Weet u zeker dat u deze gebruiker wilt deactiveren?",
"Deactivate user": "Gebruiker deactiveren",
"Remove recent messages": "Recente berichten verwijderen",
"Bold": "Vet",
@ -1638,7 +1638,7 @@
"Explore rooms": "Gesprekken ontdekken",
"Show previews/thumbnails for images": "Toon voorbeelden voor afbeeldingen",
"Clear cache and reload": "Cache wissen en herladen",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit kan niet ongedaan gemaakt worden. Wilt u doorgaan?",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?",
"Remove %(count)s messages|one": "1 bericht verwijderen",
"%(count)s unread messages including mentions.|other": "%(count)s ongelezen berichten, inclusief vermeldingen.",
"%(count)s unread messages.|other": "%(count)s ongelezen berichten.",
@ -1791,7 +1791,7 @@
"Compare unique emoji": "Vergelijk unieke emoji",
"Compare a unique set of emoji if you don't have a camera on either device": "Vergelijk een unieke lijst met emoji als geen van beide apparaten een camera heeft",
"Start": "Start",
"Securely cache encrypted messages locally for them to appear in search results.": "Sla versleutelde berichten beveiligd op om ze weer te geven in zoekresultaten.",
"Securely cache encrypted messages locally for them to appear in search results.": "Sla versleutelde berichten veilig lokaal op om ze doorzoekbaar te maken.",
"Enable": "Inschakelen",
"Connecting to integration manager...": "Verbinding maken met de integratiebeheerder…",
"Cannot connect to integration manager": "Kan geen verbinding maken met de integratiebeheerder",
@ -1873,7 +1873,7 @@
"in secret storage": "in de sleutelopslag",
"Secret storage public key:": "Sleutelopslag publieke sleutel:",
"in account data": "in accountinformatie",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Sla versleutelde berichten beveiligd op om ze weer te geven in de zoekresultaten, door gebruik te maken van ",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Sla versleutelde berichten veilig lokaal op opdat ze doorzocht kunnen worden, middels ",
" to store messages from ": " om berichten op te slaan van ",
"Manage": "Beheren",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Verbind deze sessie met de sleutelback-up voordat u zich afmeldt. Dit voorkomt dat u sleutels verliest die alleen op deze sessie voorkomen.",
@ -1905,8 +1905,8 @@
"Show rooms with unread notifications first": "Gesprekken met ongelezen meldingen eerst tonen",
"Show shortcuts to recently viewed rooms above the room list": "Snelkoppelingen naar de gesprekken die u recent heeft bekeken bovenaan de gesprekslijst weergeven",
"Cancelling…": "Bezig met annuleren…",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Riot beschikt niet over alle onderdelen die nodig zijn om versleutelde berichten veilig in het lokale cachegeheugen te bewaren. Als u deze functie wilt uittesten, kunt u een aangepaste versie van Riot Desktop compileren, waarbij <nativeLink>de zoekonderdelen toegevoegd zijn</nativeLink>.",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot kan versleutelde berichten niet veilig bewaren in het lokale cachegeheugen wanneer het uitgevoerd wordt in een webbrowser. Gebruik <riotLink>Riot Desktop</riotLink> om versleutelde berichten in de zoekresultaten te laten verschijnen.",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "In Riot ontbreken enige modulen vereist voor het veilig lokaal bewaren van versleutelde berichten. Wilt u deze functie uittesten, compileer dan een aangepaste versie van Riot Desktop <nativeLink>die de zoekmodulen bevat</nativeLink>.",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Als Riot in een webbrowser draait kan het versleutelde berichten niet veilig lokaal bewaren. Gebruik <riotLink>Riot Desktop</riotLink> om versleutelde berichten doorzoekbaar te maken.",
"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.",
"Backup key stored in secret storage, but this feature is not enabled on this session. Please enable cross-signing in Labs to modify key backup state.": "Er is een back-upsleutel opgeslagen in de geheime opslag, maar deze functie is niet ingeschakeld voor deze sessie. Schakel kruiselings ondertekenen in in de experimentele instellingen om de sleutelback-upstatus te wijzigen.",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliseer uw ervaring met experimentele functies. <a>Klik hier voor meer informatie</a>.",
@ -1952,7 +1952,7 @@
"Yours, or the other users session": "De sessie van uzelf of de andere gebruiker",
"Not Trusted": "Niet vertrouwd",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s heeft zich aangemeld bij een nieuwe sessie zonder deze te verifiëren:",
"Ask this user to verify their session, or manually verify it below.": "Vraag deze gebruiker om zijn/haar sessie te verifiëren, of verifieer deze hieronder handmatig.",
"Ask this user to verify their session, or manually verify it below.": "Vraag deze gebruiker haar/zijn sessie te verifiëren, of verifieer die hieronder handmatig.",
"Done": "Klaar",
"Manually Verify": "Handmatig verifiëren",
"Trusted": "Vertrouwd",
@ -2028,10 +2028,10 @@
"More options": "Meer opties",
"Language Dropdown": "Taalselectie",
"Destroy cross-signing keys?": "Sleutels voor kruiselings ondertekenen verwijderen?",
"Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Het verwijderen van sleutels voor kruiselings ondertekenen kan niet ongedaan gemaakt worden. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.",
"Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Het verwijderen van sleutels voor kruiselings ondertekenen is onherroepelijk. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.",
"Clear cross-signing keys": "Sleutels voor kruiselings ondertekenen wissen",
"Clear all data in this session?": "Alle gegevens in deze sessie verwijderen?",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie kan niet ongedaan gemaakt worden. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van hun sleutels heeft.",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is onherroepelijk. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van hun sleutels heeft.",
"Verify session": "Sessie verifiëren",
"To verify that this session can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Controleer of de sleutel in de gebruikersinstellingen op het apparaat overeenkomt met onderstaande sleutel om te verifiëren dat de sessie vertrouwd kan worden:",
"To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:": "Neem contact op met de eigenaar op een andere manier (bv. onder vier ogen of met een telefoongesprek) en vraag of de sleutel in zijn/haar gebruikersinstellingen overeenkomt met onderstaande sleutel om te verifiëren dat de sessie vertrouwd kan worden:",
@ -2209,5 +2209,14 @@
"Verify the new login accessing your account: %(name)s": "Verifieer de nieuwe aanmelding op uw account: %(name)s",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Bevestig uw intentie deze account te sluiten door met Single Sign On uw identiteit te bewijzen.",
"Are you sure you want to deactivate your account? This is irreversible.": "Weet u zeker dat u uw account wil sluiten? Dit is onomkeerbaar.",
"Confirm account deactivation": "Bevestig accountsluiting"
"Confirm account deactivation": "Bevestig accountsluiting",
"Room name or address": "Gespreksnaam of -adres",
"Joins room with given address": "Treedt tot het gesprek met het opgegeven adres toe",
"Unrecognised room address:": "Gespreksadres niet herkend:",
"Help us improve Riot": "Help ons Riot nog beter te maken",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Stuur <UsageDataLink>anonieme gebruiksinformatie</UsageDataLink> waarmee we Riot kunnen verbeteren. Dit plaatst een <PolicyLink>cookie</PolicyLink>.",
"I want to help": "Ik wil helpen",
"Your homeserver has exceeded its user limit.": "Uw thuisserver heeft het maximaal aantal gebruikers overschreden.",
"Your homeserver has exceeded one of its resource limits.": "Uw thuisserver heeft een van zijn limieten overschreden.",
"Ok": "Oké"
}

View file

@ -13,7 +13,7 @@
"Answer": "Svar",
"You are already in a call.": "Du er allereie i ei samtale.",
"VoIP is unsupported": "VoIP er ikkje støtta",
"You cannot place VoIP calls in this browser.": "Du kan ikkje utføre med anrop med VoIP i denne nettlesaren.",
"You cannot place VoIP calls in this browser.": "Du kan ikkje utføra samtalar med VoIP i denne nettlesaren.",
"You cannot place a call with yourself.": "Du kan ikkje samtala med deg sjølv.",
"Could not connect to the integration server": "Kunne ikkje kopla til integreringstenaren",
"Call in Progress": "Ei Samtale er i Gang",
@ -167,7 +167,7 @@
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s-widget lagt til av %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget fjerna av %(senderName)s",
"Failure to create room": "Klarte ikkje å laga rommet",
"Server may be unavailable, overloaded, or you hit a bug.": "Serveren er kanskje utilgjengeleg, overlasta elles så traff du ein bug.",
"Server may be unavailable, overloaded, or you hit a bug.": "Tenaren er kanskje utilgjengeleg, overlasta elles så traff du ein bug.",
"Send anyway": "Send likevel",
"Send": "Send",
"Unnamed Room": "Rom utan namn",
@ -305,7 +305,7 @@
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (tilgangsnivå %(powerLevelNumber)s)",
"Attachment": "Vedlegg",
"Hangup": "Legg på",
"Voice call": "Taleanrop",
"Voice call": "Talesamtale",
"Video call": "Videosamtale",
"Upload file": "Last opp fil",
"Send an encrypted reply…": "Send eit kryptert svar…",
@ -762,7 +762,7 @@
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Send alle på nytt</resendText> eller <cancelText>avbryt alle</cancelText>. Du kan og markere enkelte meldingar for å sende på nytt eller avbryte.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Send melding på nytt</resendText> eller <cancelText>avbryt</cancelText>.",
"Connectivity to the server has been lost.": "Tilkoplinga til tenaren vart tapt.",
"Sent messages will be stored until your connection has returned.": "Sendte meldingar vil blir lagra lokalt fram til nettverket er oppe igjen.",
"Sent messages will be stored until your connection has returned.": "Sende meldingar vil lagrast lokalt fram til nettverket er oppe att.",
"Active call": "Pågåande samtale",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "Det er ingen andre her! Vil du <inviteText>invitera andre</inviteText> eller <nowarnText>skru av varselet om det tomme rommet?</nowarnText>?",
"You seem to be uploading files, are you sure you want to quit?": "Det ser ut til at du lastar opp filer, er du sikker på at du vil avslutte?",
@ -825,8 +825,8 @@
"Incorrect username and/or password.": "Feil brukarnamn og/eller passord.",
"Please note you are logging into the %(hs)s server, not matrix.org.": "Merk deg at du loggar inn på %(hs)s-tenaren, ikkje matrix.org.",
"The phone number entered looks invalid": "Det innskrivne telefonnummeret virkar å vere ugyldig",
"Error: Problem communicating with the given homeserver.": "Feil: Det gjekk ikkje an å kommunisere med den spesifiserte heimeserveren.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Kan ikkje koble til heimeserveren via HTTP fordi URL-adressa i nettlesaren er HTTPS. Bruk HTTPS, eller <a>aktiver usikre skript</a>.",
"Error: Problem communicating with the given homeserver.": "Feil: Det gjekk ikkje an å kommunisera med den spesifiserte heimetenaren.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Kan ikkje kobla til heimetenaren via HTTP fordi URL-adressa i nettlesaren er HTTPS. Bruk HTTPS, eller <a>aktiver usikre skript</a>.",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Kan ikkje kopla til heimtenaren - ver venleg og sjekk tilkoplinga di, og sjå til at <a>heimtenaren din sitt CCL-sertifikat</a> er stolt på og at ein nettlesartillegg ikkje hindrar førespurnader.",
"Failed to fetch avatar URL": "Klarte ikkje å henta avatar-URLen",
"Set a display name:": "Set eit visningsnamn:",
@ -894,7 +894,7 @@
"Clear filter": "Tøm filter",
"Profile": "Brukar",
"Access Token:": "Tilgangs-token:",
"This homeserver doesn't offer any login flows which are supported by this client.": "Heimeserveren tilbyr ingen påloggingsmetodar som er støtta av denne klienten.",
"This homeserver doesn't offer any login flows which are supported by this client.": "Heimetenaren tilbyr ingen innloggingsmetodar som er støtta av denne klienten.",
"Claimed Ed25519 fingerprint key": "Gjorde krav på Ed25519-fingeravtrykksnøkkel",
"Export room keys": "Eksporter romnøklar",
"Export": "Eksporter",
@ -915,11 +915,11 @@
"Whether or not you're logged in (we don't record your username)": "Uansett om du er innlogga eller ikkje (så lagrar vi ikkje brukarnamnet ditt)",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Fila %(fileName)s er større enn heimetenaren si grense for opplastningar",
"Unable to load! Check your network connectivity and try again.": "Klarte ikkje lasta! Sjå på nettilkoplinga di og prøv igjen.",
"Your Riot is misconfigured": "Riot-klienten din er feilkonfiguert",
"Your Riot is misconfigured": "Riot-klienten din er sett opp feil",
"Sign In": "Logg inn",
"Explore rooms": "Utforsk romma",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Meldingen din vart ikkje sent for denne heimeserveren har nådd grensa for maksimalt aktive brukarar pr. månad. Kontakt <a>systemadministratoren</a> for å kunne vidare nytte denne tenesten.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Denne meldingen vart ikkje sendt fordi heimeserveren har nådd grensa for tilgjengelege systemressursar. Kontakt <a>systemadministratoren</a> for å vidare nytte denne tenesten.",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Meldinga di vart ikkje send, for denne heimetenaren har nådd grensa for maksimalt aktive brukarar pr. månad. Kontakt <a>systemadministratoren</a> for å vidare nytte denne tenesta.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Denne meldingen vart ikkje send fordi heimetenaren har nådd grensa for tilgjengelege systemressursar. Kontakt <a>systemadministratoren</a> for å vidare nytta denne tenesta.",
"Add room": "Legg til rom",
"You have %(count)s unread notifications in a prior version of this room.|other": "Du har %(count)s uleste varslingar i ein tidligare versjon av dette rommet.",
"You have %(count)s unread notifications in a prior version of this room.|one": "Du har %(count)s ulest varsel i ein tidligare versjon av dette rommet.",
@ -928,26 +928,26 @@
"Could not load user profile": "Klarde ikkje å laste brukarprofilen",
"Your Matrix account on %(serverName)s": "Din Matrix-konto på %(serverName)s",
"Your Matrix account on <underlinedServerName />": "Din Matrix-konto på <underlinedServerName />",
"No identity server is configured: add one in server settings to reset your password.": "Ingen identitetsserver er konfigurert: legg til ein i innstillingane for å nullstille passordet ditt.",
"No identity server is configured: add one in server settings to reset your password.": "Ingen identitetstenar er satt opp: legg til ein i innstillingane for å nullstille passordet ditt.",
"Sign in instead": "Logg inn istaden",
"A verification email will be sent to your inbox to confirm setting your new password.": "For å stadfeste tilbakestilling av passordet, vil ein e-post vil bli sendt til din innboks.",
"Your password has been reset.": "Passodet ditt vart nullstilt.",
"Set a new password": "Sett nytt passord",
"Invalid homeserver discovery response": "Ugyldig svar frå heimeserveren (discovery response)",
"Failed to get autodiscovery configuration from server": "Kladte ikkje å hente automatisk oppsett frå server",
"Invalid base_url for m.homeserver": "Ugyldig base_url for m.homeserver",
"Homeserver URL does not appear to be a valid Matrix homeserver": "URL-adressa virkar ikkje til å vere ein gyldig Matrix-heimeserver",
"Invalid identity server discovery response": "Ugyldig svar frå identitetsserveren (discovery response)",
"Invalid base_url for m.identity_server": "Ugyldig base_url for m.identity_server",
"Identity server URL does not appear to be a valid identity server": "URL-adressa virkar ikkje til å vere ein gyldig identitetsserver",
"Invalid homeserver discovery response": "Feil svar frå heimetenaren (discovery response)",
"Failed to get autodiscovery configuration from server": "Klarde ikkje å hente automatisk oppsett frå tenaren",
"Invalid base_url for m.homeserver": "Feil base_url for m.homeserver",
"Homeserver URL does not appear to be a valid Matrix homeserver": "URL-adressa virkar ikkje til å vere ein gyldig Matrix-heimetenar",
"Invalid identity server discovery response": "Feil svar frå identitetstenaren (discovery response)",
"Invalid base_url for m.identity_server": "Feil base_url for m.identity_server",
"Identity server URL does not appear to be a valid identity server": "URL-adressa virkar ikkje til å vere ein gyldig identitetstenar",
"General failure": "Generell feil",
"This homeserver does not support login using email address.": "Denne heimeserveren støttar ikkje innloggingar med e-postadresser.",
"This homeserver does not support login using email address.": "Denne heimetenaren støttar ikkje innloggingar med e-postadresser.",
"Please <a>contact your service administrator</a> to continue using this service.": "<a>Kontakt din systemadministrator</a> for å vidare å bruke tenesta.",
"This account has been deactivated.": "Denne kontoen har blitt deaktivert.",
"Failed to perform homeserver discovery": "Fekk ikkje til å utforske heimeserveren",
"Failed to perform homeserver discovery": "Fekk ikkje til å utforska heimetenaren",
"Sign in with single sign-on": "Logg på med Single-Sign-On",
"Create account": "Lag konto",
"Registration has been disabled on this homeserver.": "Registrering er deaktivert på denne heimeserveren.",
"Registration has been disabled on this homeserver.": "Registrering er skrudd av for denne heimetenaren.",
"Unable to query for supported registration methods.": "Klarte ikkje å spørre etter støtta registreringsmetodar.",
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Kontoen din %(newAccountId)s er no registrert, men du er frå tidligare logga på med ein annan konto (%(loggedInUserId)s).",
"Continue with previous account": "Fortsett med tidligare konto",
@ -955,7 +955,7 @@
"You can now close this window or <a>log in</a> to your new account.": "Du kan lukke dette vindauget eller<a>logge inn</a> med din nye konto.",
"Registration Successful": "Registrering fullført",
"Create your account": "Lag din konto",
"Failed to re-authenticate due to a homeserver problem": "Fekk ikkje til å re-authentisere grunna ein feil på heimeserveren",
"Failed to re-authenticate due to a homeserver problem": "Fekk ikkje til å re-authentisere grunna ein feil på heimetenaren",
"Failed to re-authenticate": "Fekk ikkje til å re-autentisere",
"Enter your password to sign in and regain access to your account.": "Skriv inn ditt passord for å logge på og ta tilbake tilgang til kontoen din.",
"Forgotten your password?": "Gløymt passord ?",
@ -1007,14 +1007,14 @@
"Add Email Address": "Legg til e-postadresse",
"Add Phone Number": "Legg til telefonnummer",
"Call failed due to misconfigured server": "Kallet gjekk gale fordi tenaren er oppsatt feil",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Spør administratoren for din heimetenar<code>%(homeserverDomain)s</code> om å setje opp ein \"TURN-server\" slik at heimetenaren svarar korrekt.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Spør administratoren for din heimetenar<code>%(homeserverDomain)s</code> om å setje opp ein \"TURN-server\" slik at talesamtalar fungerer på rett måte.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativt, kan du prøva å nytta den offentlege tenaren på <code>turn.matrix.org</code>, men det kan vera mindre stabilt og IP-adressa di vil bli delt med den tenaren. Du kan og endra på det under Innstillingar.",
"Try using turn.matrix.org": "Prøv med å nytta turn.matrix.org",
"A conference call could not be started because the integrations server is not available": "Ein konferansesamtale kunne ikkje starta fordi integrasjons-tenaren er utilgjengeleg",
"Replying With Files": "Send svar med filer",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nett no er det ikkje mogleg å senda svar med ei fil. Vil du lasta opp denne fila utan å senda svaret?",
"The file '%(fileName)s' failed to upload.": "Fila '%(fileName)s' vart ikkje lasta opp.",
"The server does not support the room version specified.": "Serveren støttar ikkje den spesifikke versjonen av rommet.",
"The server does not support the room version specified.": "Tenaren støttar ikkje den spesifikke versjonen av rommet.",
"Name or Matrix ID": "Namn eller Matrix ID",
"Registration Required": "Registrering er obligatorisk",
"You need to register to do this. Would you like to register now?": "Du må registrera for å gjera dette. Ynskjer du å registrera no?",
@ -1062,13 +1062,13 @@
"Encryption upgrade available": "Kryptering kan oppgraderast",
"Set up encryption": "Sett opp kryptering",
"Unverified session": "Uverifisert sesjon",
"Identity server has no terms of service": "Identitetsserveren manglar bruksvilkår",
"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 handlinga krev kommunikasjon mot <server />(standard identitetsserver) for å verifisere e-post eller telefonnummer, men serveren manglar bruksvilkår.",
"Only continue if you trust the owner of the server.": "Gå vidare så lenge du har tillit til eigar av serveren.",
"Identity server has no terms of service": "Identitetstenaren manglar bruksvilkår",
"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 handlinga krev kommunikasjon mot <server />(standard identitetstenar) for å verifisere e-post eller telefonnummer, men tenaren manglar bruksvilkår.",
"Only continue if you trust the owner of the server.": "Gå vidare så lenge du har tillit til eigar av tenaren.",
"Trust": "Tillat",
"Custom (%(level)s)": "Tilpassa (%(level)s)",
"Error upgrading room": "Feil ved oppgradering av rom",
"Double check that your server supports the room version chosen and try again.": "Sjekk at server støttar romversjon, og prøv på nytt.",
"Double check that your server supports the room version chosen and try again.": "Sjekk at tenar støttar denne romversjonen, og prøv på nytt.",
"Verifies a user, session, and pubkey tuple": "Verifiser brukar, økt eller public-key objekt (pubkey tuple)",
"Unknown (user, session) pair:": "Ukjent (brukar,økt) par:",
"Session already verified!": "Sesjon er tidligare verifisert!",
@ -1094,13 +1094,13 @@
"%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s la til %(addedAddresses)s og tok vekk %(removedAddresses)s som adresser for dette rommet.",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s satte standardadressa for dette rommet til %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s fjerna standardadressa for dette rommet.",
"%(senderName)s placed a voice call.": "%(senderName)s starta eit taleanrop.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s starta eit taleanrop. (ikkje støtta av denne nettlesaren)",
"%(senderName)s placed a voice call.": "%(senderName)s starta ein talesamtale.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s starta ein talesamtale. (ikkje støtta av denne nettlesaren)",
"%(senderName)s placed a video call.": "%(senderName)s starta ein videosamtale.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s starta ein videosamtale. (ikkje støtta av denne nettlesaren)",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s trekte tilbake invitasjonen for at %(targetDisplayName)s kan bli medlem i rommet.",
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du er administrator for dette fellesskapet. Du kan ikkje melde deg inn igjen utan at du har invitasjon frå ein annan administrator.",
"Want more than a community? <a>Get your own server</a>": "Treng du meir enn eit fellesskap? <a>Skaff din eigen server</a>",
"Want more than a community? <a>Get your own server</a>": "Treng du meir enn eit fellesskap? <a>Skaff din eigen tenar</a>",
"Sign In or Create Account": "Logg inn eller opprett konto",
"Create Account": "Opprett konto",
"Sends the given emote coloured as a rainbow": "Sendar emojien med regnbogefargar",
@ -1183,7 +1183,7 @@
"You're previewing %(roomName)s. Want to join it?": "Du førehandsviser %(roomName)s. Ynskjer du å bli med ?",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s kan ikkje førehandsvisast. Ynskjer du å bli med ?",
"This room doesn't exist. Are you sure you're at the right place?": "Dette rommet eksisterar ikkje. Er du sikker på at du er på rett plass?",
"Try again later, or ask a room admin to check if you have access.": "Prøv igjen seinare, eller spør ein rom-administrator om du har tilgang.",
"Try again later, or ask a room admin to check if you have access.": "Prøv om att seinare, eller spør ein rom-administrator om du har tilgang.",
"Never lose encrypted messages": "Aldri la krypterte meldingar gå tapt",
"Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Meldingane i rommet er sikra med ende-til-ende kryptering. Berre du og mottakarane har krypteringsnøklane for desse meldingane.",
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Kopier nøklane dine for å unngå i miste dei. <a>Les meir.</a>",
@ -1197,13 +1197,13 @@
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Start meldinga med <code>//</code> for å starte den med skråstrek.",
"Send as message": "Send som melding",
"Failed to revoke invite": "Fekk ikkje til å trekke invitasjonen",
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Fekk ikkje til å trekke invitasjonen. Det kan ha oppstått eit midlertidig problem på serveren, eller så har ikkje du tilstrekkelege rettigheiter for å trekke invitasjonen.",
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Fekk ikkje til å ta attende invitasjonen. Det kan ha oppstått ein mellombels feil på tenaren, eller så har ikkje du tilstrekkelege rettar for å ta attende invitasjonen.",
"Revoke invite": "Trekk invitasjon",
"Invited by %(sender)s": "Invitert av %(sender)s",
"Mark all as read": "Merk alle som lesne",
"Error updating main address": "Feil under oppdatering av hovedadresse",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Det skjedde ein feil under oppdatering av hovedadressa for rommet. Det kan hende at dette er midlertidig, eller at det ikkje er tillate på serveren.",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Feil under oppdatering av sekundæradresse. Det kan hende at dette er midlertidig, eller at det ikkje er tillate på serveren.",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Det skjedde ein feil under oppdatering av hovudadressa for rommet. Det kan hende at dette er ein mellombels feil, eller at det ikkje er tillate på tenaren.",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Feil under oppdatering av alternativ adresse. Det kan hende at dette er mellombels, eller at det ikkje er tillate på tenaren.",
"Error creating alias": "Feil under oppretting av alias",
"There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.": "Det skjedde ein feil under oppretting av dette aliaset. Det kan hende at dette er midlertidig, eller at det ikkje er tillate på serveren.",
"You don't have permission to delete the alias.": "Du har ikkje lov å slette aliaset.",
@ -1212,14 +1212,14 @@
"Main address": "Hovudadresse",
"Local address": "Lokal adresse",
"Published Addresses": "Publisert adresse",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Publiserte adresser kan bli brukt av alle uansett server for å bli med i rommet. For å publisere ei adresse, må den vere sett som ei lokal adresse fyrst.",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Publiserte adresser kan bli brukt av alle uansett tenar for å bli med i rommet. For å publisera ei adresse, må den vere sett som ei lokal adresse fyrst.",
"Other published addresses:": "Andre publiserte adresser:",
"No other published addresses yet, add one below": "Ingen publiserte adresser til no, legg til ei under",
"New published address (e.g. #alias:server)": "Ny publisert adresse (t.d. #alias:server)",
"New published address (e.g. #alias:server)": "Ny publisert adresse (t.d. #alias:tenar)",
"Local Addresses": "Lokale adresser",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Sett adresse for dette rommet, slik at brukarar kan finne rommet på din heimeserver (%(localDomain)s)",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Sett adresse for dette rommet, slik at brukarar kan finne rommet på din heimetenar (%(localDomain)s)",
"Error updating flair": "Oppdatering av etikett gjekk gale",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Feil under oppdatering av etikett for dette rommet. Dette kan vere deaktivert på server , eller så oppstod det ein feil.",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Feil under oppdatering av etikett for dette rommet. Dette kan vere deaktivert på tenaren, eller så oppstod det ein feil.",
"Room Name": "Romnamn",
"Room Topic": "Romemne",
"Room avatar": "Rom-avatar",
@ -1231,7 +1231,7 @@
"Match system theme": "Følg systemtema",
"Show shortcuts to recently viewed rooms above the room list": "Vis snarvegar til sist synte rom over romkatalogen",
"Show hidden events in timeline": "Vis skjulte hendelsar i historikken",
"This is your list of users/servers you have blocked - don't leave the room!": "Dette er di liste over brukarar/serverar du har blokkert - ikkje forlat rommet!",
"This is your list of users/servers you have blocked - don't leave the room!": "Dette er di liste over brukarar/tenarar du har blokkert - ikkje forlat rommet!",
"Upload": "Last opp",
"Show less": "Vis mindre",
"Show more": "Vis meir",
@ -1270,7 +1270,7 @@
"Select the roles required to change various parts of the room": "Juster roller som er påkrevd for å endre ulike deler av rommet",
"Once enabled, encryption cannot be disabled.": "Etter aktivering, kan ikkje kryptering bli deaktivert.",
"Your display name": "Ditt visningsnamn",
"Can't find this server or its room list": "Klarde ikkje å finne serveren eller romkatalogen til den",
"Can't find this server or its room list": "Klarde ikkje å finna tenaren eller romkatalogen til den",
"Upload completed": "Opplasting fullført",
"Cancelled signature upload": "Kansellerte opplasting av signatur",
"Unabled to upload": "Klarte ikkje å laste opp",
@ -1290,7 +1290,7 @@
"Enable Emoji suggestions while typing": "Aktiver Emoji-forslag under skriving",
"Show a placeholder for removed messages": "Vis ein plassholdar for sletta meldingar",
"Show avatar changes": "Vis avatar-endringar",
"Show read receipts sent by other users": "Vis lest-rapportar sendt av andre brukarar",
"Show read receipts sent by other users": "Vis lese-rapportar sendt av andre brukarar",
"Enable big emoji in chat": "Aktiver store emolji-ar i samtalen",
"Send typing notifications": "Kringkast \"skriv...\"-status til andre",
"Show typing notifications": "Vis \"skriv...\"-status frå andre",
@ -1321,7 +1321,7 @@
"Jump to start/end of the composer": "Hopp til start/slutt av teksteditoren",
"Navigate composer history": "Naviger historikk for teksteditor",
"Use Single Sign On to continue": "Bruk Single-sign-on for å fortsette",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Stadfest at du legger til denne e-postadressa, ved å bruke Single-sign-on for å bevise identiteten din.",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Stadfest at du legger til denne e-postadressa, ved å bruke Single-sign-on for å stadfeste identiteten din.",
"Single Sign On": "Single-sign-on",
"Review Sessions": "Sjå gjennom økter",
"Capitalization doesn't help very much": "Store bokstavar hjelp dessverre lite",
@ -1330,7 +1330,7 @@
"Keep secret storage passphrase in memory for this session": "Hald passfrase for hemmeleg lager i systemminnet for denne økta",
"Cross-signing and secret storage are enabled.": "Krysssignering og hemmeleg lager er aktivert.",
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Kontoen din har ein kryss-signert identitet det hemmelege lageret, økta di stolar ikkje på denne enno.",
"Cross-signing and secret storage are not yet set up.": "Krysssignering og hemmeleg lager er endå ikkje konfiguert.",
"Cross-signing and secret storage are not yet set up.": "Krysssignering og hemmeleg lager er endå ikkje sett opp.",
"Reset cross-signing and secret storage": "Tilbakestill krysssignering og hemmeleg lager",
"Bootstrap cross-signing and secret storage": "Førebur krysssignering og hemmeleg lager",
"in secret storage": "i hemmeleg lager",
@ -1338,10 +1338,10 @@
"Secret Storage key format:": "Nøkkel-format for hemmeleg lager:",
"Keyboard Shortcuts": "Tastatursnarvegar",
"Ignored users": "Ignorerte brukarar",
"Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Legg til brukarar og serverar du vil ignorera her. Bruk stjerne/wildcard (*) for at markere eitkvart teikn. Til dømes, <code>@bot*</code> vil ignorere alle brukarar med namn 'bot' på uansett server.",
"Server or user ID to ignore": "Server eller brukar-ID for å ignorere",
"Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Legg til brukarar og tenarar du vil ignorera her. Bruk stjerne/wildcard (*) for at markere eitkvart teikn. Til dømes, <code>@bot*</code> vil ignorere alle brukarar med namn 'bot' på uansett tenar.",
"Server or user ID to ignore": "Tenar eller brukar-ID for å ignorere",
"If this isn't what you want, please use a different tool to ignore users.": "Om det ikkje var dette du ville, bruk eit anna verktøy til å ignorera brukarar.",
"Enter the name of a new server you want to explore.": "Skriv inn namn på ny server du ynskjer å utforske.",
"Enter the name of a new server you want to explore.": "Skriv inn namn på ny tenar du ynskjer å utforske.",
"Matrix rooms": "Matrix-rom",
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Om du har meir info rundt korleis problemet oppstod, som kva du prøvde å gjere på det tidspunktet, brukar-IDar m.m ,inkluder gjerne den informasjonen her.",
"Topic (optional)": "Emne (valfritt)",
@ -1363,8 +1363,8 @@
"Help": "Hjelp",
"Explore": "Utforsk",
"%(creator)s created and configured the room.": "%(creator)s oppretta og konfiguerte dette rommet.",
"Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot klarde ikkje å hente protokolllister frå heimeserveren. Det kan hende at serveren er for gammal til å støtte tredjeparts-nettverk",
"The homeserver may be unavailable or overloaded.": "Heimeserveren kan vere overlasta eller utilgjengeleg.",
"Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot klarde ikkje å hente protokolllister frå heimetenaren. Det kan hende at tenaren er for gammal til å støtte tredjeparts-nettverk",
"The homeserver may be unavailable or overloaded.": "Heimetenaren kan vere overlasta eller utilgjengeleg.",
"Preview": "Førehandsvis",
"View": "Vis",
"Find a room…": "Finn eit rom…",
@ -1382,5 +1382,81 @@
"Unable to set up secret storage": "Oppsett av hemmeleg lager feila",
"This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Denne økta har oppdaga at gjenopprettingspassfrasen og nøkkelen for sikre meldingar vart fjerna.",
"Toggle microphone mute": "Slå av/på demping av mikrofon",
"Confirm": "Stadfest"
"Confirm": "Stadfest",
"Confirm adding email": "Stadfest at du ynskjer å legga til e-postadressa",
"Click the button below to confirm adding this email address.": "Trykk på knappen under for å stadfesta at du ynskjer å legga til denne e-postadressa.",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Stadfest dette telefonnummeret for å bruka Single-sign-on for å bevisa din identitet.",
"Confirm adding phone number": "Stadfest dette telefonnummeret",
"Click the button below to confirm adding this phone number.": "Trykk på knappen nedanfor for å legge til dette telefonnummeret.",
"Room name or address": "Romnamn eller adresse",
"%(name)s is requesting verification": "%(name)s spør etter verifikasjon",
"Use your account or create a new one to continue.": "Bruk kontoen din eller opprett ein ny for å halda fram.",
"Sends a message as html, without interpreting it as markdown": "Sender melding som HTML, utan å tolka den som markdown",
"Failed to set topic": "Fekk ikkje til å setta emne",
"Joins room with given address": "Legg saman rom med spesifisert adresse",
"Unrecognised room address:": "Rom-adressa vart ikkje kjend att:",
"Command failed": "Kommandoen feila",
"Could not find user in room": "Klarde ikkje å finna brukaren i rommet",
"Please supply a widget URL or embed code": "Oppgje ein widget-URL eller innebygd kode",
"Displays information about a user": "Viser informasjon om ein brukar",
"Send a bug report with logs": "Send ein feilrapport med loggar",
"Opens chat with the given user": "Opna ein samtale med den spesifiserte brukaren",
"Sends a message to the given user": "Send ein melding til den spesifiserte brukaren",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s endra romnamnet frå %(oldRoomName)s til %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s la til dei alternative adressene %(addresses)s for dette rommet.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s la til ei alternativ adresse %(addresses)s for dette rommet.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s tok vekk dei alternative adressene %(addresses)s for dette rommet.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s tok vekk den alternative adressa %(addresses)s for dette rommet.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s endre den alternative adressa for dette rommet.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s endra hovud- og alternativ-adressene for dette rommet.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s endre adressene for dette rommet.",
"Review where youre logged in": "Sjå over kvar du er logga inn",
"Later": "Seinare",
"Allow Peer-to-Peer for 1:1 calls": "Tillat peer-to-peer (P2P) for ein-til-ein samtalar",
"Never send encrypted messages to unverified sessions from this session": "Aldri send krypterte meldingar til ikkje-verifiserte sesjonar frå denne sesjonen",
"Never send encrypted messages to unverified sessions in this room from this session": "Aldri send krypterte meldingar i dette rommet til ikkje-verifiserte sesjonar frå denne sesjonen",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Tillat å bruke assistansetenaren turn.matrix.org for talesamtalar viss heimetenaren din ikkje tilbyr dette (IP-adressa di vil bli delt under talesamtalen)",
"Enable message search in encrypted rooms": "Aktiver søk etter meldingar i krypterte rom",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sikre meldingar med denne brukaren er ende-til-ende krypterte og kan ikkje lesast av tredjepart.",
"Public Name": "Offentleg namn",
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Er du sikker? Alle dine krypterte meldingar vil gå tapt viss nøklane dine ikkje er sikkerheitskopierte.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Krypterte meldingar er sikra med ende-til-ende kryptering. Berre du og mottakar(ane) har nøklane for å lese desse meldingane.",
"wait and try again later": "vent og prøv om att seinare",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "For å rapportere eit Matrix-relatert sikkerheitsproblem, les Matrix.org sin <a>Security Disclosure Policy</a>.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Tenaradministratoren din har deaktivert ende-til-ende kryptering som standard i direktemeldingar og private rom.",
"Where youre logged in": "Stader du er innlogga",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Handter namn på eller logg ut av sesjonane dine nedanfor eller <a>verifiser dei under brukarprofilen din</a>.",
"A session's public name is visible to people you communicate with": "Ein sesjon sitt offentlege namn, er synleg for andre du kommuniserar med",
"Reset": "Nullstill",
"Set a new custom sound": "Set ein ny tilpassa lyd",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Når kryptering er aktivert for eit rom, kan ein ikkje deaktivere det. Meldingar som blir sende i eit kryptert rom kan ikkje bli lesne av tenaren, men berre av deltakarane i rommet. Aktivering av kryptering kan hindre mange botar og bruer frå å fungera på rett måte. <a>Les meir om kryptering her.</a>",
"Encrypted": "Kryptert",
"This room is end-to-end encrypted": "Dette rommet er ende-til-ende kryptert",
"Encrypted by an unverified session": "Kryptert av ein ikkje-verifisert sesjon",
"Encrypted by a deleted session": "Kryptert av ein sletta sesjon",
"Create room": "Lag rom",
"Messages in this room are end-to-end encrypted.": "Meldingar i dette rommet er ende-til-ende kryptert.",
"Messages in this room are not end-to-end encrypted.": "Meldingar i dette rommet er ikkje ende-til-ende kryptert.",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Når du nyttar krypterte rom er meldingane din sikra. Berre du og mottakaren har unike nøklar som kan gjere meldingane lesbare.",
"This client does not support end-to-end encryption.": "Denne klienten støttar ikkje ende-til-ende kryptering.",
"In encrypted rooms, verify all users to ensure its secure.": "Når du nyttar krypterte rom, verifiser alle brukarar for å vere trygg på at det er sikkert.",
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Meldingar i dette rommet er ende-til-ende krypterte. Meir om dette, samt verifisering av denne brukaren finn du under deira brukarprofil.",
"Join": "Bli med",
"Remove server": "Ta vekk tenar",
"Matrix": "Matrix",
"Add a new server": "Legg til ein ny tenar",
"Add a new server...": "Legg til ein ny tenar",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Tømming av data frå denne sesjonen er permanent. Krypterte meldingar vil gå tapt med mindre krypteringsnøklane har blitt sikkerheitskopierte.",
"This room is private, and can only be joined by invitation.": "Dette rommet er privat, brukarar kan berre bli med viss dei har ein invitasjon",
"You cant disable this later. Bridges & most bots wont work yet.": "Du kan ikkje skru av dette seinare. Bruer og dei fleste botar vil ikkje fungere enno.",
"Enable end-to-end encryption": "Skru på ende-til-ende kryptering",
"Create a private room": "Lag eit privat rom",
"Make this room public": "Gjer dette rommet offentleg",
"Hide advanced": "Gøym avanserte alternativ",
"Show advanced": "Vis avanserte alternativ",
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Utesteng brukarar på andre Matrix heimetenarar frå å koma inn i rommet (Dette kan endrast seinare!)",
"I don't want my encrypted messages": "Eg treng ikkje mine krypterte meldingar",
"You'll lose access to your encrypted messages": "Du vil miste tilgangen til dine krypterte meldingar",
"No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Ingen identitetstenar er konfiguert, så i framtida kan ikkje legge til ei e-postadresse for å nullstille passordet.",
"Join millions for free on the largest public server": "Kom ihop med millionar av andre på den største offentlege tenaren"
}

View file

@ -1565,7 +1565,7 @@
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odstránil/a alternatívne adresy %(addresses)s pre túto miestnosť.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odstránil/a alternatívnu adresu %(addresses)s pre túto miestnosť.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s zmenil/a alternatívne adresy pre túto miestnosť.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s zmenil/a hlavnú a alternatívnu/e adresy pre túto miestnosť.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s zmenil/a hlavnú a alternatívne adresy pre túto miestnosť.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s zmenil/a adresy pre túto miestnosť.",
"You signed in to a new session without verifying it:": "Prihlásili ste sa do novej relácie bez jej overenia:",
"Verify your other session using one of the options below.": "Overte svoje ostatné relácie pomocou jednej z nižšie uvedených možností.",
@ -1708,7 +1708,7 @@
"Manage": "Spravovať",
"Securely cache encrypted messages locally for them to appear in search results.": "Bezpečne cachovať šifrované správy lokálne, aby sa mohli zobraziť vo vyhľadávaní.",
"Enable": "Povoliť",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Riotu chýbajú niektoré komponenty potrebné na bezpečné cachovanie šifrovaných správ lokálne. Pokiaľ chcete experimentovať s touto funkciou, spravte si svoj vlastný Riot Desktop <nativeLink>s pridanými vyhľadávacími komponentami</nativeLink>.",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Riotu chýbajú niektoré komponenty potrebné na bezpečné cachovanie šifrovaných správ lokálne. Pokiaľ chcete experimentovať s touto funkciou, spravte si svoj vlastný Riot Desktop s <nativeLink>pridanými vyhľadávacími komponentami</nativeLink>.",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riotu nemôže bezpečne cachovať šifrované správy lokálne keď beží v prehliadači. Použite <riotLink>Riot Desktop</riotLink>, aby sa šifrované správy zobrazili vo vyhľadávaní.",
"This session is backing up your keys. ": "Táto relácia zálohuje vaše kľúče. ",
"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.": "Táto relácia <b>nezálohuje vaše kľúče</b>, ale už máte jednu existujúcu zálohu z ktorej sa môžete obnoviť a postupne pridávať.",
@ -1727,5 +1727,17 @@
"Enable desktop notifications for this session": "Povoliť desktopové notifikácie pre túto reláciu",
"Enable audible notifications for this session": "Povoliť zvukové notifikácie pre túto reláciu",
"Size must be a number": "Veľkosť musí byť číslo",
"Custom font size can only be between %(min)s pt and %(max)s pt": "Vlastná veľkosť písma môže byť len v rozmedzí %(min)s pt až %(max)s pt"
"Custom font size can only be between %(min)s pt and %(max)s pt": "Vlastná veľkosť písma môže byť len v rozmedzí %(min)s pt až %(max)s pt",
"Help us improve Riot": "Pomôžte nám zlepšovať Riot",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Posielať <UsageDataLink>anonymné dáta o používaní</UsageDataLink>, ktoré nám pomôžu zlepšiť Riot. Toto bude vyžadovať <PolicyLink>sušienku</PolicyLink>.",
"I want to help": "Chcem pomôcť",
"Your homeserver has exceeded its user limit.": "Na vašom domovskom serveri bol prekročený limit počtu používateľov.",
"Your homeserver has exceeded one of its resource limits.": "Na vašom domovskom serveri bol prekročený jeden z limitov systémových zdrojov.",
"Contact your <a>server admin</a>.": "Kontaktujte svojho <a>administrátora serveru</a>.",
"Ok": "Ok",
"Set password": "Nastaviť heslo",
"To return to your account in future you need to set a password": "Aby ste sa k účtu mohli vrátiť aj neskôr, je potrebné nastaviť heslo",
"Restart": "Reštartovať",
"Upgrade your Riot": "Upgradujte svoj Riot",
"A new version of Riot is available!": "Nová verzia Riotu je dostupná!"
}

View file

@ -1382,7 +1382,7 @@
"Failed to revoke invite": "Su arrit të shfuqizohej ftesa",
"Revoke invite": "Shfuqizoje ftesën",
"Invited by %(sender)s": "Ftuar nga %(sender)s",
"edited": "përpunoi",
"edited": "e përpunuar",
"Maximize apps": "Maksimizoni aplikacione",
"Rotate Left": "Rrotulloje Majtas",
"Rotate counter-clockwise": "Rrotulloje në kah kundërorar",
@ -2470,5 +2470,31 @@
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Të fshihet adresa e dhomës %(alias)s dhe të hiqet %(name)s nga drejtoria?",
"delete the address.": "fshije adresën.",
"Use a different passphrase?": "Të përdoret një frazëkalim tjetër?",
"New version available. <a>Update now.</a>": "Version i ri gati. <a>Përditësojeni tani.</a>"
"New version available. <a>Update now.</a>": "Version i ri gati. <a>Përditësojeni tani.</a>",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Përgjegjësi i shërbyesit tuaj ka çaktivizuar fshehtëzimin skaj-më-skaj, si parazgjedhje, në dhoma private & Mesazhe të Drejtpërdrejtë.",
"Emoji picker": "Zgjedhës emoji-sh",
"People": "Persona",
"Show %(n)s more": "Shfaq %(n)s të tjerë",
"Recently Direct Messaged": "Me Mesazhe të Drejtpërdrejtë Së Fundi",
"Switch to light mode": "Kalo nën mënyrën e çelët",
"Switch to dark mode": "Kalo nën mënyrën e errët",
"Switch theme": "Ndërroni temën",
"Security & privacy": "Siguri & privatësi",
"All settings": "Krejt rregullimet",
"Archived rooms": "Dhoma të arkivuara",
"Feedback": "Mendime",
"Account settings": "Rregullime llogarie",
"sent an image.": "dërgoi një figurë.",
"You: %(message)s": "Ju: %(message)s",
"No recently visited rooms": "Ska dhoma të vizituara së fundi",
"Sort by": "Renditi sipas",
"Unread rooms": "Dhoma të palexuara",
"Always show first": "Shfaq përherë të parën",
"Show": "Shfaqe",
"Message preview": "Paraparje mesazhi",
"List options": "Mundësi liste",
"Show %(count)s more|other": "Shfaq %(count)s të tjera",
"Show %(count)s more|one": "Shfaq %(count)s tjetër",
"Leave Room": "Dil Nga Dhoma",
"Room options": "Mundësi dhome"
}

View file

@ -2480,5 +2480,32 @@
"Restart": "重新啟動",
"Upgrade your Riot": "升級您的 Riot",
"A new version of Riot is available!": "已有新版的 Riot",
"New version available. <a>Update now.</a>": "有可用的新版本。<a>立刻更新。</a>"
"New version available. <a>Update now.</a>": "有可用的新版本。<a>立刻更新。</a>",
"Emoji picker": "顏文字挑選器",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "您的伺服器管理員已在私人聊天室與直接訊息中預設停用端到端加密。",
"Show %(n)s more": "顯示另外 %(n)s 個",
"People": "夥伴",
"Switch to light mode": "切換至淺色模式",
"Switch to dark mode": "切換至深色模式",
"Switch theme": "切換佈景主題",
"Security & privacy": "安全性與隱私權",
"All settings": "所有設定",
"Archived rooms": "已封存的聊天室",
"Feedback": "回饋",
"Account settings": "帳號設定",
"No recently visited rooms": "沒有最近造訪過的聊天室",
"Sort by": "排序方式",
"Unread rooms": "未讀聊天室",
"Always show first": "一律先顯示",
"Show": "顯示",
"Message preview": "訊息預覽",
"List options": "列表選項",
"Show %(count)s more|other": "再顯示 %(count)s 個",
"Show %(count)s more|one": "再顯示 %(count)s 個",
"Leave Room": "離開聊天室",
"Room options": "聊天室選項",
"sent an image.": "傳送圖片。",
"You: %(message)s": "您:%(message)s",
"Activity": "活動",
"A-Z": "A-Z"
}

View file

@ -290,6 +290,33 @@ export default class EventIndex extends EventEmitter {
return validEventType && validMsgType && hasContentValue;
}
eventToJson(ev) {
const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
if (ev.isEncrypted()) {
// Let us store some additional data so we can re-verify the event.
// The js-sdk checks if an event is encrypted using the algorithm,
// the sender key and ed25519 signing key are used to find the
// correct device that sent the event which allows us to check the
// verification state of the event, either directly or using cross
// signing.
e.curve25519Key = ev.getSenderKey();
e.ed25519Key = ev.getClaimedEd25519Key();
e.algorithm = ev.getWireContent().algorithm;
e.forwardingCurve25519KeyChain = ev.getForwardingCurve25519KeyChain();
} else {
// Make sure that unencrypted events don't contain any of that data,
// despite what the server might give to us.
delete e.curve25519Key;
delete e.ed25519Key;
delete e.algorithm;
delete e.forwardingCurve25519KeyChain;
}
return e;
}
/**
* Queue up live events to be added to the event index.
*
@ -300,8 +327,7 @@ export default class EventIndex extends EventEmitter {
if (!this.isValidEvent(ev)) return;
const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
const e = this.eventToJson(ev);
const profile = {
displayname: ev.sender.rawDisplayName,
@ -477,8 +503,7 @@ export default class EventIndex extends EventEmitter {
// Let us convert the events back into a format that EventIndex can
// consume.
const events = filteredEvents.map((ev) => {
const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
const e = this.eventToJson(ev);
let profile = {};
if (e.sender in profiles) profile = profiles[e.sender];

View file

@ -145,6 +145,10 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
if (enabledLabs.length) {
body.append('enabled_labs', enabledLabs.join(', '));
}
// if low bandwidth mode is enabled, say so over rageshake, it causes many issues
if (SettingsStore.getValue("lowBandwidth")) {
body.append("lowBandwidth", "enabled");
}
// add storage persistence/quota information
if (navigator.storage && navigator.storage.persisted) {

View file

@ -170,10 +170,10 @@ export const SETTINGS = {
displayName: _td("Show info about bridges in room settings"),
default: false,
},
"fontSize": {
"baseFontSize": {
displayName: _td("Font size"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: 15,
default: 10,
controller: new FontSizeController(),
},
"useCustomFontSize": {

View file

@ -181,6 +181,8 @@ export default class SettingsStore {
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
*/
static monitorSetting(settingName, roomId) {
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
if (!this._monitors[settingName]) this._monitors[settingName] = {};
const registerWatcher = () => {

View file

@ -20,8 +20,10 @@ import IWatcher from "./Watcher";
import { toPx } from '../../utils/units';
export class FontWatcher implements IWatcher {
public static readonly MIN_SIZE = 13;
public static readonly MAX_SIZE = 20;
public static readonly MIN_SIZE = 8;
public static readonly MAX_SIZE = 15;
// Externally we tell the user the font is size 15. Internally we use 10.
public static readonly SIZE_DIFF = 5;
private dispatcherRef: string;
@ -30,7 +32,7 @@ export class FontWatcher implements IWatcher {
}
public start() {
this.setRootFontSize(SettingsStore.getValue("fontSize"));
this.setRootFontSize(SettingsStore.getValue("baseFontSize"));
this.dispatcherRef = dis.register(this.onAction);
}
@ -48,7 +50,7 @@ export class FontWatcher implements IWatcher {
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
if (fontSize !== size) {
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, fontSize);
}
(<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize);
};

View file

@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update";
* help prevent lock conflicts.
*/
export abstract class AsyncStore<T extends Object> extends EventEmitter {
private storeState: T = <T>{};
private storeState: T;
private lock = new AwaitLock();
private readonly dispatcherRef: string;
/**
* Creates a new AsyncStore using the given dispatcher.
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
* @param {T} initialState The initial state for the store.
*/
protected constructor(private dispatcher: Dispatcher<ActionPayload>) {
protected constructor(private dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
super();
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
this.storeState = initialState;
}
/**

View file

@ -0,0 +1,53 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
import { AsyncStore } from "./AsyncStore";
import { ActionPayload } from "../dispatcher/payloads";
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
protected matrixClient: MatrixClient;
protected abstract async onAction(payload: ActionPayload);
protected async onReady() {
// Default implementation is to do nothing.
}
protected async onNotReady() {
// Default implementation is to do nothing.
}
protected async onDispatch(payload: ActionPayload) {
await this.onAction(payload);
if (payload.action === 'MatrixActions.sync') {
// Filter out anything that isn't the first PREPARED sync.
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
return;
}
this.matrixClient = payload.matrixClient;
await this.onReady();
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
if (this.matrixClient) {
await this.onNotReady();
this.matrixClient = null;
}
}
}
}

View file

@ -0,0 +1,166 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore, { SettingLevel } from "../settings/SettingsStore";
import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
interface IState {
enabled?: boolean;
rooms?: Room[];
}
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new BreadcrumbsStore();
private waitingRooms: { roomId: string, addedTs: number }[] = [];
private constructor() {
super(defaultDispatcher);
SettingsStore.monitorSetting("breadcrumb_rooms", null);
SettingsStore.monitorSetting("breadcrumbs", null);
}
public static get instance(): BreadcrumbsStore {
return BreadcrumbsStore.internalInstance;
}
public get rooms(): Room[] {
return this.state.rooms || [];
}
public get visible(): boolean {
return this.state.enabled;
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'setting_updated') {
if (payload.settingName === 'breadcrumb_rooms') {
await this.updateRooms();
} else if (payload.settingName === 'breadcrumbs') {
await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
}
} else if (payload.action === 'view_room') {
if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately. We're probably just
// waiting for a room join to complete.
this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()});
} else {
// The tests might not result in a valid room object.
const room = this.matrixClient.getRoom(payload.room_id);
if (room) await this.appendRoom(room);
}
}
}
protected async onReady() {
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
await this.updateRooms();
await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
this.matrixClient.on("Room.myMembership", this.onMyMembership);
this.matrixClient.on("Room", this.onRoom);
}
protected async onNotReady() {
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
this.matrixClient.removeListener("Room.myMembership", this.onMyMembership);
this.matrixClient.removeListener("Room", this.onRoom);
}
private onMyMembership = async (room: Room) => {
// We turn on breadcrumbs by default once the user has at least 1 room to show.
if (!this.state.enabled) {
await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
}
};
private onRoom = async (room: Room) => {
const waitingRoom = this.waitingRooms.find(r => r.roomId === room.roomId);
if (!waitingRoom) return;
this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1);
if ((Date.now() - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
await this.appendRoom(room);
};
private async updateRooms() {
let roomIds = SettingsStore.getValue("breadcrumb_rooms");
if (!roomIds || roomIds.length === 0) roomIds = [];
const rooms = roomIds.map(r => this.matrixClient.getRoom(r)).filter(r => !!r);
const currentRooms = this.state.rooms || [];
if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo)
await this.updateState({rooms});
}
private async appendRoom(room: Room) {
const rooms = (this.state.rooms || []).slice(); // cheap clone
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = this.matrixClient.getRoomUpgradeHistory(room.roomId);
if (history.length > 1) {
room = history[history.length - 1]; // Last room is most recent in history
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1);
}
}
// Remove the existing room, if it is present
const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
// Splice the room to the start of the list
rooms.splice(0, 0, room);
if (rooms.length > MAX_ROOMS) {
// This looks weird, but it's saying to start at the MAX_ROOMS point in the
// list and delete everything after it.
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
// Update the breadcrumbs
await this.updateState({rooms});
const roomIds = rooms.map(r => r.roomId);
if (roomIds.length > 0) {
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
}

View file

@ -0,0 +1,135 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
import { textForEvent } from "../TextForEvent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../languageHandler";
const PREVIEWABLE_EVENTS = [
// This is the same list from RiotX
{type: "m.room.message", isState: false},
{type: "m.room.name", isState: true},
{type: "m.room.topic", isState: true},
{type: "m.room.member", isState: true},
{type: "m.room.history_visibility", isState: true},
{type: "m.call.invite", isState: false},
{type: "m.call.hangup", isState: false},
{type: "m.call.answer", isState: false},
{type: "m.room.encrypted", isState: false},
{type: "m.room.encryption", isState: true},
{type: "m.room.third_party_invite", isState: true},
{type: "m.sticker", isState: false},
{type: "m.room.create", isState: true},
];
// The maximum number of events we're willing to look back on to get a preview.
const MAX_EVENTS_BACKWARDS = 50;
interface IState {
[roomId: string]: string | null; // null indicates the preview is empty
}
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new MessagePreviewStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): MessagePreviewStore {
return MessagePreviewStore.internalInstance;
}
/**
* Gets the pre-translated preview for a given room
* @param room The room to get the preview for.
* @returns The preview, or null if none present.
*/
public getPreviewForRoom(room: Room): string {
if (!room) return null; // invalid room, just return nothing
// It's faster to do a lookup this way than it is to use Object.keys().includes()
// We only want to generate a preview if there's one actually missing and not explicitly
// set as 'none'.
const val = this.state[room.roomId];
if (val !== null && typeof(val) !== "string") {
this.generatePreview(room);
}
return this.state[room.roomId];
}
private generatePreview(room: Room) {
const timeline = room.getLiveTimeline();
if (!timeline) return; // usually only happens in tests
const events = timeline.getEvents();
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
const event = events[i];
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: preview.preview});
return; // break - we found some text
}
}
// if we didn't find anything, subscribe ourselves to an update
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: null});
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
await this.updateState({[event.getRoomId()]: preview.preview});
return; // break - we found some text
}
}
}
private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
const isSelf = event.getSender() === this.matrixClient.getUserId();
let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
if (!text || text.trim().length === 0) text = null; // force null if useless to us
if (text && isSelf) {
// XXX: i18n doesn't really work here if the language doesn't support prefixing.
// We'd ideally somehow route the `You:` bit to the textForEvent call, however
// threading that through is non-trivial.
text = _t("You: %(message)s", {message: text});
}
return {isPreviewable: true, preview: text};
}
return {isPreviewable: false, preview: null};
}
}

View file

@ -0,0 +1,105 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TagID } from "./models";
const TILE_HEIGHT_PX = 44;
interface ISerializedListLayout {
numTiles: number;
showPreviews: boolean;
}
export class ListLayout {
private _n = 0;
private _previews = false;
constructor(public readonly tagId: TagID) {
const serialized = localStorage.getItem(this.key);
if (serialized) {
// We don't use the setters as they cause writes.
const parsed = <ISerializedListLayout>JSON.parse(serialized);
this._n = parsed.numTiles;
this._previews = parsed.showPreviews;
}
}
public get showPreviews(): boolean {
return this._previews;
}
public set showPreviews(v: boolean) {
this._previews = v;
this.save();
}
public get tileHeight(): number {
return TILE_HEIGHT_PX;
}
private get key(): string {
return `mx_sublist_layout_${this.tagId}_boxed`;
}
public get visibleTiles(): number {
return Math.max(this._n, this.minVisibleTiles);
}
public set visibleTiles(v: number) {
this._n = v;
this.save();
}
public get minVisibleTiles(): number {
// the .65 comes from the CSS where the show more button is
// mathematically 65% of a tile when floating.
return 4.65;
}
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
// Only apply the padding if we're about to use maxTiles as we need to
// plan for the padding. If we're using n, the padding is already accounted
// for by the resizing stuff.
let padding = 0;
if (maxTiles < n) {
padding = possiblePadding;
}
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
}
public tilesToPixelsWithPadding(n: number, padding: number): number {
return this.tilesToPixels(n) + padding;
}
public tilesToPixels(n: number): number {
return n * this.tileHeight;
}
public pixelsToTiles(px: number): number {
return px / this.tileHeight;
}
private save() {
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
}
private serialize(): ISerializedListLayout {
return {
numTiles: this.visibleTiles,
showPreviews: this.showPreviews,
};
}
}

View file

@ -17,24 +17,21 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm";
import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import TagOrderStore from "../TagOrderStore";
import { AsyncStore } from "../AsyncStore";
import { Room } from "matrix-js-sdk/src/models/room";
import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
import { getListAlgorithmInstance } from "./algorithms/list-ordering";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
import { ActionPayload } from "../../dispatcher/payloads";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { IFilterCondition } from "./filters/IFilterCondition";
import { TagWatcher } from "./TagWatcher";
import RoomViewStore from "../RoomViewStore";
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
interface IState {
tagsEnabled?: boolean;
preferredSort?: SortAlgorithm;
preferredAlgorithm?: ListAlgorithm;
}
/**
@ -47,7 +44,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private _matrixClient: MatrixClient;
private initialListsGenerated = false;
private enabled = false;
private algorithm: Algorithm;
private algorithm = new Algorithm();
private filterConditions: IFilterCondition[] = [];
private tagWatcher = new TagWatcher(this);
@ -62,6 +59,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
this.checkEnabled();
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
RoomViewStore.addListener(this.onRVSUpdate);
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
}
public get orderedLists(): ITagMap {
@ -83,16 +82,29 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private async readAndCacheSettingsFromStore() {
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
await this.updateState({
tagsEnabled,
preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
});
this.setAlgorithmClass();
await this.updateAlgorithmInstances();
}
private onRVSUpdate = () => {
if (!this.enabled) return; // TODO: Remove enabled flag when RoomListStore2 takes over
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
const activeRoomId = RoomViewStore.getRoomId();
if (!activeRoomId && this.algorithm.stickyRoom) {
this.algorithm.stickyRoom = null;
} else if (activeRoomId) {
const activeRoom = this.matrixClient.getRoom(activeRoomId);
if (!activeRoom) throw new Error(`${activeRoomId} is current in RVS but missing from client`);
if (activeRoom !== this.algorithm.stickyRoom) {
console.log(`Changing sticky room to ${activeRoomId}`);
this.algorithm.stickyRoom = activeRoom;
}
}
};
protected async onDispatch(payload: ActionPayload) {
if (payload.action === 'MatrixActions.sync') {
// Filter out anything that isn't the first PREPARED sync.
@ -110,6 +122,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.log("Regenerating room lists: Startup");
await this.readAndCacheSettingsFromStore();
await this.regenerateAllLists();
this.onRVSUpdate(); // fake an RVS update to adjust sticky room, if needed
}
// TODO: Remove this once the RoomListStore becomes default
@ -145,13 +158,19 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
// First see if the receipt event is for our own user. If it was, trigger
// a room update (we probably read the room on a different device).
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
// TODO: Update room now that it's been read
console.log(payload);
console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`);
const room = this.matrixClient.getRoom(payload.event.roomId);
if (!room) {
console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`);
return;
}
await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
return;
}
} else if (payload.action === 'MatrixActions.Room.tags') {
// TODO: Update room from tags
console.log(payload);
const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
} else if (payload.action === 'MatrixActions.Room.timeline') {
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
@ -189,26 +208,39 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
// cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :(
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
// TODO: Update DMs
console.log(payload);
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
console.log(`[RoomListDebug] Received updated DM map`);
const dmMap = eventPayload.event.getContent();
for (const userId of Object.keys(dmMap)) {
const roomIds = dmMap[userId];
for (const roomId of roomIds) {
const room = this.matrixClient.getRoom(roomId);
if (!room) {
console.warn(`${roomId} was found in DMs but the room is not in the store`);
continue;
}
// We expect this RoomUpdateCause to no-op if there's no change, and we don't expect
// the user to have hundreds of rooms to update in one event. As such, we just hammer
// away at updates until the problem is solved. If we were expecting more than a couple
// of rooms to be updated at once, we would consider batching the rooms up.
await this.handleRoomUpdate(room, RoomUpdateCause.PossibleTagChange);
}
}
} else if (payload.action === 'MatrixActions.Room.myMembership') {
// TODO: Improve new room check
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
if (membershipPayload.oldMembership !== "join" && membershipPayload.membership === "join") {
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
return;
}
// TODO: Update room from membership change
console.log(payload);
} else if (payload.action === 'MatrixActions.Room') {
// TODO: Improve new room check
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
} else if (payload.action === 'view_room') {
// TODO: Update sticky room
console.log(payload);
// If it's not a join, it's transitioning into a different list (possibly historical)
if (membershipPayload.oldMembership !== membershipPayload.membership) {
console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
return;
}
}
}
@ -220,17 +252,57 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
}
private getSortAlgorithmFor(tagId: TagID): SortAlgorithm {
switch (tagId) {
case DefaultTagID.Invite:
case DefaultTagID.Untagged:
case DefaultTagID.Archived:
case DefaultTagID.LowPriority:
case DefaultTagID.DM:
return this.state.preferredSort;
case DefaultTagID.Favourite:
default:
return SortAlgorithm.Manual;
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
await this.algorithm.setTagSorting(tagId, sort);
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
}
public getTagSorting(tagId: TagID): SortAlgorithm {
return this.algorithm.getTagSorting(tagId);
}
// noinspection JSMethodCanBeStatic
private getStoredTagSorting(tagId: TagID): SortAlgorithm {
return <SortAlgorithm>localStorage.getItem(`mx_tagSort_${tagId}`);
}
public async setListOrder(tagId: TagID, order: ListAlgorithm) {
await this.algorithm.setListOrdering(tagId, order);
localStorage.setItem(`mx_listOrder_${tagId}`, order);
}
public getListOrder(tagId: TagID): ListAlgorithm {
return this.algorithm.getListOrdering(tagId);
}
// noinspection JSMethodCanBeStatic
private getStoredListOrder(tagId: TagID): ListAlgorithm {
return <ListAlgorithm>localStorage.getItem(`mx_listOrder_${tagId}`);
}
private async updateAlgorithmInstances() {
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
const defaultSort = orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent;
const defaultOrder = orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural;
for (const tag of Object.keys(this.orderedLists)) {
const definedSort = this.getTagSorting(tag);
const definedOrder = this.getListOrder(tag);
const storedSort = this.getStoredTagSorting(tag);
const storedOrder = this.getStoredListOrder(tag);
const tagSort = storedSort ? storedSort : (definedSort ? definedSort : defaultSort);
const listOrder = storedOrder ? storedOrder : (definedOrder ? definedOrder : defaultOrder);
if (tagSort !== definedSort) {
await this.setTagSorting(tag, tagSort);
}
if (listOrder !== definedOrder) {
await this.setListOrder(tag, listOrder);
}
}
}
@ -240,15 +312,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
await super.updateState(newState);
}
private setAlgorithmClass() {
if (this.algorithm) {
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
}
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
this.algorithm.setFilterConditions(this.filterConditions);
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
}
private onAlgorithmListUpdated = () => {
console.log("Underlying algorithm has triggered a list update - refiring");
this.emit(LISTS_UPDATE_EVENT, this);
@ -257,9 +320,11 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private async regenerateAllLists() {
console.warn("Regenerating all room lists");
const tags: ITagSortingMap = {};
const sorts: ITagSortingMap = {};
const orders: IListOrderingMap = {};
for (const tagId of OrderedDefaultTagIDs) {
tags[tagId] = this.getSortAlgorithmFor(tagId);
sorts[tagId] = this.getStoredTagSorting(tagId) || SortAlgorithm.Alphabetic;
orders[tagId] = this.getStoredListOrder(tagId) || ListAlgorithm.Natural;
}
if (this.state.tagsEnabled) {
@ -268,7 +333,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.log("rtags", roomTags);
}
await this.algorithm.populateTags(tags);
await this.algorithm.populateTags(sorts, orders);
await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
this.initialListsGenerated = true;

View file

@ -31,11 +31,14 @@ export class RoomListStoreTempProxy {
return SettingsStore.isFeatureEnabled("feature_new_room_list");
}
public static addListener(handler: () => void) {
public static addListener(handler: () => void): RoomListStoreTempToken {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return RoomListStore.instance.on(UPDATE_EVENT, handler);
const offFn = () => RoomListStore.instance.off(UPDATE_EVENT, handler);
RoomListStore.instance.on(UPDATE_EVENT, handler);
return new RoomListStoreTempToken(offFn);
} else {
return OldRoomListStore.addListener(handler);
const token = OldRoomListStore.addListener(handler);
return new RoomListStoreTempToken(() => token.remove());
}
}
@ -47,3 +50,12 @@ export class RoomListStoreTempProxy {
}
}
}
export class RoomListStoreTempToken {
constructor(private offFn: () => void) {
}
public remove(): void {
this.offFn();
}
}

View file

@ -74,6 +74,11 @@ export class TagWatcher {
this.store.removeFilter(filter);
}
// Destroy any and all old filter conditions to prevent resource leaks
for (const filter of this.filters.values()) {
filter.destroy();
}
this.filters = newFilters;
}
};

View file

@ -0,0 +1,542 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import DMRoomMap from "../../../utils/DMRoomMap";
import { EventEmitter } from "events";
import { arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
import { getEnumValues } from "../../../utils/enums";
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
import {
IListOrderingMap,
IOrderingAlgorithmMap,
ITagMap,
ITagSortingMap,
ListAlgorithm,
SortAlgorithm
} from "./models";
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
import { EffectiveMembership, splitRoomsByMembership } from "../membership";
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering";
// TODO: Add locking support to avoid concurrent writes?
/**
* Fired when the Algorithm has determined a list has been updated.
*/
export const LIST_UPDATED_EVENT = "list_updated_event";
interface IStickyRoom {
room: Room;
position: number;
tag: TagID;
}
/**
* Represents a list ordering algorithm. This class will take care of tag
* management (which rooms go in which tags) and ask the implementation to
* deal with ordering mechanics.
*/
export class Algorithm extends EventEmitter {
private _cachedRooms: ITagMap = {};
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
private filteredRooms: ITagMap = {};
private _stickyRoom: IStickyRoom = null;
private sortAlgorithms: ITagSortingMap;
private listAlgorithms: IListOrderingMap;
private algorithms: IOrderingAlgorithmMap;
private rooms: Room[] = [];
private roomIdsToTags: {
[roomId: string]: TagID[];
} = {};
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
public constructor() {
super();
}
public get stickyRoom(): Room {
return this._stickyRoom ? this._stickyRoom.room : null;
}
public set stickyRoom(val: Room) {
// setters can't be async, so we call a private function to do the work
// noinspection JSIgnoredPromiseFromCall
this.updateStickyRoom(val);
}
protected get hasFilters(): boolean {
return this.allowedByFilter.size > 0;
}
protected set cachedRooms(val: ITagMap) {
this._cachedRooms = val;
this.recalculateFilteredRooms();
this.recalculateStickyRoom();
}
protected get cachedRooms(): ITagMap {
// 🐉 Here be dragons.
// Note: this is used by the underlying algorithm classes, so don't make it return
// the sticky room cache. If it ends up returning the sticky room cache, we end up
// corrupting our caches and confusing them.
return this._cachedRooms;
}
public getTagSorting(tagId: TagID): SortAlgorithm {
return this.sortAlgorithms[tagId];
}
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
if (!tagId) throw new Error("Tag ID must be defined");
if (!sort) throw new Error("Algorithm must be defined");
this.sortAlgorithms[tagId] = sort;
const algorithm: OrderingAlgorithm = this.algorithms[tagId];
await algorithm.setSortAlgorithm(sort);
this._cachedRooms[tagId] = algorithm.orderedRooms;
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
}
public getListOrdering(tagId: TagID): ListAlgorithm {
return this.listAlgorithms[tagId];
}
public async setListOrdering(tagId: TagID, order: ListAlgorithm) {
if (!tagId) throw new Error("Tag ID must be defined");
if (!order) throw new Error("Algorithm must be defined");
this.listAlgorithms[tagId] = order;
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
this.algorithms[tagId] = algorithm;
await algorithm.setRooms(this._cachedRooms[tagId])
this._cachedRooms[tagId] = algorithm.orderedRooms;
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
}
public addFilterCondition(filterCondition: IFilterCondition): void {
// Populate the cache of the new filter
this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r)));
this.recalculateFilteredRooms();
filterCondition.on(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
}
public removeFilterCondition(filterCondition: IFilterCondition): void {
filterCondition.off(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
if (this.allowedByFilter.has(filterCondition)) {
this.allowedByFilter.delete(filterCondition);
// If we removed the last filter, tell consumers that we've "updated" our filtered
// view. This will trick them into getting the complete room list.
if (!this.hasFilters) {
this.emit(LIST_UPDATED_EVENT);
}
}
}
private async updateStickyRoom(val: Room) {
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
// otherwise we risk duplicating rooms.
// It's possible to have no selected room. In that case, clear the sticky room
if (!val) {
if (this._stickyRoom) {
// Lie to the algorithm and re-add the room to the algorithm
await this.handleRoomUpdate(this._stickyRoom.room, RoomUpdateCause.NewRoom);
}
this._stickyRoom = null;
return;
}
// When we do have a room though, we expect to be able to find it
const tag = this.roomIdsToTags[val.roomId][0];
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
let position = this.cachedRooms[tag].indexOf(val);
if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);
// 🐉 Here be dragons.
// Before we can go through with lying to the underlying algorithm about a room
// we need to ensure that when we do we're ready for the innevitable sticky room
// update we'll receive. To prepare for that, we first remove the sticky room and
// recalculate the state ourselves so that when the underlying algorithm calls for
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
// a new update for ourselves.
const lastStickyRoom = this._stickyRoom;
console.log(`Last sticky room:`, lastStickyRoom);
this._stickyRoom = null;
this.recalculateStickyRoom();
// When we do have the room, re-add the old room (if needed) to the algorithm
// and remove the sticky room from the algorithm. This is so the underlying
// algorithm doesn't try and confuse itself with the sticky room concept.
if (lastStickyRoom) {
// Lie to the algorithm and re-add the room to the algorithm
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
}
// Lie to the algorithm and remove the room from it's field of view
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
// Now that we're done lying to the algorithm, we need to update our position
// marker only if the user is moving further down the same list. If they're switching
// lists, or moving upwards, the position marker will splice in just fine but if
// they went downwards in the same list we'll be off by 1 due to the shifting rooms.
if (lastStickyRoom && lastStickyRoom.tag === tag && lastStickyRoom.position <= position) {
position++;
}
this._stickyRoom = {
room: val,
position: position,
tag: tag,
};
this.recalculateStickyRoom();
// Finally, trigger an update
this.emit(LIST_UPDATED_EVENT);
}
protected recalculateFilteredRooms() {
if (!this.hasFilters) {
return;
}
console.warn("Recalculating filtered room list");
const filters = Array.from(this.allowedByFilter.keys());
const orderedFilters = new ArrayUtil(filters)
.groupBy(f => f.relativePriority)
.orderBy(getEnumValues(FilterPriority))
.value;
const newMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) {
// Cheaply clone the rooms so we can more easily do operations on the list.
// We optimize our lookups by trying to reduce sample size as much as possible
// to the rooms we know will be deduped by the Set.
const rooms = this.cachedRooms[tagId];
let remainingRooms = rooms.map(r => r);
let allowedRoomsInThisTag = [];
let lastFilterPriority = orderedFilters[0].relativePriority;
for (const filter of orderedFilters) {
if (filter.relativePriority !== lastFilterPriority) {
// Every time the filter changes priority, we want more specific filtering.
// To accomplish that, reset the variables to make it look like the process
// has started over, but using the filtered rooms as the seed.
remainingRooms = allowedRoomsInThisTag;
allowedRoomsInThisTag = [];
lastFilterPriority = filter.relativePriority;
}
const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
for (const room of filteredRooms) {
const idx = remainingRooms.indexOf(room);
if (idx >= 0) remainingRooms.splice(idx, 1);
allowedRoomsInThisTag.push(room);
}
}
newMap[tagId] = allowedRoomsInThisTag;
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
}
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
this.allowedRoomsByFilters = new Set(allowedRooms);
this.filteredRooms = newMap;
this.emit(LIST_UPDATED_EVENT);
}
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
const filters = this.allowedByFilter.keys();
for (const room of added) {
for (const filter of filters) {
if (filter.isVisible(room)) {
this.allowedRoomsByFilters.add(room);
break;
}
}
}
// Now that we've updated the allowed rooms, recalculate the tag
this.recalculateFilteredRoomsForTag(tagId);
}
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
console.log(`Recalculating filtered rooms for ${tagId}`);
delete this.filteredRooms[tagId];
const rooms = this.cachedRooms[tagId];
const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
if (filteredRooms.length > 0) {
this.filteredRooms[tagId] = filteredRooms;
}
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
}
/**
* Recalculate the sticky room position. If this is being called in relation to
* a specific tag being updated, it should be given to this function to optimize
* the call.
* @param updatedTag The tag that was updated, if possible.
*/
protected recalculateStickyRoom(updatedTag: TagID = null): void {
// 🐉 Here be dragons.
// This function does far too much for what it should, and is called by many places.
// Not only is this responsible for ensuring the sticky room is held in place at all
// times, it is also responsible for ensuring our clone of the cachedRooms is up to
// date. If either of these desyncs, we see weird behaviour like duplicated rooms,
// outdated lists, and other nonsensical issues that aren't necessarily obvious.
if (!this._stickyRoom) {
// If there's no sticky room, just do nothing useful.
if (!!this._cachedStickyRooms) {
// Clear the cache if we won't be needing it
this._cachedStickyRooms = null;
this.emit(LIST_UPDATED_EVENT);
}
return;
}
if (!this._cachedStickyRooms || !updatedTag) {
console.log(`Generating clone of cached rooms for sticky room handling`);
const stickiedTagMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) {
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
}
this._cachedStickyRooms = stickiedTagMap;
}
if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date.
console.log(`Replacing cached sticky rooms for ${updatedTag}`);
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
}
// Now try to insert the sticky room, if we need to.
// We need to if there's no updated tag (we regenned the whole cache) or if the tag
// we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) {
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
}
// Finally, trigger an update
this.emit(LIST_UPDATED_EVENT);
}
/**
* Asks the Algorithm to regenerate all lists, using the tags given
* as reference for which lists to generate and which way to generate
* them.
* @param {ITagSortingMap} tagSortingMap The tags to generate.
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
* @returns {Promise<*>} A promise which resolves when complete.
*/
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<any> {
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
throw new Error(`Both maps must contain the exact same tags`);
}
this.sortAlgorithms = tagSortingMap;
this.listAlgorithms = listOrderingMap;
this.algorithms = {};
for (const tag of Object.keys(tagSortingMap)) {
this.algorithms[tag] = getListAlgorithmInstance(this.listAlgorithms[tag], tag, this.sortAlgorithms[tag]);
}
return this.setKnownRooms(this.rooms);
}
/**
* Gets an ordered set of rooms for the all known tags, filtered.
* @returns {ITagMap} The cached list of rooms, ordered,
* for each tag. May be empty, but never null/undefined.
*/
public getOrderedRooms(): ITagMap {
if (!this.hasFilters) {
return this._cachedStickyRooms || this.cachedRooms;
}
return this.filteredRooms;
}
/**
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
* previously known information and instead use these rooms instead.
* @param {Room[]} rooms The rooms to force the algorithm to use.
* @returns {Promise<*>} A promise which resolves when complete.
*/
public async setKnownRooms(rooms: Room[]): Promise<any> {
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
this.rooms = rooms;
const newTags: ITagMap = {};
for (const tagId in this.sortAlgorithms) {
// noinspection JSUnfilteredForInLoop
newTags[tagId] = [];
}
// If we can avoid doing work, do so.
if (!rooms.length) {
await this.generateFreshTags(newTags); // just in case it wants to do something
this.cachedRooms = newTags;
return;
}
// Split out the easy rooms first (leave and invite)
const memberships = splitRoomsByMembership(rooms);
for (const room of memberships[EffectiveMembership.Invite]) {
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
newTags[DefaultTagID.Invite].push(room);
}
for (const room of memberships[EffectiveMembership.Leave]) {
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
newTags[DefaultTagID.Archived].push(room);
}
// Now process all the joined rooms. This is a bit more complicated
for (const room of memberships[EffectiveMembership.Join]) {
let tags = Object.keys(room.tags || {});
if (tags.length === 0) {
// Check to see if it's a DM if it isn't anything else
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
tags = [DefaultTagID.DM];
}
}
let inTag = false;
if (tags.length > 0) {
for (const tag of tags) {
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
if (!isNullOrUndefined(newTags[tag])) {
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
newTags[tag].push(room);
inTag = true;
}
}
}
if (!inTag) {
// TODO: Determine if DM and push there instead
newTags[DefaultTagID.Untagged].push(room);
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
}
}
await this.generateFreshTags(newTags);
this.cachedRooms = newTags;
this.updateTagsFromCache();
}
/**
* Updates the roomsToTags map
*/
protected updateTagsFromCache() {
const newMap = {};
const tags = Object.keys(this.cachedRooms);
for (const tagId of tags) {
const rooms = this.cachedRooms[tagId];
for (const room of rooms) {
if (!newMap[room.roomId]) newMap[room.roomId] = [];
newMap[room.roomId].push(tagId);
}
}
this.roomIdsToTags = newMap;
}
/**
* Called when the Algorithm believes a complete regeneration of the existing
* lists is needed.
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
* will already have the rooms which belong to it - they just need ordering. Must
* be mutated in place.
* @returns {Promise<*>} A promise which resolves when complete.
*/
private async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
for (const tag of Object.keys(updatedTagMap)) {
const algorithm: OrderingAlgorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
await algorithm.setRooms(updatedTagMap[tag]);
updatedTagMap[tag] = algorithm.orderedRooms;
}
}
/**
* Asks the Algorithm to update its knowledge of a room. For example, when
* a user tags a room, joins/creates a room, or leaves a room the Algorithm
* should be told that the room's info might have changed. The Algorithm
* may no-op this request if no changes are required.
* @param {Room} room The room which might have affected sorting.
* @param {RoomUpdateCause} cause The reason for the update being triggered.
* @returns {Promise<boolean>} A promise which resolve to true or false
* depending on whether or not getOrderedRooms() should be called after
* processing.
*/
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
if (cause === RoomUpdateCause.PossibleTagChange) {
// TODO: Be smarter and splice rather than regen the planet.
// TODO: No-op if no change.
await this.setKnownRooms(this.rooms);
return true;
}
if (cause === RoomUpdateCause.NewRoom) {
// TODO: Be smarter and insert rather than regen the planet.
await this.setKnownRooms([room, ...this.rooms]);
return true;
}
if (cause === RoomUpdateCause.RoomRemoved) {
// TODO: Be smarter and splice rather than regen the planet.
await this.setKnownRooms(this.rooms.filter(r => r !== room));
return true;
}
let tags = this.roomIdsToTags[room.roomId];
if (!tags) {
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
return false;
}
let changed = false;
for (const tag of tags) {
const algorithm: OrderingAlgorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
await algorithm.handleRoomUpdate(room, cause);
this.cachedRooms[tag] = algorithm.orderedRooms;
// Flag that we've done something
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
changed = true;
}
return true;
};
}

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