diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index 90a30968d9b..599cacde13d 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -1,5 +1,5 @@ const EventEmitter = require("events"); -const { LngLat, NavigationControl } = require('maplibre-gl'); +const { LngLat, NavigationControl, LngLatBounds } = require('maplibre-gl'); class MockMap extends EventEmitter { addControl = jest.fn(); @@ -8,6 +8,7 @@ class MockMap extends EventEmitter { zoomOut = jest.fn(); setCenter = jest.fn(); setStyle = jest.fn(); + fitBounds = jest.fn(); } const MockMapInstance = new MockMap(); @@ -24,5 +25,6 @@ module.exports = { GeolocateControl: jest.fn().mockReturnValue(MockGeolocateInstance), Marker: jest.fn().mockReturnValue(MockMarker), LngLat, + LngLatBounds, NavigationControl, }; diff --git a/res/css/_common.scss b/res/css/_common.scss index 5c6349c2204..b5d873194dd 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -692,3 +692,9 @@ legend { } } } + +@define-mixin ListResetDefault { + list-style: none; + padding: 0; + margin: 0; +} diff --git a/res/css/_components.scss b/res/css/_components.scss index f39147cc647..60cacf03836 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -4,8 +4,10 @@ @import "./_font-sizes.scss"; @import "./_font-weights.scss"; @import "./_spacing.scss"; +@import "./components/views/beacon/_BeaconListItem.scss"; @import "./components/views/beacon/_BeaconStatus.scss"; @import "./components/views/beacon/_BeaconViewDialog.scss"; +@import "./components/views/beacon/_DialogSidebar.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; @import "./components/views/beacon/_LiveTimeRemaining.scss"; @import "./components/views/beacon/_OwnBeaconStatus.scss"; diff --git a/res/css/components/views/beacon/_BeaconListItem.scss b/res/css/components/views/beacon/_BeaconListItem.scss new file mode 100644 index 00000000000..60311a4466f --- /dev/null +++ b/res/css/components/views/beacon/_BeaconListItem.scss @@ -0,0 +1,61 @@ +/* +Copyright 2022 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_BeaconListItem { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + padding: $spacing-12 0; + + border-bottom: 1px solid $system; +} + +.mx_BeaconListItem_avatarIcon { + flex: 0 0; + height: 32px; + width: 32px; +} + +.mx_BeaconListItem_avatar { + flex: 0 0; + box-sizing: border-box; + + margin-right: $spacing-8; + border: 2px solid $location-live-color; +} + +.mx_BeaconListItem_info { + flex: 1 1 0; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.mx_BeaconListItem_status { + // override beacon status padding + padding: 0 !important; + margin-bottom: $spacing-8; + + .mx_BeaconStatus_label { + font-weight: $font-semi-bold; + } +} + +.mx_BeaconListItem_lastUpdated { + color: $tertiary-content; + font-size: $font-10px; +} diff --git a/res/css/components/views/beacon/_BeaconStatus.scss b/res/css/components/views/beacon/_BeaconStatus.scss index 8ac873604d2..4dd3d325475 100644 --- a/res/css/components/views/beacon/_BeaconStatus.scss +++ b/res/css/components/views/beacon/_BeaconStatus.scss @@ -59,3 +59,7 @@ limitations under the License. .mx_BeaconStatus_expiryTime { color: $secondary-content; } + +.mx_BeaconStatus_label { + margin-bottom: 2px; +} diff --git a/res/css/components/views/beacon/_BeaconViewDialog.scss b/res/css/components/views/beacon/_BeaconViewDialog.scss index 901b4564395..6ad1a2a6139 100644 --- a/res/css/components/views/beacon/_BeaconViewDialog.scss +++ b/res/css/components/views/beacon/_BeaconViewDialog.scss @@ -29,6 +29,9 @@ limitations under the License. height: calc(80vh - 0.5px); overflow: hidden; + // sidebar is absolutely positioned inside + position: relative; + .mx_Dialog_header { margin: 0px; padding: 0px; @@ -40,7 +43,7 @@ limitations under the License. .mx_Dialog_cancelButton { z-index: 4010; - position: absolute; + position: fixed; right: 5vw; top: 5vh; width: 20px; @@ -55,3 +58,31 @@ limitations under the License. height: 80vh; border-radius: 8px; } + +.mx_BeaconViewDialog_mapFallback { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + background: url('$(res)/img/location/map.svg'); + background-size: cover; +} + +.mx_BeaconViewDialog_mapFallbackIcon { + width: 65px; + margin-bottom: $spacing-16; + color: $quaternary-content; +} + +.mx_BeaconViewDialog_mapFallbackMessage { + color: $secondary-content; + margin-bottom: $spacing-16; +} + +.mx_BeaconViewDialog_viewListButton { + position: absolute; + top: $spacing-24; + left: $spacing-24; +} diff --git a/res/css/components/views/beacon/_DialogSidebar.scss b/res/css/components/views/beacon/_DialogSidebar.scss new file mode 100644 index 00000000000..1989b57c301 --- /dev/null +++ b/res/css/components/views/beacon/_DialogSidebar.scss @@ -0,0 +1,60 @@ +/* +Copyright 2022 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_DialogSidebar { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 265px; + + display: flex; + flex-direction: column; + + box-sizing: border-box; + padding: $spacing-16; + + background-color: $background; + box-shadow: 0px 4px 4px $menu-box-shadow-color; +} + +.mx_DialogSidebar_header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + flex: 0 0; + margin-bottom: $spacing-16; + + color: $primary-content; +} + +.mx_DialogSidebar_closeButton { + @mixin ButtonResetDefault; +} + +.mx_DialogSidebar_closeButtonIcon { + color: $tertiary-content; + height: 12px; +} + +.mx_DialogSidebar_list { + @mixin ListResetDefault; + flex: 1 1 0; + width: 100%; + overflow: auto; +} diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index f18b4917cf1..07735ad0278 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -22,84 +22,99 @@ limitations under the License. margin: 0; padding: 0; } -} -.mx_RoomSettingsDialog_BridgeList li { - list-style-type: none; - padding: 5px; - margin-bottom: 8px; - border-width: 1px 1px; - border-color: $primary-hairline-color; - border-style: solid; - border-radius: 5px; + li { + list-style-type: none; - .column-icon { - float: left; - padding-right: 10px; + &.mx_RoomSettingsDialog_BridgeList_listItem { + display: flex; + flex-wrap: wrap; + gap: $spacing-8; + padding: 5px; + margin-bottom: $spacing-8; - * { + // border-style around each bridge list item + border-width: 1px 1px; + border-color: $primary-hairline-color; + border-style: solid; border-radius: 5px; - border: 1px solid $input-darker-bg-color; - } - - .noProtocolIcon { - width: 48px; - height: 48px; - background: $input-darker-bg-color; - border-radius: 5px; - } - .protocol-icon { - float: left; - margin-right: 5px; - img { - border-radius: 5px; - border-width: 1px 1px; - border-color: $primary-hairline-color; + .mx_RoomSettingsDialog_column_icon { + .mx_RoomSettingsDialog_protocolIcon, + .mx_RoomSettingsDialog_protocolIcon span, + .mx_RoomSettingsDialog_noProtocolIcon { + box-sizing: border-box; + border-radius: 5px; + border: 1px solid $input-darker-bg-color; + } + + .mx_RoomSettingsDialog_noProtocolIcon, + .mx_RoomSettingsDialog_protocolIcon img { + border-radius: 5px; + } + + .mx_RoomSettingsDialog_noProtocolIcon { + width: 48px; + height: 48px; + background: $input-darker-bg-color; + } + + .mx_RoomSettingsDialog_protocolIcon { + img { + border-width: 1px 1px; + border-color: $primary-hairline-color; + } + + span { + /* Correct letter placement */ + left: auto; + } + } } - span { - /* Correct letter placement */ - left: auto; - } - } - } - - .column-data { - display: inline-block; - width: 85%; - - > h3 { - margin-top: 0px; - margin-bottom: 0px; - font-size: 16pt; - color: $primary-content; - } - - > * { - margin-top: 4px; - margin-bottom: 0; - } - - .workspace-channel-details { - color: $primary-content; - font-weight: 600; - - .channel { - margin-left: 5px; - } - } - .metadata { - color: $muted-fg-color; - margin-bottom: 0; - overflow-y: visible; - text-overflow: ellipsis; - white-space: normal; - padding: 0; - - > li { - padding: 0; - border: 0; + .mx_RoomSettingsDialog_column_data { + display: inline-block; + width: 85%; + + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata, + .mx_RoomSettingsDialog_column_data_metadata li, + .mx_RoomSettingsDialog_column_data_protocolName { + margin-bottom: 0; + } + + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata { + margin-top: $spacing-4; + } + + .mx_RoomSettingsDialog_column_data_metadata li { + margin-top: $spacing-8; + } + + .mx_RoomSettingsDialog_column_data_protocolName { + margin-top: 0; + font-size: 16pt; + color: $primary-content; + } + + .mx_RoomSettingsDialog_workspace_channel_details { + color: $primary-content; + font-weight: $font-semi-bold; + + .mx_RoomSettingsDialog_channel { + margin-inline-start: 5px; + } + } + + .mx_RoomSettingsDialog_metadata { + color: $muted-fg-color; + margin-bottom: 0; + overflow-y: visible; + text-overflow: ellipsis; + white-space: normal; + padding: 0; + } } } } diff --git a/res/css/views/elements/_CopyableText.scss b/res/css/views/elements/_CopyableText.scss index a08306b66a4..ceafd422730 100644 --- a/res/css/views/elements/_CopyableText.scss +++ b/res/css/views/elements/_CopyableText.scss @@ -18,14 +18,17 @@ limitations under the License. .mx_CopyableText { display: flex; justify-content: space-between; - border-radius: 5px; - border: solid 1px $light-fg-color; - margin-bottom: 10px; - margin-top: 10px; - padding: 10px; width: max-content; max-width: 100%; + &.mx_CopyableText_border { + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; + } + .mx_CopyableText_copyButton { flex-shrink: 0; width: 20px; diff --git a/res/css/views/elements/_SettingsFlag.scss b/res/css/views/elements/_SettingsFlag.scss index 533487d98cf..c6f4cf6ec5c 100644 --- a/res/css/views/elements/_SettingsFlag.scss +++ b/res/css/views/elements/_SettingsFlag.scss @@ -41,4 +41,10 @@ limitations under the License. font-size: $font-12px; line-height: $font-15px; color: $secondary-content; + + // Support code/pre elements in settings flag descriptions + pre, code { + font-family: $monospace-font-family !important; + background-color: $rte-code-bg-color; + } } diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss index f5bdb8d2d58..7efe83281f5 100644 --- a/res/css/views/elements/_TagComposer.scss +++ b/res/css/views/elements/_TagComposer.scss @@ -29,7 +29,9 @@ limitations under the License. margin-left: 16px; // distance from } - .mx_Field, .mx_Field input, .mx_AccessibleButton { + .mx_Field, + .mx_Field input, + .mx_AccessibleButton { // So they look related to each other by feeling the same border-radius: 8px; } @@ -39,39 +41,48 @@ limitations under the License. display: flex; flex-wrap: wrap; margin-top: 12px; // this plus 12px from the tags makes 24px from the input + } - .mx_TagComposer_tag { - padding: 6px 8px 8px 12px; - position: relative; - margin-right: 12px; - margin-top: 12px; - - // Cheaty way to get an opacified variable colour background - &::before { - content: ''; - border-radius: 20px; - background-color: $tertiary-content; - opacity: 0.15; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - // Pass through the pointer otherwise we have effectively put a whole div - // on top of the component, which makes it hard to interact with buttons. - pointer-events: none; - } - } + .mx_Tag { + margin-right: 12px; + margin-top: 12px; + } +} - .mx_AccessibleButton { - background-image: url('$(res)/img/subtract.svg'); - width: 16px; - height: 16px; - margin-left: 8px; - display: inline-block; +.mx_Tag { + + font-size: $font-15px; + + display: inline-flex; + align-items: center; + + gap: 8px; + padding: 8px; + border-radius: 8px; + + color: $primary-content; + background: $quinary-content; + + >svg:first-child { + width: 1em; + color: $secondary-content; + transform: scale(1.25); + transform-origin: center; + } + + .mx_Tag_delete { + border-radius: 50%; + text-align: center; + width: 1.066666em; // 16px; + height: 1.066666em; + line-height: 1em; + color: $secondary-content; + background: $system; + position: relative; + + svg { + width: .5em; vertical-align: middle; - cursor: pointer; } } } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 170fbb81e3e..85c139402be 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -141,17 +141,24 @@ limitations under the License. } .mx_RoomHeader_topic { + $lineHeight: $font-16px; + $lines: 2; + flex: 1; color: $roomtopic-color; font-weight: 400; font-size: $font-13px; + line-height: $lineHeight; + max-height: calc($lineHeight * $lines); + border-bottom: 1px solid transparent; + // to align baseline of topic with room name margin: 4px 7px 0; + overflow: hidden; - text-overflow: ellipsis; - border-bottom: 1px solid transparent; - line-height: 1.2em; - max-height: 2.4em; + -webkit-line-clamp: $lines; // See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp + -webkit-box-orient: vertical; + display: -webkit-box; } .mx_RoomHeader_avatar { diff --git a/res/css/views/typography/_Heading.scss b/res/css/views/typography/_Heading.scss index 9b7ddeaef3f..84a008c18f8 100644 --- a/res/css/views/typography/_Heading.scss +++ b/res/css/views/typography/_Heading.scss @@ -37,3 +37,11 @@ limitations under the License. margin-inline: unset; margin-block: unset; } + +.mx_Heading_h4 { + font-size: $font-15px; + font-weight: $font-semi-bold; + line-height: $font-20px; + margin-inline: unset; + margin-block: unset; +} diff --git a/res/css/views/voip/_VideoLobby.scss b/res/css/views/voip/_VideoLobby.scss index f9921c489a7..a708e79c90e 100644 --- a/res/css/views/voip/_VideoLobby.scss +++ b/res/css/views/voip/_VideoLobby.scss @@ -144,7 +144,8 @@ limitations under the License. } &.mx_VideoLobby_deviceButtonWrapper_active { - .mx_VideoLobby_deviceButton, .mx_VideoLobby_deviceListButton { + .mx_VideoLobby_deviceButton, + .mx_VideoLobby_deviceListButton { background-color: $video-lobby-system; &::before { diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 1dd37a8c412..3009bedae3e 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -163,7 +163,9 @@ const EmptyThread: React.FC = ({ hasThreads, filterOption, sh body = <>

{ _t("Threads help keep your conversations on-topic and easy to track.") }

- { _t('Tip: Use "Reply in thread" when hovering over a message.', {}, { + { _t('Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.', { + replyInThread: _t("Reply in thread"), + }, { b: sub => { sub }, }) }

diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index e84a26409e2..c8628a7f96f 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -167,8 +167,16 @@ export default class ViewSource extends React.Component { return (
-
{ _t("Room ID: %(roomId)s", { roomId }) }
-
{ _t("Event ID: %(eventId)s", { eventId }) }
+
+ roomId} border={false}> + { _t("Room ID: %(roomId)s", { roomId }) } + +
+
+ eventId} border={false}> + { _t("Event ID: %(eventId)s", { eventId }) } + +
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx new file mode 100644 index 00000000000..eda1580700e --- /dev/null +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -0,0 +1,82 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useContext } from 'react'; +import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; + +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import { useEventEmitterState } from '../../../hooks/useEventEmitter'; +import { humanizeTime } from '../../../utils/humanize'; +import { _t } from '../../../languageHandler'; +import MemberAvatar from '../avatars/MemberAvatar'; +import CopyableText from '../elements/CopyableText'; +import BeaconStatus from './BeaconStatus'; +import { BeaconDisplayStatus } from './displayStatus'; +import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; + +interface Props { + beacon: Beacon; +} + +const BeaconListItem: React.FC = ({ beacon }) => { + const latestLocationState = useEventEmitterState( + beacon, + BeaconEvent.LocationUpdate, + () => beacon.latestLocationState, + ); + const matrixClient = useContext(MatrixClientContext); + const room = matrixClient.getRoom(beacon.roomId); + + if (!latestLocationState || !beacon.isLive) { + return null; + } + + const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self; + const beaconMember = isSelfLocation ? + room.getMember(beacon.beaconInfoOwner) : + undefined; + + const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp); + + return
  • + { isSelfLocation ? + : + + } +
    + + latestLocationState?.uri} + /> + + { _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) } +
    +
  • ; +}; + +export default BeaconListItem; diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 8c176ab9c07..f7f284b88ed 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -58,6 +58,7 @@ const BeaconMarker: React.FC = ({ map, beacon }) => { id={beacon.identifier} geoUri={geoUri} roomMember={markerRoomMember} + useMemberColor />; }; diff --git a/src/components/views/beacon/BeaconStatus.tsx b/src/components/views/beacon/BeaconStatus.tsx index c9d7bd3762d..935e22f4f0b 100644 --- a/src/components/views/beacon/BeaconStatus.tsx +++ b/src/components/views/beacon/BeaconStatus.tsx @@ -28,6 +28,7 @@ import { formatTime } from '../../../DateUtils'; interface Props { displayStatus: BeaconDisplayStatus; displayLiveTimeRemaining?: boolean; + withIcon?: boolean; beacon?: Beacon; label?: string; } @@ -45,6 +46,7 @@ const BeaconStatus: React.FC> = label, className, children, + withIcon, ...rest }) => { const isIdle = displayStatus === BeaconDisplayStatus.Loading || @@ -54,11 +56,11 @@ const BeaconStatus: React.FC> = {...rest} className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)} > - + /> }
    { displayStatus === BeaconDisplayStatus.Loading && { _t('Loading live location...') } } @@ -68,7 +70,7 @@ const BeaconStatus: React.FC> = { displayStatus === BeaconDisplayStatus.Active && beacon && <> <> - { label } + { label } { displayLiveTimeRemaining ? : diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index 052a456fe69..76b9b75e3e4 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Beacon, @@ -22,6 +22,7 @@ import { } from 'matrix-js-sdk/src/matrix'; import maplibregl from 'maplibre-gl'; +import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg'; import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; import BaseDialog from "../dialogs/BaseDialog"; @@ -29,29 +30,46 @@ import { IDialogProps } from "../dialogs/IDialogProps"; import Map from '../location/Map'; import ZoomButtons from '../location/ZoomButtons'; import BeaconMarker from './BeaconMarker'; +import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds'; +import { getGeoUri } from '../../../utils/beacon'; +import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from '../elements/AccessibleButton'; +import DialogSidebar from './DialogSidebar'; interface IProps extends IDialogProps { roomId: Room['roomId']; matrixClient: MatrixClient; + // open the map centered on this beacon's location + focusBeacon?: Beacon; } -// TODO actual center is coming soon -// for now just center around first beacon in list -const getMapCenterUri = (beacons: Beacon[]): string => { - const firstBeaconWithLocation = beacons.find(beacon => beacon.latestLocationState); - - return firstBeaconWithLocation?.latestLocationState?.uri; +const getBoundsCenter = (bounds: Bounds): string | undefined => { + if (!bounds) { + return; + } + return getGeoUri({ + latitude: (bounds.north + bounds.south) / 2, + longitude: (bounds.east + bounds.west) / 2, + timestamp: Date.now(), + }); }; /** * Dialog to view live beacons maximised */ -const BeaconViewDialog: React.FC = ({ roomId, matrixClient, onFinished }) => { +const BeaconViewDialog: React.FC = ({ + focusBeacon, + roomId, + matrixClient, + onFinished, +}) => { const liveBeacons = useLiveBeacons(roomId, matrixClient); - const mapCenterUri = getMapCenterUri(liveBeacons); - // TODO probably show loader or placeholder when there is no location - // to center the map on + const [isSidebarOpen, setSidebarOpen] = useState(false); + + const bounds = getBeaconBounds(liveBeacons); + const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds); return ( = ({ roomId, matrixClient, onFinished } fixedWidth={false} > - @@ -77,7 +96,34 @@ const BeaconViewDialog: React.FC = ({ roomId, matrixClient, onFinished } } - + : +
    + + { _t('No live locations') } + + { _t('Close') } + +
    + } + { isSidebarOpen ? + setSidebarOpen(false)} /> : + setSidebarOpen(true)} + data-test-id='beacon-view-dialog-open-sidebar' + className='mx_BeaconViewDialog_viewListButton' + > +   + { _t('View list') } + + }
    ); diff --git a/src/components/views/beacon/DialogSidebar.tsx b/src/components/views/beacon/DialogSidebar.tsx new file mode 100644 index 00000000000..4365b5fa8b6 --- /dev/null +++ b/src/components/views/beacon/DialogSidebar.tsx @@ -0,0 +1,50 @@ +/* +Copyright 2022 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 { Beacon } from 'matrix-js-sdk/src/matrix'; + +import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from '../elements/AccessibleButton'; +import Heading from '../typography/Heading'; +import BeaconListItem from './BeaconListItem'; + +interface Props { + beacons: Beacon[]; + requestClose: () => void; +} + +const DialogSidebar: React.FC = ({ beacons, requestClose }) => { + return
    +
    + { _t('View List') } + + + +
    +
      + { beacons.map((beacon) => ) } +
    +
    ; +}; + +export default DialogSidebar; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 204e2968293..0a682b11641 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -54,6 +54,7 @@ const OwnBeaconStatus: React.FC> = ({ displayStatus={ownDisplayStatus} label={_t('Live location enabled')} displayLiveTimeRemaining + withIcon {...rest} > { ownDisplayStatus === BeaconDisplayStatus.Active && ); const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent; - let openInMapSiteButton: JSX.Element; - let endPollButton: JSX.Element; let resendReactionsButton: JSX.Element; - let redactButton: JSX.Element; - let forwardButton: JSX.Element; - let pinButton: JSX.Element; - let unhidePreviewButton: JSX.Element; - let externalURLButton: JSX.Element; - let quoteButton: JSX.Element; - let redactItemList: JSX.Element; - let reportEventButton: JSX.Element; - let copyButton: JSX.Element; - let editButton: JSX.Element; - let replyButton: JSX.Element; - let reactButton: JSX.Element; - let reactionPicker: JSX.Element; - let quickItemsList: JSX.Element; - let nativeItemsList: JSX.Element; - let permalinkButton: JSX.Element; - let collapseReplyChainButton: JSX.Element; - let viewInRoomButton: JSX.Element; - if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) { resendReactionsButton = ( ); } + let redactButton: JSX.Element; if (isSent && this.state.canRedact) { redactButton = ( ); } + let openInMapSiteButton: JSX.Element; if (this.canOpenInMapSite(mxEvent)) { const mapSiteLink = createMapSiteLink(mxEvent); openInMapSiteButton = ( @@ -404,6 +375,7 @@ export default class MessageContextMenu extends React.Component ); } + let forwardButton: JSX.Element; if (contentActionable && canForward(mxEvent)) { forwardButton = ( ); } + let pinButton: JSX.Element; if (contentActionable && this.state.canPin) { pinButton = ( ); } - let viewSourceButton: JSX.Element; - if (SettingsStore.getValue("developerMode")) { - viewSourceButton = ( - - ); - } + // This is specifically not behind the developerMode flag to give people insight into the Matrix + const viewSourceButton = ( + + ); + let unhidePreviewButton: JSX.Element; if (eventTileOps?.isWidgetHidden()) { unhidePreviewButton = ( ); } + let permalinkButton: JSX.Element; if (permalink) { permalinkButton = ( ); } + let endPollButton: JSX.Element; if (this.canEndPoll(mxEvent)) { endPollButton = ( ); } + let quoteButton: JSX.Element; if (eventTileOps) { // this event is rendered using TextualBody quoteButton = ( } // Bridges can provide a 'external_url' to link back to the source. + let externalURLButton: JSX.Element; if ( typeof (mxEvent.getContent().external_url) === "string" && isUrlPermitted(mxEvent.getContent().external_url) @@ -511,6 +487,7 @@ export default class MessageContextMenu extends React.Component ); } + let collapseReplyChainButton: JSX.Element; if (collapseReplyChain) { collapseReplyChainButton = ( ); } + let reportEventButton: JSX.Element; if (mxEvent.getSender() !== me) { reportEventButton = ( ); } + let copyButton: JSX.Element; if (rightClick && getSelectedText()) { copyButton = ( ); } + let editButton: JSX.Element; if (rightClick && canEditContent(mxEvent)) { editButton = ( ); } + let replyButton: JSX.Element; if (rightClick && contentActionable && canSendMessages) { replyButton = ( ); } + let reactButton; if (rightClick && contentActionable && canReact) { reactButton = ( ); } + let viewInRoomButton: JSX.Element; if (isThreadRootEvent) { viewInRoomButton = ( ); } + let nativeItemsList: JSX.Element; if (copyButton) { nativeItemsList = ( @@ -591,6 +575,7 @@ export default class MessageContextMenu extends React.Component ); } + let quickItemsList: JSX.Element; if (editButton || replyButton || reactButton) { quickItemsList = ( @@ -619,6 +604,7 @@ export default class MessageContextMenu extends React.Component ); + let redactItemList: JSX.Element; if (redactButton) { redactItemList = ( @@ -627,6 +613,7 @@ export default class MessageContextMenu extends React.Component ); } + let reactionPicker: JSX.Element; if (this.state.reactionPickerDisplayed) { const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect(); reactionPicker = ( @@ -662,20 +649,3 @@ export default class MessageContextMenu extends React.Component } } -function canForward(event: MatrixEvent): boolean { - return !( - isLocationEvent(event) || - M_POLL_START.matches(event.getType()) - ); -} - -function isLocationEvent(event: MatrixEvent): boolean { - const eventType = event.getType(); - return ( - M_LOCATION.matches(eventType) || - ( - eventType === EventType.RoomMessage && - M_LOCATION.matches(event.getContent().msgtype) - ) - ); -} diff --git a/src/components/views/dialogs/devtools/ServerInfo.tsx b/src/components/views/dialogs/devtools/ServerInfo.tsx index ff18d836e5c..23b6528eacc 100644 --- a/src/components/views/dialogs/devtools/ServerInfo.tsx +++ b/src/components/views/dialogs/devtools/ServerInfo.tsx @@ -74,13 +74,13 @@ const ServerInfo = ({ onBack }: IDevtoolsProps) => { }

    { _t("Client Versions") }

    - { capabilities !== FAILED_TO_LOAD + { clientVersions !== FAILED_TO_LOAD ? :
    { _t("Failed to load.") }
    }

    { _t("Server Versions") }

    - { capabilities !== FAILED_TO_LOAD + { serverVersions !== FAILED_TO_LOAD ? :
    { _t("Failed to load.") }
    } diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx index d1632af3825..f95cbcbd168 100644 --- a/src/components/views/elements/CopyableText.tsx +++ b/src/components/views/elements/CopyableText.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React, { useState } from "react"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { copyPlaintext } from "../../../utils/strings"; @@ -23,11 +24,12 @@ import { ButtonEvent } from "./AccessibleButton"; import AccessibleTooltipButton from "./AccessibleTooltipButton"; interface IProps { - children: React.ReactNode; + children?: React.ReactNode; getTextToCopy: () => string; + border?: boolean; } -const CopyableText: React.FC = ({ children, getTextToCopy }) => { +const CopyableText: React.FC = ({ children, getTextToCopy, border=true }) => { const [tooltip, setTooltip] = useState(undefined); const onCopyClickInternal = async (e: ButtonEvent) => { @@ -42,7 +44,11 @@ const CopyableText: React.FC = ({ children, getTextToCopy }) => { } }; - return
    + const className = classNames("mx_CopyableText", { + mx_CopyableText_border: border, + }); + + return
    { children } JSX.Element; + label: string; + onDeleteClick?: () => void; + disabled?: boolean; +} + +export const Tag = ({ + icon, + label, + onDeleteClick, + disabled = false, +}: IProps) => { + return
    + { icon?.() } + { label } + { onDeleteClick && ( + + + + ) } +
    ; +}; diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx index 19f3523f067..5d1bff84e8e 100644 --- a/src/components/views/elements/TagComposer.tsx +++ b/src/components/views/elements/TagComposer.tsx @@ -19,6 +19,7 @@ import React, { ChangeEvent, FormEvent } from "react"; import Field from "./Field"; import { _t } from "../../../languageHandler"; import AccessibleButton from "./AccessibleButton"; +import { Tag } from "./Tag"; interface IProps { tags: string[]; @@ -80,10 +81,13 @@ export default class TagComposer extends React.PureComponent {
    - { this.props.tags.map((t, i) => (
    - { t } - -
    )) } + { this.props.tags.map((t, i) => ( + + )) }
    ; } diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 8776e8e8264..fc3bfab3eb7 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { ReactNode, useContext, useEffect } from 'react'; import classNames from 'classnames'; +import maplibregl from 'maplibre-gl'; import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/matrix'; import { logger } from 'matrix-js-sdk/src/logger'; @@ -24,8 +25,9 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { parseGeoUri } from '../../../utils/location'; import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils'; import { useMap } from '../../../utils/location/useMap'; +import { Bounds } from '../../../utils/beacon/bounds'; -const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => { +const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) => { const bodyId = `mx_Map_${id}`; // style config @@ -55,6 +57,20 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => { } }, [map, centerGeoUri]); + useEffect(() => { + if (map && bounds) { + try { + const lngLatBounds = new maplibregl.LngLatBounds( + [bounds.west, bounds.south], + [bounds.east, bounds.north], + ); + map.fitBounds(lngLatBounds, { padding: 100 }); + } catch (error) { + logger.error('Invalid map bounds', error); + } + } + }, [map, bounds]); + return { map, bodyId, @@ -65,6 +81,7 @@ interface MapProps { id: string; interactive?: boolean; centerGeoUri?: string; + bounds?: Bounds; className?: string; onClick?: () => void; onError?: (error: Error) => void; @@ -74,9 +91,15 @@ interface MapProps { } const Map: React.FC = ({ - centerGeoUri, className, id, onError, onClick, children, interactive, + bounds, + centerGeoUri, + children, + className, + id, + interactive, + onError, onClick, }) => { - const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive }); + const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive, bounds }); const onMapClick = ( event: React.MouseEvent, diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index f61ec346e4f..bd7e10f044d 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -32,8 +32,8 @@ import Spinner from '../elements/Spinner'; import Map from '../location/Map'; import SmartMarker from '../location/SmartMarker'; import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; -import { IBodyProps } from "./IBodyProps"; import BeaconViewDialog from '../beacon/BeaconViewDialog'; +import { IBodyProps } from "./IBodyProps"; const useBeaconState = (beaconInfoEvent: MatrixEvent): { beacon?: Beacon; @@ -105,6 +105,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => { roomId: mxEvent.getRoomId(), matrixClient, + focusBeacon: beacon, }, "mx_BeaconViewDialog_wrapper", false, // isPriority @@ -151,6 +152,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => beacon={beacon} displayStatus={displayStatus} label={_t('View live location')} + withIcon /> }
    diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 2bafadf5178..d52629b56d6 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -26,7 +26,6 @@ import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; import UnknownBody from "./UnknownBody"; import { IMediaBody } from "./IMediaBody"; -import { IOperableEventTile } from "../context_menus/MessageContextMenu"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { ReactAnyComponent } from "../../../@types/common"; import { IBodyProps } from "./IBodyProps"; @@ -41,6 +40,7 @@ import MPollBody from "./MPollBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; +import { IEventTileOps } from "../rooms/EventTile"; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -54,6 +54,10 @@ interface IProps extends Omit implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper: MediaEventHelper; diff --git a/src/components/views/messages/UnknownBody.tsx b/src/components/views/messages/UnknownBody.tsx index d9e70ff241f..cd1f06a788b 100644 --- a/src/components/views/messages/UnknownBody.tsx +++ b/src/components/views/messages/UnknownBody.tsx @@ -23,12 +23,12 @@ interface IProps { children?: React.ReactNode; } -export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject) => { +export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject) => { const text = mxEvent.getContent().body; return ( - +
    { text } { children } - +
    ); }); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index a5dcf038133..e93119643fb 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -103,6 +103,7 @@ interface IProps { } interface IState { + useMarkdown: boolean; showPillAvatar: boolean; query?: string; showVisualBell?: boolean; @@ -124,6 +125,7 @@ export default class BasicMessageEditor extends React.Component private lastCaret: DocumentOffset; private lastSelection: ReturnType; + private readonly useMarkdownHandle: string; private readonly emoticonSettingHandle: string; private readonly shouldShowPillAvatarSettingHandle: string; private readonly surroundWithHandle: string; @@ -133,10 +135,13 @@ export default class BasicMessageEditor extends React.Component super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"), showVisualBell: false, }; + this.useMarkdownHandle = SettingsStore.watchSetting('MessageComposerInput.useMarkdown', null, + this.configureUseMarkdown); this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, this.configureEmoticonAutoReplace); this.configureEmoticonAutoReplace(); @@ -442,7 +447,7 @@ export default class BasicMessageEditor extends React.Component } } else if (!selection.isCollapsed && !isEmpty) { this.hasTextSelected = true; - if (this.formatBarRef.current) { + if (this.formatBarRef.current && this.state.useMarkdown) { const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); this.formatBarRef.current.showAt(selectionRect); } @@ -630,6 +635,14 @@ export default class BasicMessageEditor extends React.Component this.setState({ completionIndex }); }; + private configureUseMarkdown = (): void => { + const useMarkdown = SettingsStore.getValue("MessageComposerInput.useMarkdown"); + this.setState({ useMarkdown }); + if (!useMarkdown && this.formatBarRef.current) { + this.formatBarRef.current.hide(); + } + }; + private configureEmoticonAutoReplace = (): void => { this.props.model.setTransformCallback(this.transform); }; @@ -654,6 +667,7 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.removeEventListener("input", this.onInput, true); this.editorRef.current.removeEventListener("compositionstart", this.onCompositionStart, true); this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true); + SettingsStore.unwatchSetting(this.useMarkdownHandle); SettingsStore.unwatchSetting(this.emoticonSettingHandle); SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle); SettingsStore.unwatchSetting(this.surroundWithHandle); @@ -694,6 +708,10 @@ export default class BasicMessageEditor extends React.Component } public onFormatAction = (action: Formatting): void => { + if (!this.state.useMarkdown) { + return; + } + const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); this.historyManager.ensureLastChangesPushed(this.props.model); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index ce6d1b844e0..de1bdc9c85b 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -95,7 +95,10 @@ function createEditContent( body: `${plainPrefix} * ${body}`, }; - const formattedBody = htmlSerializeIfNeeded(model, { forceHTML: isReply }); + const formattedBody = htmlSerializeIfNeeded(model, { + forceHTML: isReply, + useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), + }); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; @@ -404,7 +407,9 @@ class EditMessageComposer extends React.Component { if (protocol.avatar_url) { const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64); - networkIcon = { url={avatarUrl} />; } else { - networkIcon =
    ; + networkIcon =
    ; } let networkItem = null; if (network) { @@ -146,19 +146,19 @@ export default class BridgeTile extends React.PureComponent { } const id = this.props.ev.getId(); - return (
  • -
    + return (
  • +
    { networkIcon }
    -
    -

    { protocolName }

    -

    +

    +

    { protocolName }

    +

    { networkItem } - { _t("Channel: ", {}, { + { _t("Channel: ", {}, { channelLink: () => channelLink, }) }

    -
      +
        { creator } { bot }
    diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index a7a54da66e0..8b3bdeb7fcf 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -63,6 +63,7 @@ export default class PreferencesUserSettingsTab extends React.Component { size: Size; } diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index 88351c20d84..84bc470273e 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -178,7 +178,7 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { facePile =
    { _t("%(count)s people connected", { count: connectedMembers.length }) } - +
    ; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index 1fbccf45fff..57ef52cafd3 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -32,7 +32,7 @@ function escape(text: string): string { // Finds the length of the longest backtick sequence in the given text, used for // escaping backticks in code blocks -function longestBacktickSequence(text: string): number { +export function longestBacktickSequence(text: string): number { let length = 0; let currentLength = 0; @@ -52,12 +52,12 @@ function isListChild(n: Node): boolean { return LIST_TYPES.includes(n.parentNode?.nodeName); } -function parseAtRoomMentions(text: string, pc: PartCreator, shouldEscape = true): Part[] { +function parseAtRoomMentions(text: string, pc: PartCreator, opts: IParseOptions): Part[] { const ATROOM = "@room"; const parts: Part[] = []; text.split(ATROOM).forEach((textPart, i, arr) => { if (textPart.length) { - parts.push(...pc.plainWithEmoji(shouldEscape ? escape(textPart) : textPart)); + parts.push(...pc.plainWithEmoji(opts.shouldEscape ? escape(textPart) : textPart)); } // it's safe to never append @room after the last textPart // as split will report an empty string at the end if @@ -70,7 +70,7 @@ function parseAtRoomMentions(text: string, pc: PartCreator, shouldEscape = true) return parts; } -function parseLink(n: Node, pc: PartCreator): Part[] { +function parseLink(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const { href } = n as HTMLAnchorElement; const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID @@ -81,18 +81,18 @@ function parseLink(n: Node, pc: PartCreator): Part[] { const children = Array.from(n.childNodes); if (href === n.textContent && children.every(c => c.nodeType === Node.TEXT_NODE)) { - return parseAtRoomMentions(n.textContent, pc); + return parseAtRoomMentions(n.textContent, pc, opts); } else { - return [pc.plain("["), ...parseChildren(n, pc), pc.plain(`](${href})`)]; + return [pc.plain("["), ...parseChildren(n, pc, opts), pc.plain(`](${href})`)]; } } -function parseImage(n: Node, pc: PartCreator): Part[] { +function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const { alt, src } = n as HTMLImageElement; return pc.plainWithEmoji(`![${escape(alt)}](${src})`); } -function parseCodeBlock(n: Node, pc: PartCreator): Part[] { +function parseCodeBlock(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { let language = ""; if (n.firstChild?.nodeName === "CODE") { for (const className of (n.firstChild as HTMLElement).classList) { @@ -117,10 +117,10 @@ function parseCodeBlock(n: Node, pc: PartCreator): Part[] { return parts; } -function parseHeader(n: Node, pc: PartCreator): Part[] { +function parseHeader(n: Node, pc: PartCreator, opts: IParseOptions): Part[] { const depth = parseInt(n.nodeName.slice(1), 10); const prefix = pc.plain("#".repeat(depth) + " "); - return [prefix, ...parseChildren(n, pc)]; + return [prefix, ...parseChildren(n, pc, opts)]; } function checkIgnored(n) { @@ -144,10 +144,10 @@ function prefixLines(parts: Part[], prefix: string, pc: PartCreator) { } } -function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] { +function parseChildren(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] { let prev; return Array.from(n.childNodes).flatMap(c => { - const parsed = parseNode(c, pc, mkListItem); + const parsed = parseNode(c, pc, opts, mkListItem); if (parsed.length && prev && (checkBlockNode(prev) || checkBlockNode(c))) { if (isListChild(c)) { // Use tighter spacing within lists @@ -161,12 +161,12 @@ function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part }); } -function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] { +function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] { if (checkIgnored(n)) return []; switch (n.nodeType) { case Node.TEXT_NODE: - return parseAtRoomMentions(n.nodeValue, pc); + return parseAtRoomMentions(n.nodeValue, pc, opts); case Node.ELEMENT_NODE: switch (n.nodeName) { case "H1": @@ -175,43 +175,43 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): case "H4": case "H5": case "H6": - return parseHeader(n, pc); + return parseHeader(n, pc, opts); case "A": - return parseLink(n, pc); + return parseLink(n, pc, opts); case "IMG": - return parseImage(n, pc); + return parseImage(n, pc, opts); case "BR": return [pc.newline()]; case "HR": return [pc.plain("---")]; case "EM": - return [pc.plain("_"), ...parseChildren(n, pc), pc.plain("_")]; + return [pc.plain("_"), ...parseChildren(n, pc, opts), pc.plain("_")]; case "STRONG": - return [pc.plain("**"), ...parseChildren(n, pc), pc.plain("**")]; + return [pc.plain("**"), ...parseChildren(n, pc, opts), pc.plain("**")]; case "DEL": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "SUB": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "SUP": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "U": - return [pc.plain(""), ...parseChildren(n, pc), pc.plain("")]; + return [pc.plain(""), ...parseChildren(n, pc, opts), pc.plain("")]; case "PRE": - return parseCodeBlock(n, pc); + return parseCodeBlock(n, pc, opts); case "CODE": { // Escape backticks by using multiple backticks for the fence if necessary const fence = "`".repeat(longestBacktickSequence(n.textContent) + 1); return pc.plainWithEmoji(`${fence}${n.textContent}${fence}`); } case "BLOCKQUOTE": { - const parts = parseChildren(n, pc); + const parts = parseChildren(n, pc, opts); prefixLines(parts, "> ", pc); return parts; } case "LI": - return mkListItem?.(n) ?? parseChildren(n, pc); + return mkListItem?.(n) ?? parseChildren(n, pc, opts); case "UL": { - const parts = parseChildren(n, pc, li => [pc.plain("- "), ...parseChildren(li, pc)]); + const parts = parseChildren(n, pc, opts, li => [pc.plain("- "), ...parseChildren(li, pc, opts)]); if (isListChild(n)) { prefixLines(parts, " ", pc); } @@ -219,8 +219,8 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): } case "OL": { let counter = (n as HTMLOListElement).start ?? 1; - const parts = parseChildren(n, pc, li => { - const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc)]; + const parts = parseChildren(n, pc, opts, li => { + const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc, opts)]; counter++; return parts; }); @@ -247,15 +247,20 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): } } - return parseChildren(n, pc); + return parseChildren(n, pc, opts); } -function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolean): Part[] { +interface IParseOptions { + isQuotedMessage?: boolean; + shouldEscape?: boolean; +} + +function parseHtmlMessage(html: string, pc: PartCreator, opts: IParseOptions): Part[] { // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine - const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc); - if (isQuotedMessage) { + const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc, opts); + if (opts.isQuotedMessage) { prefixLines(parts, "> ", pc); } return parts; @@ -264,14 +269,14 @@ function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolea export function parsePlainTextMessage( body: string, pc: PartCreator, - opts: { isQuotedMessage?: boolean, shouldEscape?: boolean }, + opts: IParseOptions, ): Part[] { const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n return lines.reduce((parts, line, i) => { if (opts.isQuotedMessage) { parts.push(pc.plain("> ")); } - parts.push(...parseAtRoomMentions(line, pc, opts.shouldEscape)); + parts.push(...parseAtRoomMentions(line, pc, opts)); const isLast = i === lines.length - 1; if (!isLast) { parts.push(pc.newline()); @@ -280,19 +285,19 @@ export function parsePlainTextMessage( }, [] as Part[]); } -export function parseEvent(event: MatrixEvent, pc: PartCreator, { isQuotedMessage = false } = {}) { +export function parseEvent(event: MatrixEvent, pc: PartCreator, opts: IParseOptions = { shouldEscape: true }) { const content = event.getContent(); let parts: Part[]; const isEmote = content.msgtype === "m.emote"; let isRainbow = false; if (content.format === "org.matrix.custom.html") { - parts = parseHtmlMessage(content.formatted_body || "", pc, isQuotedMessage); + parts = parseHtmlMessage(content.formatted_body || "", pc, opts); if (content.body && content.formatted_body && textToHtmlRainbow(content.body) === content.formatted_body) { isRainbow = true; } } else { - parts = parsePlainTextMessage(content.body || "", pc, { isQuotedMessage }); + parts = parsePlainTextMessage(content.body || "", pc, opts); } if (isEmote && isRainbow) { diff --git a/src/editor/operations.ts b/src/editor/operations.ts index 6129681815c..40a438cc562 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -17,6 +17,7 @@ limitations under the License. import Range from "./range"; import { Part, Type } from "./parts"; import { Formatting } from "../components/views/rooms/MessageComposerFormatBar"; +import { longestBacktickSequence } from './deserialize'; /** * Some common queries and transformations on the editor model @@ -181,12 +182,12 @@ export function formatRangeAsCode(range: Range): void { const hasBlockFormatting = (range.length > 0) && range.text.startsWith("```") - && range.text.endsWith("```"); + && range.text.endsWith("```") + && range.text.includes('\n'); const needsBlockFormatting = parts.some(p => p.type === Type.Newline); if (hasBlockFormatting) { - // Remove previously pushed backticks and new lines parts.shift(); parts.pop(); if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") { @@ -205,7 +206,10 @@ export function formatRangeAsCode(range: Range): void { parts.push(partCreator.newline()); } } else { - toggleInlineFormat(range, "`"); + const fenceLen = longestBacktickSequence(range.text); + const hasInlineFormatting = range.text.startsWith("`") && range.text.endsWith("`"); + //if it's already formatted untoggle based on fenceLen which returns the max. num of backtick within a text else increase the fence backticks with a factor of 1. + toggleInlineFormat(range, "`".repeat(hasInlineFormatting ? fenceLen : fenceLen + 1)); return; } @@ -240,6 +244,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix // compute paragraph [start, end] indexes const paragraphIndexes = []; let startIndex = 0; + // start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end for (let i = 2; i < parts.length; i++) { // paragraph breaks can be denoted in a multitude of ways, diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 8e0d3d66db9..7c4d62e9ab5 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -17,6 +17,7 @@ limitations under the License. import { AllHtmlEntities } from 'html-entities'; import cheerio from 'cheerio'; +import escapeHtml from "escape-html"; import Markdown from '../Markdown'; import { makeGenericPermalink } from "../utils/permalinks/Permalinks"; @@ -48,7 +49,19 @@ export function mdSerialize(model: EditorModel): string { }, ""); } -export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string { +interface ISerializeOpts { + forceHTML?: boolean; + useMarkdown?: boolean; +} + +export function htmlSerializeIfNeeded( + model: EditorModel, + { forceHTML = false, useMarkdown = true }: ISerializeOpts = {}, +): string { + if (!useMarkdown) { + return escapeHtml(textSerialize(model)).replace(/\n/g, '
    '); + } + let md = mdSerialize(model); // copy of raw input to remove unwanted math later const orig = md; diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 18bf7ac891b..d9246337e8b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -110,7 +110,7 @@ "Failed to reject invite": "Nepodařilo se odmĆ­tnout pozvĆ”nku", "Failed to send request.": "OdeslĆ”nĆ­ Å¾Ć”dosti se nezdařilo.", "Failed to set display name": "Nepodařilo se nastavit zobrazovanĆ© jmĆ©no", - "Failed to unban": "PřijetĆ­ zpět se nezdařilo", + "Failed to unban": "ZruÅ”enĆ­ vykĆ”zĆ”nĆ­ se nezdařilo", "Failed to upload profile picture!": "NahrĆ”nĆ­ profilovĆ©ho obrĆ”zku se nezdařilo!", "Failure to create room": "VytvořenĆ­ mĆ­stnosti se nezdařilo", "Forget room": "Zapomenout mĆ­stnost", @@ -208,7 +208,7 @@ "Unable to create widget.": "Nepodařilo se vytvořit widget.", "Unable to remove contact information": "Nepodařilo se smazat kontaktnĆ­ Ćŗdaje", "Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.", - "Unban": "Přijmout zpět", + "Unban": "ZruÅ”it vykĆ”zĆ”nĆ­", "Unable to enable Notifications": "Nepodařilo se povolit oznĆ”menĆ­", "Unmute": "Povolit", "Unnamed Room": "NepojmenovanĆ” mĆ­stnost", @@ -408,10 +408,10 @@ "were banned %(count)s times|one": "byl(a) vykĆ”zĆ”n(a)", "was banned %(count)s times|other": "byli %(count)s krĆ”t vykĆ”zĆ”ni", "was banned %(count)s times|one": "byl(a) vykĆ”zĆ”n(a)", - "were unbanned %(count)s times|other": "byli %(count)s přijati zpět", - "were unbanned %(count)s times|one": "byl(a) přijat(a) zpět", - "was unbanned %(count)s times|other": "byli %(count)s krĆ”t přijati zpět", - "was unbanned %(count)s times|one": "byl(a) přijat(a) zpět", + "were unbanned %(count)s times|other": "měli %(count)s krĆ”t zruÅ”eno vykĆ”zĆ”nĆ­", + "were unbanned %(count)s times|one": "měli zruÅ”eno vykĆ”zĆ”nĆ­", + "was unbanned %(count)s times|other": "měl(a) %(count)s krĆ”t zruÅ”eno vykĆ”zĆ”nĆ­", + "was unbanned %(count)s times|one": "mĆ” zruÅ”eno vykĆ”zĆ”nĆ­", "were kicked %(count)s times|other": "byli %(count)s krĆ”t vyhozeni", "were kicked %(count)s times|one": "byli vyhozeni", "was kicked %(count)s times|other": "byl %(count)s krĆ”t vyhozen", @@ -1016,7 +1016,7 @@ "Name or Matrix ID": "JmĆ©no nebo Matrix ID", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.": "VarovĆ”nĆ­: Upgrade mĆ­stnosti automaticky převede vÅ”echny členy na novou verzi mĆ­stnosti. Do starĆ© mĆ­stnosti poÅ”leme odkaz na novou mĆ­stnost - vÅ”ichni členovĆ© na něj budou muset klepnout, aby se přidali do novĆ© mĆ­stnosti.", "Changes your avatar in this current room only": "ZměnĆ­ vĆ”Å” avatar jen v tĆ©to mĆ­stnosti", - "Unbans user with given ID": "Přijmout zpět uživatele s danĆ½m identifikĆ”torem", + "Unbans user with given ID": "ZruÅ”Ć­ vykĆ”zĆ”nĆ­ uživatele s danĆ½m identifikĆ”torem", "Adds a custom widget by URL to the room": "PřidĆ” do mĆ­stnosti vlastnĆ­ widget podle adresy URL", "Please supply a https:// or http:// widget URL": "Zadejte webovou adresu widgetu (začƭnajĆ­cĆ­ na https:// nebo http://)", "You cannot modify widgets in this room.": "V tĆ©to mĆ­stnosti nemÅÆžete manipulovat s widgety.", @@ -2908,7 +2908,7 @@ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s vykopl(a) uživatele %(targetName)s: %(reason)s", "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s zruÅ”il(a) pozvĆ”nĆ­ pro uživatele %(targetName)s", "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s zruÅ”il(a) pozvĆ”nĆ­ pro uživatele %(targetName)s: %(reason)s", - "%(senderName)s unbanned %(targetName)s": "%(senderName)s přijal(a) zpět uživatele %(targetName)s", + "%(senderName)s unbanned %(targetName)s": "%(senderName)s zruÅ”il(a) vykĆ”zĆ”nĆ­ uživatele %(targetName)s", "%(targetName)s left the room": "%(targetName)s opustil(a) mĆ­stnost", "%(targetName)s left the room: %(reason)s": "%(targetName)s opustil(a) mĆ­stnost: %(reason)s", "%(targetName)s rejected the invitation": "%(targetName)s odmĆ­tl(a) pozvĆ”nĆ­", @@ -3227,13 +3227,13 @@ "Shows all threads youā€™ve participated in": "ZobrazĆ­ vÅ”echna vlĆ”kna, kterĆ½ch jste se zĆŗčastnili", "My threads": "Moje vlĆ”kna", "They won't be able to access whatever you're not an admin of.": "Nebudou mĆ­t pÅ™Ć­stup ke vÅ”emu, čeho nejste sprĆ”vcem.", - "Unban them from specific things I'm able to": "Přijmout je zpět do konkrĆ©tnĆ­ch mĆ­st, do kterĆ½ch jsem schopen", - "Unban them from everything I'm able to": "Přijmout je zpět vÅ”ude, kam mohu", + "Unban them from specific things I'm able to": "ZruÅ”it jejich vykĆ”zĆ”nĆ­ z konkrĆ©tnĆ­ch mĆ­st, kde mĆ”m oprĆ”vněnĆ­", + "Unban them from everything I'm able to": "ZruÅ”it jejich vykĆ”zĆ”nĆ­ vÅ”ude, kde mĆ”m oprĆ”vněnĆ­", "Ban them from specific things I'm able to": "VykĆ”zat je z konkrĆ©tnĆ­ch mĆ­st, ze kterĆ½ch jsem schopen", "Kick them from specific things I'm able to": "Vykopnout je z konkrĆ©tnĆ­ch mĆ­st, ze kterĆ½ch jsem schopen", "Ban them from everything I'm able to": "VykĆ”zat je vÅ”ude, kde mohu", "Ban from %(roomName)s": "VykĆ”zat z %(roomName)s", - "Unban from %(roomName)s": "Přijmout zpět do %(roomName)s", + "Unban from %(roomName)s": "ZruÅ”it vykĆ”zĆ”nĆ­ z %(roomName)s", "They'll still be able to access whatever you're not an admin of.": "StĆ”le budou mĆ­t pÅ™Ć­stup ke vÅ”emu, čeho nejste sprĆ”vcem.", "Kick them from everything I'm able to": "Vykopnout je ze vÅ”eho, co to jde", "Kick from %(roomName)s": "Vykopnout z %(roomName)s", @@ -3830,5 +3830,43 @@ "%(count)s participants|other": "%(count)s ĆŗčastnĆ­kÅÆ", "New video room": "NovĆ” video mĆ­stnost", "New room": "NovĆ” mĆ­stnost", - "Video rooms (under active development)": "Video mĆ­stnosti (v aktivnĆ­m vĆ½voji)" + "Video rooms (under active development)": "Video mĆ­stnosti (v aktivnĆ­m vĆ½voji)", + "Give feedback": "Poskytnout zpětnou vazbu", + "%(featureName)s Beta feedback": "ZpětnĆ” vazba beta funkce %(featureName)s", + "Beta feature. Click to learn more.": "Beta funkce. KliknutĆ­m zĆ­skĆ”te dalÅ”Ć­ informace.", + "Beta feature": "Beta funkce", + "Threads are a beta feature": "VlĆ”kna jsou beta funkcĆ­", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Při najetĆ­ na zprĆ”vu použijte možnost \"Odpovědět ve vlĆ”kně\".", + "Threads help keep your conversations on-topic and easy to track.": "VlĆ”kna pomĆ”hajĆ­ udržovat konverzace k tĆ©matu a snadno je sledovat.", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Chcete-li odejĆ­t, vraÅ„te se na tuto strĆ”nku a použijte tlačƭtko \"Opustit beta verzi\".", + "Use \"Reply in thread\" when hovering over a message.": "Po najetĆ­ na zprĆ”vu použijte možnost \"Odpovědět ve vlĆ”kně\".", + "How can I start a thread?": "Jak mohu založit vlĆ”kno?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "VlĆ”kna pomĆ”hajĆ­ udržovat konverzace k tĆ©matu a snadno je sledovat. DalÅ”Ć­ informace.", + "Keep discussions organised with threads.": "Diskuse udržovat organizovanĆ© pomocĆ­ vlĆ”ken.", + "sends hearts": "posĆ­lĆ” srdƭčka", + "Sends the given message with hearts": "OdeÅ”le danou zprĆ”vu se srdƭčky", + "Confirm signing out these devices|one": "Potvrďte odhlĆ”Å”enĆ­ z tohoto zaÅ™Ć­zenĆ­", + "Confirm signing out these devices|other": "Potvrďte odhlĆ”Å”enĆ­ z těchto zaÅ™Ć­zenĆ­", + "Live location ended": "SdĆ­lenĆ­ polohy živě skončilo", + "Loading live location...": "NačƭtĆ”nĆ­ polohy živě...", + "View live location": "Zobrazit polohu živě", + "Live location enabled": "Poloha živě povolena", + "Live location error": "Chyba polohy živě", + "Live until %(expiryTime)s": "Živě do %(expiryTime)s", + "Yes, enable": "Ano, povolit", + "Do you want to enable threads anyway?": "Chcete i přesto vlĆ”kna povolit?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "VĆ”Å” domovskĆ½ server v současnĆ© době nepodporuje vlĆ”kna, takže tato funkce mÅÆže bĆ½t nespolehlivĆ”. NěkterĆ© zprĆ”vy ve vlĆ”knech nemusĆ­ bĆ½t spolehlivě dostupnĆ©. Dozvědět se vĆ­ce.", + "Partial Support for Threads": "ÄŒĆ”stečnĆ” podpora vlĆ”ken", + "Ban from room": "VykĆ”zat z mĆ­stnosti", + "Unban from room": "ZruÅ”it vykĆ”zĆ”nĆ­ z mĆ­stnosti", + "Ban from space": "VykĆ”zat z prostoru", + "Unban from space": "ZruÅ”it vykĆ”zĆ”nĆ­ z prostoru", + "Jump to the given date in the timeline": "PřejĆ­t na zadanĆ© datum na časovĆ© ose", + "Right-click message context menu": "Klikněte pravĆ½m tlačƭtkem pro zobrazenĆ­ kontextovĆ© nabĆ­dky", + "Remove from space": "Odebrat z prostoru", + "Disinvite from room": "ZruÅ”it pozvĆ”nku do mĆ­stnosti", + "Disinvite from space": "ZruÅ”it pozvĆ”nku do prostoru", + "Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Tip: Použijte \"%(replyInThread)s\" při najetĆ­ na zprĆ”vu.", + "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "Chcete-li odejĆ­t, vraÅ„te se na tuto strĆ”nku a použijte tlačƭtko \"%(leaveTheBeta)s\".", + "Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Použijte \"%(replyInThread)s\" při najetĆ­ na zprĆ”vu." } diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index bcd0a8129fe..ee84cf5f12d 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -2129,7 +2129,7 @@ "Collapse reply thread": "Ī£ĻĪ¼Ļ€Ļ„Ļ…Ī¾Ī· Ī½Ī®Ī¼Ī±Ļ„ĪæĻ‚ Ī±Ļ€Ī¬Ī½Ļ„Ī·ĻƒĪ·Ļ‚", "Show preview": "Ī•Ī¼Ļ†Ī¬Ī½Ī¹ĻƒĪ· Ļ€ĻĪæĪµĻ€Ī¹ĻƒĪŗĻŒĻ€Ī·ĻƒĪ·Ļ‚", "View source": "Ī ĻĪæĪ²ĪæĪ»Ī® Ļ€Ī·Ī³Ī®Ļ‚", - "Forward": "Ī•Ī¼Ļ€ĻĻŒĻ‚", + "Forward": "Ī ĻĪæĻŽĪøĪ·ĻƒĪ·", "Open in OpenStreetMap": "Ī†Ī½ĪæĪ¹Ī³Ī¼Ī± ĻƒĻ„Īæ OpenStreetMap", "Not a valid Security Key": "ĪœĪ· Ī­Ī³ĪŗĻ…ĻĪæ ĪšĪ»ĪµĪ¹Ī“ĪÆ Ī‘ĻƒĻ†Ī±Ī»ĪµĪÆĪ±Ļ‚", "This looks like a valid Security Key!": "Ī‘Ļ…Ļ„ĻŒ Ļ†Ī±ĪÆĪ½ĪµĻ„Ī±Ī¹ Ī½Ī± ĪµĪÆĪ½Ī±Ī¹ Ī­Ī½Ī± Ī­Ī³ĪŗĻ…ĻĪæ ĪšĪ»ĪµĪ¹Ī“ĪÆ Ī‘ĻƒĻ†Ī±Ī»ĪµĪÆĪ±Ļ‚!", @@ -3443,5 +3443,11 @@ "An error occured whilst sharing your live location": "Ī Ī±ĻĪæĻ…ĻƒĪ¹Ī¬ĻƒĻ„Ī·ĪŗĪµ ĻƒĻ†Ī¬Ī»Ī¼Ī± ĪŗĪ±Ļ„Ī¬ Ļ„Ī·Ī½ ĪŗĪæĪ¹Ī½Ī® Ļ‡ĻĪ®ĻƒĪ· Ļ„Ī·Ļ‚ Ļ„ĻĪ­Ļ‡ĪæĻ…ĻƒĪ±Ļ‚ Ļ„ĪæĻ€ĪæĪøĪµĻƒĪÆĪ±Ļ‚ ĻƒĪ±Ļ‚", "Force complete": "Ī•Ī¾Ī±Ī½Ī±Ī³ĪŗĪ±ĻƒĪ¼ĻŒĻ‚ ĪæĪ»ĪæĪŗĪ»Ī®ĻĻ‰ĻƒĪ·Ļ‚", "Close dialog or context menu": "ĪšĪ»ĪµĪÆĻƒĪ¹Ī¼Īæ Ī“Ī¹Ī±Ī»ĻŒĪ³ĪæĻ… Ī® Ī¼ĪµĪ½ĪæĻ Ļ€ĪµĻĪ¹Ī²Ī¬Ī»Ī»ĪæĪ½Ļ„ĪæĻ‚", - "Stop sharing and close": "Ī£Ļ„Ī±Ī¼Ī±Ļ„Ī®ĻƒĻ„Īµ Ļ„Ī·Ī½ ĪŗĪæĪ¹Ī½Ī® Ļ‡ĻĪ®ĻƒĪ· ĪŗĪ±Ī¹ ĪŗĪ»ĪµĪÆĻƒĻ„Īµ" + "Stop sharing and close": "Ī£Ļ„Ī±Ī¼Ī±Ļ„Ī®ĻƒĻ„Īµ Ļ„Ī·Ī½ ĪŗĪæĪ¹Ī½Ī® Ļ‡ĻĪ®ĻƒĪ· ĪŗĪ±Ī¹ ĪŗĪ»ĪµĪÆĻƒĻ„Īµ", + "Video rooms (under active development)": "Ī’ĪÆĪ½Ļ„ĪµĪæ Ī“Ļ‰Ī¼Ī¬Ļ„Ī¹Ī± (Ļ…Ļ€ĻŒ Ī±Ī½Ī¬Ļ€Ļ„Ļ…Ī¾Ī·)", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Ī“Ī¹Ī± Ī½Ī± Ī±Ļ€ĪæĻ‡Ļ‰ĻĪ®ĻƒĪµĻ„Īµ, ĪµĻ€Ī¹ĻƒĻ„ĻĪ­ĻˆĻ„Īµ ĻƒĪµ Ī±Ļ…Ļ„Ī®Ī½ Ļ„Ī· ĻƒĪµĪ»ĪÆĪ“Ī± ĪŗĪ±Ī¹ Ļ‡ĻĪ·ĻƒĪ¹Ī¼ĪæĻ€ĪæĪ¹Ī®ĻƒĻ„Īµ Ļ„Īæ ĪŗĪæĻ…Ī¼Ļ€ĪÆ \"Ī‘Ļ€ĪæĻ‡ĻŽĻĪ·ĻƒĪ· Ī±Ļ€ĻŒ Ļ„Ī·Ī½ Ī­ĪŗĪ“ĪæĻƒĪ· beta\".", + "Use \"Reply in thread\" when hovering over a message.": "Ī§ĻĪ·ĻƒĪ¹Ī¼ĪæĻ€ĪæĪ¹Ī®ĻƒĻ„Īµ Ļ„Ī·Ī½ \"Ī‘Ļ€Ī¬Ī½Ļ„Ī·ĻƒĪ· ĻƒĻ„Īæ Ī½Ī®Ī¼Ī±\" ĻŒĻ„Ī±Ī½ Ļ„ĪæĻ€ĪæĪøĪµĻ„ĪµĪÆĻ„Īµ Ļ„Īæ Ī“ĪµĪÆĪŗĻ„Ī· Ļ„ĪæĻ… Ļ€ĪæĪ½Ļ„Ī¹ĪŗĪ¹ĪæĻ Ļ€Ī¬Ī½Ļ‰ Ī±Ļ€ĻŒ Ī­Ī½Ī± Ī¼Ī®Ī½Ļ…Ī¼Ī±.", + "How can I start a thread?": "Ī ĻŽĻ‚ Ī¼Ļ€ĪæĻĻŽ Ī½Ī± Ī¾ĪµĪŗĪ¹Ī½Ī®ĻƒĻ‰ Ī­Ī½Ī± Ī½Ī®Ī¼Ī±;", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Ī¤Ī± Ī½Ī®Ī¼Ī±Ļ„Ī± Ī²ĪæĪ·ĪøĪæĻĪ½ ĻƒĻ„Ī·Ī½ ĪŗĪ±Ī»ĻĻ„ĪµĻĪ· ĪæĻĪ³Ī¬Ī½Ļ‰ĻƒĪ· Ļ„Ļ‰Ī½ ĻƒĻ…Ī¶Ī·Ļ„Ī®ĻƒĪµĻ‰Ī½ ĪŗĪ±Ī¹ ĻƒĻ„Ī·Ī½ ĪµĻĪŗĪæĪ»Ī· Ļ€Ī±ĻĪ±ĪŗĪæĪ»ĪæĻĪøĪ·ĻƒĪ·. ĪœĪ¬ĪøĪµĻ„Īµ Ļ€ĪµĻĪ¹ĻƒĻƒĻŒĻ„ĪµĻĪ±.", + "Keep discussions organised with threads.": "Ī”Ī¹Ī±Ļ„Ī·ĻĪ®ĻƒĻ„Īµ Ļ„Ī¹Ļ‚ ĻƒĻ…Ī¶Ī·Ļ„Ī®ĻƒĪµĪ¹Ļ‚ ĪæĻĪ³Ī±Ī½Ļ‰Ī¼Ī­Ī½ĪµĻ‚ ĻƒĪµ Ī½Ī®Ī¼Ī±Ļ„Ī±." } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5a674deb1fc..6c8a1a0a4fb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -870,9 +870,11 @@ "Keep discussions organised with threads.": "Keep discussions organised with threads.", "Threads help keep conversations on-topic and easy to track. Learn more.": "Threads help keep conversations on-topic and easy to track. Learn more.", "How can I start a thread?": "How can I start a thread?", - "Use \"Reply in thread\" when hovering over a message.": "Use \"Reply in thread\" when hovering over a message.", + "Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Use ā€œ%(replyInThread)sā€ when hovering over a message.", + "Reply in thread": "Reply in thread", "How can I leave the beta?": "How can I leave the beta?", - "To leave, return to this page and use the ā€œLeave the betaā€ button.": "To leave, return to this page and use the ā€œLeave the betaā€ button.", + "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.", + "Leave the beta": "Leave the beta", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "Custom user status messages": "Custom user status messages", "Video rooms (under active development)": "Video rooms (under active development)", @@ -930,6 +932,8 @@ "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Surround selected text when typing special characters": "Surround selected text when typing special characters", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", + "Enable Markdown": "Enable Markdown", + "Start messages with /plain to send without markdown and /md to send with.": "Start messages with /plain to send without markdown and /md to send with.", "Mirror local video feed": "Mirror local video feed", "Match system theme": "Match system theme", "Use a system font": "Use a system font", @@ -2100,7 +2104,6 @@ "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", - "Reply in thread": "Reply in thread", "Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation", "Beta feature": "Beta feature", "Beta feature. Click to learn more.": "Beta feature. Click to learn more.", @@ -2919,12 +2922,16 @@ "This is a beta feature": "This is a beta feature", "Click for more info": "Click for more info", "Beta": "Beta", - "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", + "Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s", "Live until %(expiryTime)s": "Live until %(expiryTime)s", "Loading live location...": "Loading live location...", "Live location ended": "Live location ended", "Live location error": "Live location error", + "No live locations": "No live locations", + "View list": "View list", + "View List": "View List", + "Close sidebar": "Close sidebar", "An error occured whilst sharing your live location": "An error occured whilst sharing your live location", "You are sharing your live location": "You are sharing your live location", "%(timeRemaining)s left": "%(timeRemaining)s left", @@ -3134,7 +3141,7 @@ "Reply to an ongoing thread or use ā€œ%(replyInThread)sā€ when hovering over a message to start a new one.": "Reply to an ongoing thread or use ā€œ%(replyInThread)sā€ when hovering over a message to start a new one.", "Show all threads": "Show all threads", "Threads help keep your conversations on-topic and easy to track.": "Threads help keep your conversations on-topic and easy to track.", - "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Use \"Reply in thread\" when hovering over a message.", + "Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.", "Keep discussions organised with threads": "Keep discussions organised with threads", "Threads are a beta feature": "Threads are a beta feature", "Give feedback": "Give feedback", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index d5d085d6abf..8f70a33ce06 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -19,7 +19,7 @@ "Change Password": "Cambiar la contraseƱa", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s cambiĆ³ el nivel de acceso de %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s cambiĆ³ el nombre de la sala a %(roomName)s.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s cambiĆ³ el asunto a \"%(topic)s\".", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s cambiĆ³ el asunto a Ā«%(topic)sĀ».", "Changes your display nickname": "Cambia tu apodo pĆŗblico", "Command error": "Error de comando", "Commands": "Comandos", @@ -3777,5 +3777,72 @@ "Send custom room account data event": "Enviar evento personalizado de cuenta de la sala", "Send custom timeline event": "Enviar evento personalizado de historial de mensajes", "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si no encuentras la sala que buscas, pide que te inviten a ella o crea una nueva.", - "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "AyĆŗdanos a identificar problemas y a mejorar %(analyticsOwner)s. Comparte datos anĆ³nimos sobre cĆ³mo usas la aplicaciĆ³n para que entendamos mejor cĆ³mo usa la gente varios dispositivos. Generaremos un identificador aleatorio que usarĆ”n todos tus dispositivos." + "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "AyĆŗdanos a identificar problemas y a mejorar %(analyticsOwner)s. Comparte datos anĆ³nimos sobre cĆ³mo usas la aplicaciĆ³n para que entendamos mejor cĆ³mo usa la gente varios dispositivos. Generaremos un identificador aleatorio que usarĆ”n todos tus dispositivos.", + "Give feedback": "Danos tu opiniĆ³n", + "Threads are a beta feature": "Los hilos son una funcionalidad beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Consejo: Usa Ā«Responder en un hiloĀ» al pasar el ratĆ³n sobre un mensaje.", + "Stop sharing and close": "Dejar de compartir y cerrar", + "Create room": "Crear sala", + "Create a video room": "Crear una sala de vĆ­deo", + "Create video room": "Crear sala de vĆ­deo", + "%(featureName)s Beta feedback": "Danos tu opiniĆ³n sobre la beta de %(featureName)s", + "Beta feature. Click to learn more.": "Funcionalidad beta. Haz clic para mĆ”s informaciĆ³n.", + "Beta feature": "Funcionalidad beta", + "%(count)s participants|one": "1 participante", + "%(count)s participants|other": "%(count)s participantes", + "Try again later, or ask a room or space admin to check if you have access.": "IntĆ©ntalo mĆ”s tarde, o pĆ­dele a alguien con permisos de administrador dentro de la sala o espacio que compruebe si tienes acceso.", + "This room or space is not accessible at this time.": "Esta sala o espacio no es accesible en este momento.", + "Are you sure you're at the right place?": "ĀæSeguro que estĆ”s en el sitio correcto?", + "This room or space does not exist.": "Esta sala o espacio no existe.", + "There's no preview, would you like to join?": "No hay previsualizaciĆ³n. ĀæTe quieres unir?", + "You can still join here.": "TodavĆ­a puedes unirte.", + "You were banned by %(memberName)s": "%(memberName)s te ha vetado", + "Forget this space": "Olvidar este espacio", + "You were removed by %(memberName)s": "%(memberName)s te ha sacado", + "Loading preview": "Cargando previsualizaciĆ³n", + "Joining ā€¦": "Entrandoā€¦", + "New video room": "Nueva sala de vĆ­deo", + "New room": "Nueva sala", + "View older version of %(spaceName)s.": "Ver versiĆ³n antigua de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Actualiza la versiĆ³n de este espacio a la recomendada", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Compartir ubicaciĆ³n en tiempo real: comparte tu ubicaciĆ³n actual (en desarrollo y, temporalmente, las ubicaciones persisten en el historial de la sala)", + "Video rooms (under active development)": "Salas de vĆ­deo (actualmente en desarrollo)", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Para salir, vuelve a esta pĆ”gina y usa el botĆ³n Ā«Salir de la betaĀ».", + "Use \"Reply in thread\" when hovering over a message.": "Usa Ā«Responder en hiloĀ» mientras pasas el ratĆ³n sobre un mensaje.", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Los hilos ayudan a mantener las conversaciones centradas en un Ćŗnico tema y las hace fĆ”ciles de seguir. MĆ”s informaciĆ³n.", + "How can I start a thread?": "ĀæCĆ³mo puedo empezar un hilo?", + "The person who invited you has already left.": "La persona que te invitĆ³ ya no estĆ” aquĆ­.", + "Sorry, your homeserver is too old to participate here.": "Lo siento, tu servidor base es demasiado antiguo. No puedes participar aquĆ­.", + "There was an error joining.": "Ha ocurrido un error al entrar.", + "The user's homeserver does not support the version of the space.": "El servidor base del usuario no es compatible con la versiĆ³n de este espacio.", + "User may or may not exist": "El usuario podrĆ­a no existir", + "User does not exist": "El usuario no existe", + "User is already in the room": "El usuario ya estĆ” en la sala", + "User is already in the space": "El usuario ya estĆ” en el espacio", + "User is already invited to the room": "El usuario ya estĆ” invitado a la sala", + "User is already invited to the space": "El usuario ya estĆ” invitado al espacio", + "Threads help keep your conversations on-topic and easy to track.": "Los hilos ayudan a mantener tus conversaciones centradas y a que sean fĆ”ciles de seguir.", + "Live location enabled": "UbicaciĆ³n en tiempo real activada", + "An error occured whilst sharing your live location": "Ha ocurrido un error al compartir tu ubicaciĆ³n en tiempo real", + "Live location error": "Error en la ubicaciĆ³n en tiempo real", + "Live location ended": "La ubicaciĆ³n en tiempo real ha terminado", + "Loading live location...": "Cargando ubicaciĆ³n en tiempo realā€¦", + "Live until %(expiryTime)s": "En directo hasta %(expiryTime)s", + "View live location": "Ver ubicaciĆ³n en tiempo real", + "Ban from room": "Vetar de la sala", + "Unban from room": "Dejar de vetar de la sala", + "Ban from space": "Vetar del espacio", + "Unban from space": "Dejar de vetar del espacio", + "Disinvite from room": "Retirar la invitaciĆ³n a la sala", + "Remove from space": "Quitar del espacio", + "Disinvite from space": "Retirar la invitaciĆ³n al espacio", + "Confirm signing out these devices|other": "Confirma el cierre de sesiĆ³n en estos dispositivos", + "Sends the given message with hearts": "EnvĆ­a corazones junto al mensaje", + "sends hearts": "envĆ­a corazones", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Tu servidor base no es compatible con los hilos en este momento, por lo que podrĆ­an funcionar de forma inestable. Algunos mensajes en los hilos podrĆ­an no estar disponibles de forma estable. MĆ”s informaciĆ³n.", + "Yes, enable": "SĆ­, activar", + "Do you want to enable threads anyway?": "ĀæQuieres activar los hilos de todos modos?", + "Partial Support for Threads": "Compatibilidad parcial con los hilos", + "Failed to join": "No ha sido posible unirse", + "You do not have permission to invite people to this space.": "No tienes permiso para invitar gente a este espacio." } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index fc9a5cf0fb5..52ee41160c1 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -364,7 +364,7 @@ "System Alerts": "SĆ¼steemi teated", "This room": "See jututuba", "Joining room ā€¦": "Liitun jututoaga ā€¦", - "Loading ā€¦": "Laadin ā€¦", + "Loading ā€¦": "Laadimeā€¦", "e.g. %(exampleValue)s": "nƤiteks %(exampleValue)s", "Could not find user in room": "Jututoast ei leidnud kasutajat", "Show timestamps in 12 hour format (e.g. 2:30pm)": "NƤita ajatempleid 12-tunnises vormingus (nƤiteks 2:30pl)", @@ -3838,5 +3838,31 @@ "Use \"Reply in thread\" when hovering over a message.": "SƵnumi kohal avanevast valikust kasuta ā€žVasta jutulƵngasā€œ vƵimalust.", "How can I start a thread?": "Kuidas ma alustan jutulƵnga?", "Threads help keep conversations on-topic and easy to track. Learn more.": "JutulƵngad aitavad hoida vestlusi teemakohastena ja jƤlgitavatena. Lisateavet leiad siit.", - "Keep discussions organised with threads.": "Halda vestlusi jutulƵngadena." + "Keep discussions organised with threads.": "Halda vestlusi jutulƵngadena.", + "sends hearts": "saadame sĆ¼dameid", + "Sends the given message with hearts": "Lisab sellele sƵnumile sĆ¼damed", + "Live location ended": "Reaalajas asukoha jagamine on lƵppenud", + "Loading live location...": "Reaalajas asukoha laadmine...", + "View live location": "Vaata asukohta reaalajas", + "Confirm signing out these devices|one": "Kinnita selle seadme vƤljalogimine", + "Confirm signing out these devices|other": "Kinnita nende seadmete vƤljalogimine", + "Live location enabled": "Reaalajas asukoha jagamine on kasutusel", + "Live location error": "Viga asukoha jagamisel reaalajas", + "Live until %(expiryTime)s": "Kuvamine toimib kuni %(expiryTime)s", + "Ban from room": "MƤƤra suhtluskeeld jututoas", + "Unban from room": "Eemalda suhtluskeeld jututoas", + "Ban from space": "MƤƤra suhtluskeeld kogukonnas", + "Unban from space": "Eemalda suhtluskeeld kogukonnas", + "Yes, enable": "Jah, vƵta kasutusele", + "Do you want to enable threads anyway?": "Kas sa ikkagi soovid jutulƵngad kasutusele vƵtta?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Sinu koduserver hetkel ei toeta jutulƵngasid ning seega antud funktsionaalsus ei pruugi toimida korralikult. KƵik sƵnumid jutulƵngas ilmselt ei ole loetavad. Lisateave.", + "Partial Support for Threads": "Osaline jutulƵngade tugi", + "Right-click message context menu": "Parema hiireklƵpsuga ava sƵnumi kontekstimenĆ¼Ć¼", + "Jump to the given date in the timeline": "Vaata ajajoont alates sellest kuupƤevast", + "Remove from space": "Eemalda sellest kogukonnast", + "Disinvite from room": "Eemalda kutse jututuppa", + "Disinvite from space": "Eemalda kutse kogukonda", + "Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Soovitus: SƵnumi kohal avanevast valikust kasuta ā€ž%(replyInThread)sā€œ vƵimalust.", + "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "Lahkumiseks ava sama vaade ning klƵpsi nuppu ā€ž%(leaveTheBeta)sā€œ.", + "Use ā€œ%(replyInThread)sā€ when hovering over a message.": "SƵnumi kohal avanevast valikust kasuta ā€ž%(replyInThread)sā€œ vƵimalust." } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 2ebbfc1d0dc..7dce75a7bda 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -3269,5 +3269,88 @@ "User is already in the room": "KƤyttƤjƤ on jo huoneessa", "User is already invited to the room": "KƤyttƤjƤ on jo kutsuttu huoneeseen", "%(space1Name)s and %(space2Name)s": "%(space1Name)s ja %(space2Name)s", - "Failed to invite users to %(roomName)s": "KƤyttƤjien kutsuminen huoneeseen %(roomName)s epƤonnistui" + "Failed to invite users to %(roomName)s": "KƤyttƤjien kutsuminen huoneeseen %(roomName)s epƤonnistui", + "Open user settings": "Avaa kƤyttƤjƤasetukset", + "Redo edit": "Tee uudelleen muokkaus", + "Undo edit": "Kumoa muokkaus", + "Jump to last message": "Siirry viimeiseen viestiin", + "Jump to first message": "Siirry ensimmƤiseen viestiin", + "Accessibility": "Saavutettavuus", + "Toggle webcam on/off": "Kamera pƤƤlle/pois", + "[number]": "[numero]", + "Your new device is now verified. Other users will see it as trusted.": "Uusi laitteesi on nyt vahvistettu. Muut kƤyttƤjƤt nƤkevƤt sen luotettuna.", + "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uusi laitteesi on nyt vahvistettu. Laitteella on pƤƤsy salattuihin viesteihisi, ja muut kƤyttƤjƤt nƤkevƤt sen luotettuna.", + "Verify with another device": "Vahvista toisella laitteella", + "The email address doesn't appear to be valid.": "SƤhkƶpostiosoite ei vaikuta kelvolliselta.", + "Device verified": "Laite vahvistettu", + "Verify this device": "Vahvista tƤmƤ laite", + "Unable to verify this device": "TƤtƤ laitetta ei voitu vahvistaa", + "Do not disturb": "ƄlƤ hƤiritse", + "Set a new status": "Aseta uusi tila", + "Give feedback": "Anna palautetta", + "Failed to load list of rooms.": "Huoneluettelon lataaminen epƤonnistui.", + "Joined": "Liitytty", + "Joining": "LiitytƤƤn", + "Wait!": "Odota!", + "Own your conversations.": "Omista keskustelusi.", + "Unnamed audio": "Nimetƶn ƤƤni", + "Stop sharing and close": "Lopeta jakaminen ja sulje", + "Stop sharing": "Lopeta jakaminen", + "%(timeRemaining)s left": "%(timeRemaining)s jƤljellƤ", + "Click for more info": "Napsauta tƤstƤ saadaksesi lisƤtietoja", + "This is a beta feature": "TƤmƤ on beetaominaisuus", + "Start audio stream": "KƤynnistƤ ƤƤnen suoratoisto", + "Unable to start audio streaming.": "ƄƤnen suoratoiston aloittaminen ei onnistu.", + "Open in OpenStreetMap": "Avaa OpenStreetMapissa", + "No verification requests found": "VahvistuspyyntƶjƤ ei lƶytynyt", + "Observe only": "Tarkkaile ainoastaan", + "Requester": "PyytƤjƤ", + "Methods": "MenetelmƤt", + "Timeout": "Aikakatkaisu", + "Phase": "Vaihe", + "Transaction": "Transaktio", + "Cancelled": "Peruttu", + "Started": "KƤynnistetty", + "Ready": "Valmis", + "Requested": "Pyydetty", + "Edit setting": "Muokkaa asetusta", + "Edit values": "Muokkaa arvoja", + "Failed to save settings.": "Asetusten tallentaminen epƤonnistui.", + "Number of users": "KƤyttƤjƤmƤƤrƤ", + "Server": "Palvelin", + "Failed to load.": "Lataaminen epƤonnistui.", + "Capabilities": "Kyvykkyydet", + "Doesn't look like valid JSON.": "Ei vaikuta kelvolliselta JSON:ilta.", + "Verify other device": "Vahvista toinen laite", + "Use to scroll": "KƤytƤ vierittƤƤksesi", + "Clear": "TyhjennƤ", + "Join %(roomAddress)s": "Liity %(roomAddress)s", + "Link to room": "LinkitƤ huoneeseen", + "Matrix.org is the biggest public homeserver in the world, so it's a good place for many.": "Matrix.org on maailman suurin julkinen kotipalvelin, joten se on hyvƤ valinta useimmille.", + "Automatically invite members from this room to the new one": "Kutsu jƤsenet tƤstƤ huoneesta automaattisesti uuteen huoneeseen", + "Spam or propaganda": "Roskapostitusta tai propagandaa", + "Toxic Behaviour": "Myrkyllinen kƤyttƤytyminen", + "Feedback sent! Thanks, we appreciate it!": "Palaute lƤhetetty. Kiitos, arvostamme sitƤ!", + "Server info": "Palvelimen tiedot", + "Create room": "Luo huone", + "Create video room": "Luo videohuone", + "Create a video room": "Luo videohuone", + "%(featureName)s Beta feedback": "Ominaisuuden %(featureName)s beetapalaute", + "%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s jƤsentƤ mukaan lukien sinƤ ja %(commaSeparatedMembers)s", + "%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s jƤsentƤ mukaan lukien sinƤ, %(commaSeparatedMembers)s", + "Including you, %(commaSeparatedMembers)s": "Mukaan lukien sinƤ, %(commaSeparatedMembers)s", + "Beta feature. Click to learn more.": "Beetaominaisuus. Napsauta saadaksesi lisƤtietoja.", + "Beta feature": "Beetaominaisuus", + "The beginning of the room": "Huoneen alku", + "Last month": "Viime kuukausi", + "Last week": "Viime viikko", + "%(count)s participants|one": "1 osallistuja", + "%(count)s participants|other": "%(count)s osallistujaa", + "Connected": "Yhdistetty", + "Copy room link": "Kopioi huoneen linkki", + "New video room": "Uusi videohuone", + "New room": "Uusi huone", + "The new search": "Uusi haku", + "New search experience": "Uusi hakukokemus", + "That link is no longer supported": "Se linkki ei ole enƤƤ tuettu" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 9613c106134..7f359f98a04 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3778,5 +3778,82 @@ "%(value)sh": "%(value)sh", "%(value)sd": "%(value)sd", "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon ou un identifiant dā€™utilisateur, ces donnĆ©es sont enlevĆ©es avant quā€™elle ne soit envoyĆ©e au serveur.", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si vous ne trouvez pas le salon que vous cherchez, demandez une invitation ou crĆ©ez un nouveau salon." + "If you can't find the room you're looking for, ask for an invite or create a new room.": "Si vous ne trouvez pas le salon que vous cherchez, demandez une invitation ou crĆ©ez un nouveau salon.", + "Give feedback": "Faire un commentaire", + "Threads are a beta feature": "Les fils de discussion sont une fonctionnalitĆ© bĆŖta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "ConseilĀ : Utilisez Ā«Ā RĆ©pondre dans le fil de discussionĀ Ā» en survolant un message.", + "Threads help keep your conversations on-topic and easy to track.": "Les fils de discussion vous permettent de recentrer vos conversations et de les rendre facile Ć  suivre.", + "Stop sharing and close": "ArrĆŖter le partage et fermer", + "An error occurred while stopping your live location, please try again": "Une erreur sā€™est produite en arrĆŖtant le partage de votre position, veuillez rĆ©essayer", + "An error occured whilst sharing your live location, please try again": "Une erreur sā€™est produite pendant le partage de votre position, veuillez rĆ©essayer plus tard", + "An error occured whilst sharing your live location": "Une erreur sā€™est produite pendant le partage de votre position", + "Create room": "CrĆ©er un salon", + "Create video room": "CrĆ©e le salon visio", + "Create a video room": "CrĆ©er un salon visio", + "%(featureName)s Beta feedback": "Commentaires sur la bĆŖta de %(featureName)s", + "Beta feature. Click to learn more.": "FonctionnalitĆ© bĆŖta. Cliquez pour en savoir plus.", + "Beta feature": "FonctionnalitĆ© bĆŖta", + "%(count)s participants|one": "1 participant", + "%(count)s participants|other": "%(count)s participants", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s a Ć©tĆ© retournĆ© en essayant dā€™accĆ©der au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez soumettre un rapport dā€™anomalie.", + "Try again later, or ask a room or space admin to check if you have access.": "RĆ©essayez plus tard ou demandez Ć  lā€™administrateur du salon ou de lā€™espace si vous y avez accĆØs.", + "This room or space is not accessible at this time.": "Ce salon ou cet espace nā€™est pas accessible en ce moment.", + "Are you sure you're at the right place?": "Ɗtes-vous sĆ»r dā€™ĆŖtre au bon endroit ?", + "This room or space does not exist.": "Ce salon ou cet espace nā€™existe pas.", + "There's no preview, would you like to join?": "Il nā€™y a pas dā€™aperƧu, voulez-vous rejoindreĀ ?", + "This invite was sent to %(email)s": "Cet invitation a Ć©tĆ© envoyĆ©e Ć  %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "Cette invitation a Ć©tĆ© envoyĆ©e Ć  %(email)s qui nā€™est pas associĆ© Ć  votre compte", + "You can still join here.": "Vous pouvez toujours rejoindre cet endroit.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Une erreur (%(errcode)s) sā€™est produite en essayant de valider votre invitation. Vous pouvez essayer de transmettre cette information Ć  la personne qui vous a invitĆ©(e).", + "Something went wrong with your invite.": "Quelque chose sā€™est mal passĆ© avec votre invitation.", + "You were banned by %(memberName)s": "Vous avez Ć©tĆ© banni par %(memberName)s", + "Forget this space": "Oublier cet espace", + "You were removed by %(memberName)s": "Vous avez Ć©tĆ© retirĆ© par %(memberName)s", + "Loading preview": "Chargement de lā€™aperƧu", + "Joining ā€¦": "En train de rejoindreā€¦", + "New video room": "Nouveau salon visio", + "New room": "Nouveau salon", + "View older version of %(spaceName)s.": "Voir lā€™ancienne version de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Mettre Ć  niveau cet espace vers la version recommandĆ©e", + "sends hearts": "envoie des cœurs", + "Sends the given message with hearts": "Envoie le message donnĆ© avec des cœurs", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Partage de position en direct ā€“ partage votre position actuelle (dĆ©veloppement en cours, et temporairement, les positions sont persistantes dans lā€™historique du salon)", + "Video rooms (under active development)": "Salons visios (en dĆ©veloppement actif)", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Pour quitter, revenez Ć  cette page et utilisez le bouton Ā« Quitter la bĆŖtaĀ Ā».", + "Use \"Reply in thread\" when hovering over a message.": "Utilisez Ā«Ā RĆ©pondre dans le fil de discussionĀ Ā» en survolant un message.", + "How can I start a thread?": "Comment dĆ©marrer un fil de discussionĀ ?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Les fils de discussion aident Ć  recentrer les conversations et les rends faciles Ć  suivre. En savoir plus.", + "Keep discussions organised with threads.": "Gardez vos conversations organisĆ©es avec les fils de discussion.", + "Failed to join": "Impossible de rejoindre", + "The person who invited you has already left, or their server is offline.": "La personne qui vous a invitĆ©(e) a dĆ©jĆ  quittĆ© le salon, ou son serveur est hors-ligne.", + "The person who invited you has already left.": "La personne qui vous a invitĆ©(e) a dĆ©jĆ  quittĆ© le salon.", + "Sorry, your homeserver is too old to participate here.": "DĆ©solĆ©, votre serveur d'accueil est trop vieux pour participer ici.", + "There was an error joining.": "Il y a eu une erreur en rejoignant.", + "The user's homeserver does not support the version of the space.": "Le serveur dā€™accueil de lā€™utilisateur ne prend pas en charge la version de cet espace.", + "User may or may not exist": "Lā€™utilisateur existe peut-ĆŖtre", + "User does not exist": "Lā€™utilisateur nā€™existe pas", + "User is already in the room": "Lā€™utilisateur est dĆ©jĆ  dans ce salon", + "User is already in the space": "Lā€™utilisateur est dĆ©jĆ  dans cet espace", + "User is already invited to the room": "Lā€™utilisateur a dĆ©jĆ  Ć©tĆ© invitĆ© dans ce salon", + "User is already invited to the space": "Lā€™utilisateur a dĆ©jĆ  Ć©tĆ© invitĆ© dans cet espace", + "You do not have permission to invite people to this space.": "Vous nā€™avez pas la permission dā€™inviter des personnes dans cet espace.", + "Failed to invite users to %(roomName)s": "Impossible dā€™inviter les utilisateurs dans %(roomName)s", + "Jump to the given date in the timeline": "Aller Ć  la date correspondante dans la discussion", + "View live location": "Voir la position en direct", + "Ban from room": "Bannir du salon", + "Unban from room": "RĆ©voquer le bannissement du salon", + "Ban from space": "Bannir de l'espace", + "Confirm signing out these devices|one": "Confirmer la dĆ©connexion de cet appareil", + "Confirm signing out these devices|other": "Confirmer la dĆ©connexion de ces appareils", + "Yes, enable": "Oui, activer", + "Do you want to enable threads anyway?": "Voulez-vous activer les fils de discussions malgrĆ© tout ?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Votre serveur d'accueil ne prend pas actuellement en charge les fils de discussions, cette fonctionnalitĆ© peut donc ne pas ĆŖtre fiable. Certains messages dans les fils de discussions peuvent ne pas ĆŖtre disponibles de maniĆØre fiable. En savoir plus.", + "Live location enabled": "Position en temps rĆ©el activĆ©e", + "Live location error": "Erreur de positionnement en temps rĆ©el", + "Live location ended": "Position en temps rĆ©el terminĆ©e", + "Loading live location...": "Chargement de la position en directā€¦", + "Live until %(expiryTime)s": "En direct jusquā€™Ć  %(expiryTime)s", + "Unban from space": "RĆ©voquer le bannissement de lā€™espace", + "Partial Support for Threads": "Prise en charge partielle des fils de discussions", + "Right-click message context menu": "Menu contextuel du message avec clic-droit" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 332299eb656..63679ba2654 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3284,7 +3284,7 @@ "The homeserver the user you're verifying is connected to": "O servidor ao que estĆ” conectado a persoa que estĆ”s verificando", "Can't see what you're looking for?": "Non atopas o que buscas?", "You do not have permission to start polls in this room.": "Non tes permiso para publicar enquisas nesta sala.", - "Reply in thread": "Responder na conversa", + "Reply in thread": "Responder nun fĆ­o", "Manage rooms in this space": "Xestionar salas neste espazo", "You won't get any notifications": "Non recibirĆ”s ningunha notificaciĆ³n", "Get notifications as set up in your settings": "Ter notificaciĆ³ns tal como se indica nos axustes", @@ -3782,5 +3782,76 @@ "User is already invited to the room": "A usuaria xa estĆ” convidada Ć” sala", "User is already invited to the space": "A usuaria xa estĆ” convidada ao espazo", "You do not have permission to invite people to this space.": "Non tes permiso para convidar persoas a este espazo.", - "Failed to invite users to %(roomName)s": "Fallou o convite das usuarias para %(roomName)s" + "Failed to invite users to %(roomName)s": "Fallou o convite das usuarias para %(roomName)s", + "Give feedback": "Informar e dar opiniĆ³n", + "Threads are a beta feature": "Os fĆ­os son unha ferramenta beta", + "Threads help keep your conversations on-topic and easy to track.": "Os fĆ­os axĆŗdanche a manter as conversas no tema e facilitan o seguimento.", + "Stop sharing and close": "Deter a comparticiĆ³n e pechar", + "An error occurred while stopping your live location, please try again": "Algo fallou ao deter a tĆŗa localizaciĆ³n en directo, intĆ©ntao outra vez", + "An error occured whilst sharing your live location, please try again": "Algo fallou ao compartir a tĆŗa localizaciĆ³n en directo, intĆ©ntao mĆ”is tarde", + "An error occured whilst sharing your live location": "Algo fallou ao compartir a tĆŗa localizaciĆ³n en directo", + "Create room": "Crear sala", + "Create video room": "Crear sala de vĆ­deo", + "Create a video room": "Crear sala de vĆ­deo", + "%(featureName)s Beta feedback": "Informe sobre %(featureName)s Beta", + "Beta feature. Click to learn more.": "CaracterĆ­stica beta. Preme para saber mĆ”is.", + "Beta feature": "CaracterĆ­stica Beta", + "%(count)s participants|one": "1 participante", + "%(count)s participants|other": "%(count)s participantes", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "ObtĆ­vose o erro %(errcode)s ao intentar acceder Ć” sala ou espazo. Se cres que esta mensaxe Ć© un erro, por favor envĆ­a un informe do fallo.", + "Try again later, or ask a room or space admin to check if you have access.": "IntĆ©ntao mĆ”is tarde, ou solicita a admin da sala ou espazo que mire se tes acceso.", + "This room or space is not accessible at this time.": "Esta sala ou espazo non Ć© accesible neste intre.", + "Are you sure you're at the right place?": "Tes a certeza de que Ć© o lugar correcto?", + "This room or space does not exist.": "Esta sala ou espazo no existe.", + "There's no preview, would you like to join?": "Non hai vista previa, queres unirte?", + "This invite was sent to %(email)s": "Este convite enviouse a %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "O convite enviouselle a %(email)s que non estĆ” asociado coa tĆŗa conta", + "You can still join here.": "Podes entrar aquĆ­ igualmente.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Houbo un erro (%(errcode)s) ao intentar validar o teu convite. Podes intentar enviarlle esta informaciĆ³n Ć” persoa que te convidou.", + "Something went wrong with your invite.": "Algo foi mal co teu convite.", + "You were banned by %(memberName)s": "%(memberName)s vetoute", + "Forget this space": "Esquecer este espazo", + "You were removed by %(memberName)s": "%(memberName)s eliminoute de aquĆ­", + "Loading preview": "Cargando vista previa", + "Joining ā€¦": "Entrandoā€¦", + "New video room": "Nova sala de vĆ­deo", + "New room": "Nova sala", + "View older version of %(spaceName)s.": "Ver versiĆ³n anterior de %(spaceName)s.", + "Upgrade this space to the recommended room version": "Actualiza este espazo Ć” Ćŗltima versiĆ³n recomendada da sala", + "Video rooms (under active development)": "Salas de vĆ­deo (en desenvolvemento activo)", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Para saĆ­r, volve a esta pĆ”xina e usa o botĆ³n \"SaĆ­r da beta\".", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Truco: Us \"Responder nun fĆ­o\" ao situarte sobre unha mensaxe.", + "Use \"Reply in thread\" when hovering over a message.": "Usa \"Responder nun fĆ­o\" cando te sitĆŗes nunha mensaxe.", + "How can I start a thread?": "Como abrir un fĆ­o?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Os fĆ­os axudan a centrar a conversa nun tema e facilitan o seguimento. CoƱece mĆ”is.", + "Keep discussions organised with threads.": "Marter as conversas organizadas en fĆ­os.", + "Failed to join": "Non puideches entrar", + "The person who invited you has already left, or their server is offline.": "A persoa que te convidou xa saĆ­u, ou o seu servidor non estĆ” conectado.", + "The person who invited you has already left.": "A persoa que te convidou xa deixou o lugar.", + "Sorry, your homeserver is too old to participate here.": "LamentĆ”molo, o teu servidor de inicio Ć© demasiado antigo para poder participar.", + "There was an error joining.": "Houbo un erro ao unirte.", + "The user's homeserver does not support the version of the space.": "O servidor de inicio da usuaria non soporta a versiĆ³n do Espazo.", + "sends hearts": "envĆ­a corazĆ³ns", + "Sends the given message with hearts": "EngĆ”delle moitos corazĆ³ns Ć” mensaxe", + "Confirm signing out these devices|one": "Confirma a desconexiĆ³n deste dispositivo", + "Confirm signing out these devices|other": "Confirma a desconexiĆ³n destos dispositivos", + "Live location ended": "Rematou a localizaciĆ³n en directo", + "Loading live location...": "Obtendo localizaciĆ³n en directo...", + "View live location": "Ver localizaciĆ³n en directo", + "Live location enabled": "Activada a localizaciĆ³n en directo", + "Live location error": "Erro na localizaciĆ³n en directo", + "Live until %(expiryTime)s": "En directo ata %(expiryTime)s", + "Ban from room": "Vetar na sala", + "Unban from room": "Retirar veto Ć” sala", + "Ban from space": "Vetar ao espazo", + "Unban from space": "Retirar veto ao espazo", + "Disinvite from room": "Retirar convite Ć” sala", + "Remove from space": "Retirar do espazo", + "Disinvite from space": "Retirar convite ao espazo", + "Yes, enable": "Si, activĆ”deos", + "Do you want to enable threads anyway?": "Queres activar os fĆ­os igualmente?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "O teu servidor actualmente non ten soporte para fĆ­os, polo que poderĆ­a non ser totalmente fiable. AlgĆŗn dos comentarios fiados poderĆ­an non estar dispoƱibles. Saber mĆ”is.", + "Partial Support for Threads": "Soporte parcial para FĆ­os", + "Right-click message context menu": "BotĆ³n dereito para menĆŗ contextual", + "Jump to the given date in the timeline": "Ir Ć” seguinte data dada na cronoloxĆ­a" } diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 420ad1e9062..3038fd5ecc6 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -1641,7 +1641,7 @@ "Support adding custom themes": "Dukungan penambahan tema kustom", "Try out new ways to ignore people (experimental)": "Coba cara yang baru untuk mengabaikan pengguna (eksperimental)", "Multiple integration managers (requires manual setup)": "Beberapa manajer integrasi (membutuhkan penyiapan manual)", - "Render simple counters in room header": "Tampilkan penghitung simpel di tajukan ruangan", + "Render simple counters in room header": "Tampilkan penghitung sederhana di tajukan ruangan", "Group & filter rooms by custom tags (refresh to apply changes)": "Kelompokkan & filter ruangan dengan tag kustom (muat ulang untuk menerapkan perubahan)", "Custom user status messages": "Pesan status pengguna kustom", "Threaded messaging": "Pesan utasan", @@ -1693,7 +1693,7 @@ "Don't miss a reply": "Jangan lewatkan sebuah balasan", "Review to ensure your account is safe": "Periksa untuk memastikan akun Anda aman", "You have unverified logins": "Anda punya login yang belum diverifikasi", - "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Mengirimkan data penggunaan anonim yang akan bantu kami untuk membuat %(brand)s lebih baik. Ini akan menggunakan sebuah cookie.", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Mengirimkan data penggunaan anonim yang akan bantu kami untuk membuat %(brand)s lebih baik. Ini akan menggunakan sebuah kuki.", "Help us improve %(brand)s": "Bantu kami membuat %(brand)s lebih baik", "File Attached": "File Dilampirkan", "Error fetching file": "Terjadi kesalahan saat mendapatkan file", @@ -1751,7 +1751,7 @@ "Spaces are ways to group rooms and people.": "Space adalah salah satu cara untuk mengelompokkan ruangan dan pengguna.", "Sidebar": "Bilah Samping", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Kelola sesi Anda di bawah. Sebuah nama sesi dapat dilihat oleh siapa saja yang Anda berkomunikasi.", - "Where you're signed in": "Dimana Anda masuk", + "Where you're signed in": "Di mana Anda masuk", "Learn more about how we use analytics.": "Pelajari lebih lanjut tentang bagaimana kamu menggunakan analitik.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privasi itu penting bagi kami, jadi kami tidak mengumpulkan data personal atau data yang dapat diidentifikasi untuk analitik kami.", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s mengumpulkan analitik anonim untuk membantu kami untuk membuat aplikasi ini lebih baik.", @@ -1863,7 +1863,7 @@ "Waiting for %(displayName)s to acceptā€¦": "Menunggu untuk %(displayName)s untuk menerimaā€¦", "To proceed, please accept the verification request on your other login.": "Untuk melanjutkan, mohon terima permintaan verifikasi di login Anda yang lain.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Ketika seseorang menambahkan URL di pesannya, sebuah tampilan URL dapat ditampilkan untuk memberikan informasi lainnya tentang tautan itu seperti judul, deskripsi, dan sebuah gambar dari website.", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Di ruangan terenkripsi, seperti ruangan ini, tampilan URL dinonaktifkan untuk memastikan homeserver Anda (dimana tampilannya dibuat) tidak mendapatkan informasi tentang tautan yang Anda lihat di ruangan ini.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Di ruangan terenkripsi, seperti ruangan ini, tampilan URL dinonaktifkan untuk memastikan homeserver Anda (di mana tampilannya dibuat) tidak mendapatkan informasi tentang tautan yang Anda lihat di ruangan ini.", "URL previews are disabled by default for participants in this room.": "Tampilan URL dinonaktifkan secara default untuk anggota di ruangan ini.", "URL previews are enabled by default for participants in this room.": "Tampilan URL diaktifkan secara default untuk anggota di ruangan ini.", "You have disabled URL previews by default.": "Anda telah menonaktifkan tampilan URL secara default.", @@ -2554,7 +2554,7 @@ "Message search initialisation failed, check your settings for more information": "Initialisasi pencarian pesan gagal, periksa pengaturan Anda untuk informasi lanjut", "Error - Mixed content": "Terjadi kesalahan ā€” Konten tercampur", "Error loading Widget": "Terjadi kesalahan saat memuat Widget", - "This widget may use cookies.": "Widget ini mungkin menggunakan cookie.", + "This widget may use cookies.": "Widget ini mungkin menggunakan kuki.", "Widget added by": "Widget ditambahkan oleh", "Widgets do not use message encryption.": "Widget tidak menggunakan enkripsi pesan.", "Using this widget may share data with %(widgetDomain)s.": "Menggunakan widget ini mungkin membagikan data dengan %(widgetDomain)s.", @@ -2631,7 +2631,7 @@ "You should know": "Anda seharusnya tahu", "Terms of Service": "Persyaratan Layanan", "Privacy Policy": "Kebijakan Privasi", - "Cookie Policy": "Kebijakan Cookie", + "Cookie Policy": "Kebijakan Kuki", "Learn more in our , and .": "Pelajari lebih lanjut di , , dan .", "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Melanjutkan untuk sementara mengizinkan proses pengaturan %(hostSignupBrand)s untuk mengakses akun Anda untuk mendapatkan alamat-alamat email yang terverifikasi. Data ini tidak disimpan.", "Failed to connect to your homeserver. Please close this dialog and try again.": "Gagal menghubungkan ke homeserver Anda. Mohon tutup dialog ini dan coba lagi.", @@ -3080,7 +3080,7 @@ "Verify with Security Key or Phrase": "Verifikasi dengan Kunci Keamanan atau Frasa", "Proceed with reset": "Lanjutkan dengan mengatur ulang", "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.": "Sepertinya Anda tidak memiliki Kunci Keamanan atau perangkat lainnya yang Anda dapat gunakan untuk memverifikasi. Perangkat ini tidak dapat mengakses ke pesan terenkripsi lama. Untuk membuktikan identitas Anda, kunci verifikasi harus diatur ulang.", - "Decide where your account is hosted": "Putuskan dimana untuk menghost akun Anda", + "Decide where your account is hosted": "Putuskan di mana untuk menghost akun Anda", "Host account on": "Host akun di", "You can now close this window or log in to your new account.": "Anda dapat menutup jendela ini atau masuk ke akun yang baru.", "Log in to your new account.": "Masuk ke akun yang baru.", @@ -3126,7 +3126,7 @@ "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Jika Anda mau, dicatat bahwa pesan-pesan Anda tidak dihapus, tetapi pengalaman pencarian mungkin terdegradasi untuk beberapa saat indeksnya sedang dibuat ulang", "You most likely do not want to reset your event index store": "Kemungkinan besar Anda tidak ingin mengatur ulang penyimpanan indeks peristiwa Anda", "Reset event store?": "Atur ulang penyimanan peristiwa?", - "About homeservers": "Tentang homeserver-homeserver", + "About homeservers": "Tentang homeserver", "Continuing without email": "Melanjutkan tanpa email", "Data on this screen is shared with %(widgetDomain)s": "Data di layar ini dibagikan dengan %(widgetDomain)s", "Modal Widget": "Widget Modal", @@ -3347,7 +3347,7 @@ "Help improve %(analyticsOwner)s": "Bantu membuat %(analyticsOwner)s lebih baik", "That's fine": "Saya tidak keberatan", "Some examples of the information being sent to us to help make %(brand)s better includes:": "Beberapa contoh informasi yang akan dikirim ke kami untuk membuat %(brand)s lebih baik termasuk:", - "Our complete cookie policy can be found here.": "Kebijakan cookie kami yang lengkap dapat ditemukan di sini.", + "Our complete cookie policy can be found here.": "Kebijakan kuki kami yang lengkap dapat ditemukan di sini.", "Type of location share": "Ketik deskripsi", "My location": "Lokasi saya", "Share my current location as a once off": "Bagikan lokasi saya saat ini (sekali saja)", @@ -3672,7 +3672,7 @@ "Connected": "Terhubung", "Voice & video rooms (under active development)": "Ruangan suara & video (dalam pengembangan aktif)", "That link is no longer supported": "Tautan itu tidak didukung lagi", - "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Dimana laman ini berisi informasi yang dapat dikenal, seperti sebuah ruangan, ID pengguna, data itu dihilangkan sebelum dikirimkan ke server.", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Di mana laman ini berisi informasi yang dapat dikenal, seperti sebuah ruangan, ID pengguna, data itu dihilangkan sebelum dikirimkan ke server.", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Anda dapat menggunakan opsi server khusus untuk masuk ke server Matrix lain dengan menentukan URL homeserver yang berbeda. Ini memungkinkan Anda untuk menggunakan %(brand)s dengan akun Matrix yang ada di homeserver yang berbeda.", "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "Izin %(brand)s ditolak untuk mengakses lokasi Anda. Mohon izinkan akses lokasi di pengaturan peramban Anda.", "Developer tools": "Alat pengembang", @@ -3766,6 +3766,32 @@ "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Untuk keluar, kembali ke laman ini dan gunakan tombol ā€œTinggalkan betaā€.", "Use \"Reply in thread\" when hovering over a message.": "Gunakan \"Balas dalam utasan\" ketika kursor berada di atas pesan.", "How can I start a thread?": "Bagaimana saya dapat memulai sebuah utasan?", - "Threads help keep conversations on-topic and easy to track. Learn more.": "Utasan membantu membuat percakapan dalam topik dan mudah untuk dilacak. Pelajari lebih lanjut.", - "Keep discussions organised with threads.": "Buat diskusi tetap teratur dengan utasan." + "Threads help keep conversations on-topic and easy to track. Learn more.": "Utasan membantu membuat percakapan sesuai topik dan mudah untuk dilacak. Pelajari lebih lanjut.", + "Keep discussions organised with threads.": "Buat diskusi tetap teratur dengan utasan.", + "Give feedback": "Berikan masukan", + "Threads are a beta feature": "Utasan adalah fitur beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Gunakan \"Balas dalam utasan\" ketika kursor ada di atas pesan.", + "sends hearts": "mengirim hati", + "Sends the given message with hearts": "Kirim pesan dengan hati", + "Confirm signing out these devices|one": "Konfirmasi mengeluarkan perangkat ini", + "Confirm signing out these devices|other": "Konfirmasi mengeluarkan perangkat ini", + "Live location ended": "Lokasi langsung berakhir", + "Loading live location...": "Memuat lokasi langsungā€¦", + "View live location": "Tampilkan lokasi langsung", + "Live location enabled": "Lokasi langsung diaktifkan", + "Live location error": "Kesalahan lokasi langsung", + "Live until %(expiryTime)s": "Langsung sampai %(expiryTime)s", + "Ban from room": "Cekal dari ruangan", + "Unban from room": "Batalkan cekalan dari ruangan", + "Ban from space": "Cekal dari space", + "Unban from space": "Batalkan cekalan dari space", + "Yes, enable": "Iya, aktifkan", + "Do you want to enable threads anyway?": "Apakah Anda ingin mengaktifkan utasan?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Homeserver Anda saat ini tidak mendukung utasan, jadi fitur ini mungkin tidak andal. Beberapa pesan yang diutas mungkin tidak tersedia. Pelajari lebih lanjut.", + "Partial Support for Threads": "Sebagian Dukungan untuk Utasan", + "Jump to the given date in the timeline": "Pergi ke tanggal yang diberikan di linimasa", + "Right-click message context menu": "Klik kanan menu konteks pesan", + "Disinvite from room": "Batalkan undangan dari ruangan", + "Remove from space": "Keluarkan dari space", + "Disinvite from space": "Batalkan undangan dari space" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 00c1852db37..fee5111aa60 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3829,5 +3829,17 @@ "%(count)s participants|other": "%(count)s partecipanti", "New video room": "Nuova stanza video", "New room": "Nuova stanza", - "Video rooms (under active development)": "Stanze video (in sviluppo attivo)" + "Video rooms (under active development)": "Stanze video (in sviluppo attivo)", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Le conversazioni aiutano a tenere le discussioni in tema e rintracciabili. Maggiori info.", + "Give feedback": "Lascia feedback", + "Threads are a beta feature": "Le conversazioni sono una funzionalitĆ  beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Consiglio: usa \"Rispondi nella conversazione\" passando sopra un messaggio.", + "Threads help keep your conversations on-topic and easy to track.": "Le conversazioni ti aiutano a tenere le tue discussioni in tema e rintracciabili.", + "%(featureName)s Beta feedback": "Feedback %(featureName)s beta", + "Beta feature. Click to learn more.": "FunzionalitĆ  beta. Clicca per maggiori informazioni.", + "Beta feature": "FunzionalitĆ  beta", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Per uscire, torna in questa pagina e usa il pulsante \"Abbandona la beta\".", + "Use \"Reply in thread\" when hovering over a message.": "Usa \"Rispondi nella conversazione\" passando sopra un messaggio.", + "How can I start a thread?": "Come inizio una conversazione?", + "Keep discussions organised with threads.": "Tieni le discussioni organizzate in conversazioni." } diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index e41eb8a3f65..85c40c682a6 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -217,7 +217,7 @@ "You are now ignoring %(userId)s": "%(userId)s悒ē„”č¦–ć—ć¦ć„ć¾ć™", "Stops ignoring a user, showing their messages going forward": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ć®ē„”č¦–ć‚’ę­¢ć‚ć¦ć€ćƒ”ćƒƒć‚»ćƒ¼ć‚ø悒č”Øē¤ŗ", "Unignored user": "ē„”視恗恦恄ćŖć„ćƒ¦ćƒ¼ć‚¶ćƒ¼", - "You are no longer ignoring %(userId)s": "恂ćŖ恟ćÆ悂ćÆ悄%(userId)s悒ē„”č¦–ć—ć¦ć„ć¾ć›ć‚“", + "You are no longer ignoring %(userId)s": "恂ćŖ恟ćÆ%(userId)s悒ē„”č¦–ć—ć¦ć„ć¾ć›ć‚“", "Define the power level of a user": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ć®ęØ©é™ćƒ¬ćƒ™ćƒ«ć‚’č¦å®š", "Deops user with given id": "ęŒ‡å®šć•ć‚ŒćŸIDć®ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‚’éžč”Øē¤ŗ", "Opens the Developer Tools dialog": "開ē™ŗč€…ćƒ„ćƒ¼ćƒ«ćƒ€ć‚¤ć‚¢ćƒ­ć‚°ć‚’é–‹ć", @@ -1335,7 +1335,7 @@ "Link this email with your account in Settings to receive invites directly in %(brand)s.": "ć“ć®ćƒ”ćƒ¼ćƒ«ć‚¢ćƒ‰ćƒ¬ć‚¹ć‚’čØ­å®šć‹ć‚‰ć‚ćŖćŸć®ć‚¢ć‚«ć‚¦ćƒ³ćƒˆć«ćƒŖćƒ³ć‚Æ恙悋ćØ%(brand)s恋悉ē›“ęŽ„ę‹›å¾…ć‚’å—ć‘å–ć‚‹ć“ćØćŒć§ćć¾ć™ć€‚", "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "ćƒ«ćƒ¼ćƒ  %(roomName)s ćøć®ę‹›å¾…ćŒć€ć‚¢ć‚«ć‚¦ćƒ³ćƒˆć«é–¢é€£ä»˜ć‘ć‚‰ć‚Œć¦ć„ćŖć„ćƒ”ćƒ¼ćƒ«ć‚¢ćƒ‰ćƒ¬ć‚¹ %(email)s ć«é€ć‚‰ć‚Œć¾ć—ćŸ", "You can still join it because this is a public room.": "å…¬é–‹ćƒ«ćƒ¼ćƒ ćŖć®ć§å‚åŠ ćŒåÆčƒ½ć§ć™ć€‚", - "Try to join anyway": "ē„”č¦–ć—ć¦å‚åŠ ", + "Try to join anyway": "å‚åŠ ć‚’č©¦ćæ悋", "You can only join it with a working invite.": "ęœ‰åŠ¹ćŖę‹›å¾…ćŒć‚ć‚‹å “åˆć«ć®ćæå‚åŠ ć§ćć¾ć™ć€‚", "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "ę‹›å¾…ć‚’čŖčØ¼ć™ć‚‹éš›ć«ć‚Øćƒ©ćƒ¼ļ¼ˆ%(errcode)sļ¼‰ćŒē™ŗē”Ÿć—ć¾ć—ćŸć€‚ć“ć®ęƒ…å ±ć‚’ćƒ«ćƒ¼ćƒ ć®ē®”ē†č€…ć«ä¼ćˆć¦ćæć¦ćć ć•ć„ć€‚", "Something went wrong with your invite to %(roomName)s": "%(roomName)sćøć®ę‹›å¾…ć«å•é”ŒćŒē™ŗē”Ÿć—ć¾ć—ćŸ", @@ -1434,7 +1434,7 @@ "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "ē¾åœØ悒ä½æē”Øć—ć¦ć€é€£ēµ”å…ˆć‚’ę¤œå‡ŗåÆčƒ½ć«ć—ć¦ć„ć¾ć™ć€‚ä»„äø‹ć§IDć‚µćƒ¼ćƒćƒ¼ć‚’å¤‰ę›“ć§ćć¾ć™ć€‚", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "åˆ‡ę–­ć™ć‚‹å‰ć«ć€IDć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ćƒ”ćƒ¼ćƒ«ć‚¢ćƒ‰ćƒ¬ć‚¹ćØ電話ē•Ŗå·ć‚’å‰Šé™¤ć™ć‚‹ć“ćØ悒ęŽØå„Øć—ć¾ć™ć€‚", "You are still sharing your personal data on the identity server .": "ć¾ć IDć‚µćƒ¼ćƒćƒ¼ ć§å€‹äŗŗćƒ‡ćƒ¼ć‚æć‚’å…±ęœ‰ć—ć¦ć„ć¾ć™ć€‚", - "Disconnect anyway": "ē„”č¦–ć—ć¦åˆ‡ę–­", + "Disconnect anyway": "åˆ‡ę–­", "wait and try again later": "ć—ć°ć‚‰ćå¾…ć£ć¦ć€å¾Œć§ć‚‚ć†äø€åŗ¦č©¦ć™", "contact the administrators of identity server ": "IDć‚µćƒ¼ćƒćƒ¼ 恮ē®”ē†č€…ć«é€£ēµ”", "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "ćƒ–ćƒ©ć‚¦ć‚¶ćƒ¼ć®ćƒ—ćƒ©ć‚°ć‚¤ćƒ³ć‹ć‚‰ć€IDć‚µćƒ¼ćƒćƒ¼ć‚’ćƒ–ćƒ­ćƒƒć‚Æ恙悋恋悂恗悌ćŖ恄悂恮ļ¼ˆPrivacy BadgerćŖ恩ļ¼‰ć‚’ē¢ŗčŖć—ć¦ćć ć•ć„", @@ -2707,8 +2707,8 @@ "Other rooms": "ä»–ć®ćƒ«ćƒ¼ćƒ ", "Pin to sidebar": "ć‚µć‚¤ćƒ‰ćƒćƒ¼ć«å›ŗ定", "Quick settings": "ć‚Æ悤惃ć‚Æčح定", - "Invite anyway": "ē„”č¦–ć—ć¦ę‹›å¾…", - "Invite anyway and never warn me again": "ē„”č¦–ć—ć¦ę‹›å¾…ć—ć€å†ć³č­¦å‘Šć—ćŖ恄", + "Invite anyway": "ę‹›å¾…", + "Invite anyway and never warn me again": "ę‹›å¾…ć—ć€å†ć³č­¦å‘Šć—ćŖ恄", "Recovery Method Removed": "å¾©å…ƒę–¹ę³•ć‚’å‰Šé™¤ć—ć¾ć—ćŸ", "Failed to remove some rooms. Try again later": "ć„ćć¤ć‹ć®ćƒ«ćƒ¼ćƒ ć®å‰Šé™¤ć«å¤±ę•—ć—ć¾ć—ćŸć€‚å¾Œć§ć‚‚ć†äø€åŗ¦ć‚„ć‚Šē›“ć—ć¦ćć ć•ć„", "Delete the room address %(alias)s and remove %(name)s from the directory?": "ćƒ«ćƒ¼ćƒ ć®ć‚¢ćƒ‰ćƒ¬ć‚¹ %(alias)s ć‚’å‰Šé™¤ć—ć¦%(name)sć‚’ćƒ‡ć‚£ćƒ¬ć‚Æ惈ćƒŖć‹ć‚‰å‰Šé™¤ć—ć¾ć™ć‹ļ¼Ÿ", @@ -2915,7 +2915,7 @@ "Show %(count)s other previews|one": "他%(count)så€‹ć®ćƒ—ćƒ¬ćƒ“ćƒ„ćƒ¼ć‚’č”Øē¤ŗ", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "ć“ć®ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‚’čŖčØ¼ć—ć¦ć€äæ”é ¼ęøˆćØć—ć¦ćƒžćƒ¼ć‚Æć—ć¾ć™ć€‚ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‚’äæ”é ¼ć™ć‚‹ćØ态悈悊äø€å±¤å®‰åæƒć—恦ć‚Øćƒ³ćƒ‰ćƒ„ćƒ¼ć‚Øćƒ³ćƒ‰ęš—å·åŒ–ć‚’ä½æē”Ø恙悋恓ćØćŒć§ćć¾ć™ć€‚", "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "恓恮ē«Æęœ«ć‚’čŖčØ¼ć—ć¦ć€äæ”é ¼ęøˆćØć—ć¦ćƒžćƒ¼ć‚Æć—ć¾ć™ć€‚ē›øꉋ恮ē«Æęœ«ć‚’äæ”é ¼ć™ć‚‹ćØ态悈悊äø€å±¤å®‰åæƒć—恦ć‚Øćƒ³ćƒ‰ćƒ„ćƒ¼ć‚Øćƒ³ćƒ‰ęš—å·åŒ–ć‚’ä½æē”Ø恙悋恓ćØćŒć§ćć¾ć™ć€‚", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "仄äø‹ć®Matrix IDć®ćƒ—ćƒ­ćƒ•ć‚£ćƒ¼ćƒ«ć‚’ē™ŗč¦‹ć§ćć¾ć›ć‚“ć€‚ē„”č¦–ć—ć¦ę‹›å¾…ć—ć¾ć™ć‹ļ¼Ÿ", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "仄äø‹ć®Matrix IDć®ćƒ—ćƒ­ćƒ•ć‚£ćƒ¼ćƒ«ć‚’ē™ŗč¦‹ć§ćć¾ć›ć‚“ć€‚ę‹›å¾…ć—ć¾ć™ć‹ļ¼Ÿ", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "ę–°ć—ć„å¾©å…ƒę–¹ę³•ć‚’čØ­å®šć—ćŖć‹ć£ćŸå “åˆć€ę”»ę’ƒč€…ćŒć‚¢ć‚«ć‚¦ćƒ³ćƒˆćøć‚¢ć‚Æć‚»ć‚¹ć—ć‚ˆć†ćØ恗恦恄悋åÆčƒ½ę€§ćŒć‚ć‚Šć¾ć™ć€‚čح定ē”»é¢ć«ć¦ć€ć™ćć«ć‚¢ć‚«ć‚¦ćƒ³ćƒˆć®ćƒ‘ć‚¹ćƒÆćƒ¼ćƒ‰ć‚’å¤‰ę›“ć—ć€ę–°ć—ć„å¾©å…ƒę–¹ę³•ć‚’čØ­å®šć—ć¦ćć ć•ć„ć€‚", "General failure": "äø€čˆ¬ć‚Øćƒ©ćƒ¼", "Failed to load group members": "ć‚°ćƒ«ćƒ¼ćƒ—ć®ćƒ”ćƒ³ćƒćƒ¼ć®čŖ­ćæč¾¼ćæć«å¤±ę•—ć—ć¾ć—ćŸ", @@ -3496,5 +3496,68 @@ "All thread events created during the experimental period will now be rendered in the room timeline and displayed as replies. This is a one-off transition. Threads are now part of the Matrix specification.": "ćƒ†ć‚¹ćƒˆäø­ć«ä½œęˆć•ć‚ŒćŸć‚¹ćƒ¬ćƒƒćƒ‰ć«é–¢ć™ć‚‹ć‚¤ćƒ™ćƒ³ćƒˆćÆć€ćƒ«ćƒ¼ćƒ ć®ć‚æć‚¤ćƒ ćƒ©ć‚¤ćƒ³äøŠć§ćÆčæ”äæ”ćØ恗恦č”Øē¤ŗć•ć‚Œć¾ć™ć€‚ć“ć‚ŒćÆäø€åŗ¦é™ć‚Šć®ē§»č”Œć§ć™ć€‚ć‚¹ćƒ¬ćƒƒćƒ‰ćÆMatrixć®ä»•ę§˜ć®äø€éƒØ恫ćŖć‚Šć¾ć—ćŸć€‚", "Thank you for helping us testing Threads!": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć®ćƒ†ć‚¹ćƒˆć«ć”å”åŠ›ć„ćŸć ćć€ć‚ć‚ŠćŒćØć†ć”ć–ć„ć¾ć—ćŸļ¼", "Weā€™ve recently introduced key stability improvements for Threads, which also means phasing out support for experimental Threads.": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć®å®‰å®šę€§ćŒę”¹å–„ć—ćŸćŸć‚ć€ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć®ćƒ†ć‚¹ćƒˆē‰ˆć®ć‚µćƒćƒ¼ćƒˆć‚’ēµ‚äŗ†ć—ć¾ć™ć€‚", - "Threads are no longer experimental! šŸŽ‰": "ć‚¹ćƒ¬ćƒƒćƒ‰ćÆę­£å¼ē‰ˆć«ćŖć‚Šć¾ć—ćŸšŸŽ‰" + "Threads are no longer experimental! šŸŽ‰": "ć‚¹ćƒ¬ćƒƒćƒ‰ćÆę­£å¼ē‰ˆć«ćŖć‚Šć¾ć—ćŸšŸŽ‰", + "Do you want to enable threads anyway?": "ć‚¹ćƒ¬ćƒƒćƒ‰ć‚’ęœ‰åŠ¹ć«ć—ć¾ć™ć‹ļ¼Ÿ", + "Yes, enable": "ęœ‰åŠ¹ć«ć™ć‚‹", + "Live location error": "位ē½®ęƒ…å ±ļ¼ˆćƒ©ć‚¤ćƒ–ļ¼‰ć®ć‚Øćƒ©ćƒ¼", + "sends hearts": "ćƒćƒ¼ćƒˆć‚’é€äæ”", + "Confirm signing out these devices|other": "ć“ć‚Œć‚‰ć®ē«Æęœ«ć‹ć‚‰ć®ć‚µć‚¤ćƒ³ć‚¢ć‚¦ćƒˆć‚’ę‰æčŖ", + "Confirm signing out these devices|one": "恓恮ē«Æęœ«ć‹ć‚‰ć®ć‚µć‚¤ćƒ³ć‚¢ć‚¦ćƒˆć‚’ę‰æčŖ", + "View live location": "位ē½®ęƒ…å ±ļ¼ˆćƒ©ć‚¤ćƒ–ļ¼‰ć‚’č”Øē¤ŗ", + "Loading live location...": "位ē½®ęƒ…å ±ļ¼ˆćƒ©ć‚¤ćƒ–ļ¼‰ć‚’čŖ­ćæč¾¼ć‚“ć§ć„ć¾ć™ā€¦", + "Video rooms (under active development)": "惓惇ć‚Ŗé€šč©±ćƒ«ćƒ¼ćƒ ļ¼ˆé–‹ē™ŗäø­ļ¼‰", + "How can I start a thread?": "ć‚¹ćƒ¬ćƒƒćƒ‰ć®é–‹å§‹ę–¹ę³•", + "Failed to join": "å‚åŠ ć«å¤±ę•—ć—ć¾ć—ćŸ", + "The person who invited you has already left, or their server is offline.": "ę‹›å¾…ć—ćŸäŗŗćŒę—¢ć«é€€å‡ŗć—ćŸć‹ć€ć‚µćƒ¼ćƒćƒ¼ćŒć‚Ŗćƒ•ćƒ©ć‚¤ćƒ³ć§ć™ć€‚", + "The person who invited you has already left.": "ę‹›å¾…ć—ćŸäŗŗćÆę—¢ć«é€€å‡ŗć—ć¾ć—ćŸć€‚", + "Sorry, your homeserver is too old to participate here.": "ē”³ć—čØ³ć‚ć‚Šć¾ć›ć‚“ćŒć€ć‚ćŖćŸć®ćƒ›ćƒ¼ćƒ ć‚µćƒ¼ćƒćƒ¼ćÆć“ć“ć«å‚åŠ ć™ć‚‹ć«ćÆå¤ć™ćŽć¾ć™ć€‚", + "There was an error joining.": "å‚åŠ ć™ć‚‹éš›ć«ć‚Øćƒ©ćƒ¼ćŒē™ŗē”Ÿć—ć¾ć—ćŸć€‚", + "%(value)sm": "%(value)s分", + "%(value)sh": "%(value)sꙂ", + "%(value)sd": "%(value)sę—„", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "ć“ć®ćƒšćƒ¼ć‚øćŒćƒ«ćƒ¼ćƒ ć‚„ćƒ¦ćƒ¼ć‚¶ćƒ¼IDćŖ恩恮ē‰¹å®šåÆčƒ½ćŖęƒ…å ±ć‚’å«ć‚“ć§ć„ć‚‹å “åˆćÆć€ćć®ęƒ…å ±ćÆć‚µćƒ¼ćƒćƒ¼ć«é€äæ”ć•ć‚Œć‚‹å‰ć«å‰Šé™¤ć•ć‚Œć¾ć™ć€‚", + "Beta feature": "ćƒ™ćƒ¼ć‚æē‰ˆć®ę©Ÿčƒ½", + "Live location enabled": "位ē½®ęƒ…å ±ćŒęœ‰åŠ¹ć§ć™", + "Jump to the given date in the timeline": "ć‚æć‚¤ćƒ ćƒ©ć‚¤ćƒ³ć®ęŒ‡å®šć—ćŸę—„ć«ē§»å‹•", + "Unban from space": "ć‚¹ćƒšćƒ¼ć‚¹ć‹ć‚‰ć®ćƒ–ćƒ­ćƒƒć‚Æć‚’č§£é™¤", + "Ban from space": "ć‚¹ćƒšćƒ¼ć‚¹ć‹ć‚‰ćƒ–ćƒ­ćƒƒć‚Æ", + "Unban from room": "ćƒ«ćƒ¼ćƒ ć‹ć‚‰ć®ćƒ–ćƒ­ćƒƒć‚Æć‚’č§£é™¤", + "Ban from room": "ćƒ«ćƒ¼ćƒ ć‹ć‚‰ćƒ–ćƒ­ćƒƒć‚Æ", + "Copy link": "ćƒŖćƒ³ć‚Æć‚’ć‚³ćƒ”ćƒ¼", + "%(featureName)s Beta feedback": "%(featureName)sć®ćƒ™ćƒ¼ć‚æē‰ˆć®ćƒ•ć‚£ćƒ¼ćƒ‰ćƒćƒƒć‚Æ", + "Threads are a beta feature": "ć‚¹ćƒ¬ćƒƒćƒ‰ćÆćƒ™ćƒ¼ć‚æē‰ˆć®ę©Ÿčƒ½ć§ć™", + "Give feedback": "ćƒ•ć‚£ćƒ¼ćƒ‰ćƒćƒƒć‚Æ悒送äæ”", + "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.": "č­¦å‘Šļ¼šć‚ćŖćŸć®å€‹äŗŗęƒ…å ±ļ¼ˆęš—å·åŒ–ć®éµć‚’å«ć‚€ļ¼‰ćŒć€ć“ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«äæå­˜ć•ć‚Œć¦ć„ć¾ć™ć€‚ć“ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®ä½æē”Ø悒ēµ‚äŗ†ć™ć‚‹ć‹ć€ä»–ć®ć‚¢ć‚«ć‚¦ćƒ³ćƒˆć«ćƒ­ć‚°ć‚¤ćƒ³ć—ćŸć„å “合ćÆć€ćć®ćƒ‡ćƒ¼ć‚æć‚’ę¶ˆåŽ»ć—ć¦ćć ć•ć„ć€‚", + "The user's homeserver does not support the version of the space.": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ć®ćƒ›ćƒ¼ćƒ ć‚µćƒ¼ćƒćƒ¼ćÆć€ć“ć®ćƒćƒ¼ć‚øćƒ§ćƒ³ć®ć‚¹ćƒšćƒ¼ć‚¹ć‚’ć‚µćƒćƒ¼ćƒˆć—ć¦ć„ć¾ć›ć‚“ć€‚", + "User may or may not exist": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćŒå­˜åœØ恙悋恋äøę˜Žć§ć™", + "User does not exist": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćÆ存åœØć—ć¾ć›ć‚“", + "User is already in the room": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćÆę—¢ć«ćƒ«ćƒ¼ćƒ ć«å…„ć£ć¦ć„ć¾ć™", + "User is already in the space": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćÆę—¢ć«ć‚¹ćƒšćƒ¼ć‚¹ć«å…„ć£ć¦ć„ć¾ć™", + "User is already invited to the room": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćÆę—¢ć«ćƒ«ćƒ¼ćƒ ć«ę‹›å¾…ć•ć‚Œć¦ć„ć¾ć™", + "User is already invited to the space": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ćÆę—¢ć«ć‚¹ćƒšćƒ¼ć‚¹ć«ę‹›å¾…ć•ć‚Œć¦ć„ć¾ć™", + "You do not have permission to invite people to this space.": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‚’ć“ć®ć‚¹ćƒšćƒ¼ć‚¹ć«ę‹›å¾…ć™ć‚‹ęØ©é™ćŒć‚ć‚Šć¾ć›ć‚“ć€‚", + "Failed to invite users to %(roomName)s": "ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‚’%(roomName)sć«ę‹›å¾…ć™ć‚‹ć®ć«å¤±ę•—ć—ć¾ć—ćŸ", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "ć‚³ćƒŸćƒ„ćƒ‹ćƒ†ć‚£ćƒ¼ć®ćƒŖćƒ³ć‚Æļ¼ˆ%(groupId)sļ¼‰ć«ć‚¢ć‚Æć‚»ć‚¹ć—ć‚ˆć†ćØć—ć¦ć„ć¾ć™ć€‚
    ć‚³ćƒŸćƒ„ćƒ‹ćƒ†ć‚£ćƒ¼ę©Ÿčƒ½ćÆć‚¹ćƒšćƒ¼ć‚¹ć«ć‚ˆć‚Šē½®ćę›ćˆć‚‰ć‚Œć€ć‚µćƒćƒ¼ćƒˆå¤–恫ćŖć‚Šć¾ć—ćŸć€‚ć‚¹ćƒšćƒ¼ć‚¹ć«é–¢ć—ć¦ćÆć€ć“ć”ć‚‰ć‚’ć”å‚ē…§ćć ć•ć„怂", + "That link is no longer supported": "恓恮ćƒŖćƒ³ć‚ÆćÆć‚µćƒćƒ¼ćƒˆć•ć‚Œć¦ć„ć¾ć›ć‚“", + "%(value)ss": "%(value)sē§’", + "You can still join here.": "å‚åŠ ć§ćć¾ć™ć€‚", + "This invite was sent to %(email)s": "ę‹›å¾…ćŒ%(email)s恫送äæ”ć•ć‚Œć¾ć—ćŸ", + "This room or space does not exist.": "ć“ć®ćƒ«ćƒ¼ćƒ ć¾ćŸćÆć‚¹ćƒšćƒ¼ć‚¹ćÆ存åœØć—ć¾ć›ć‚“ć€‚", + "This room or space is not accessible at this time.": "ć“ć®ćƒ«ćƒ¼ćƒ ć¾ćŸćÆć‚¹ćƒšćƒ¼ć‚¹ćÆē¾åœØć‚¢ć‚Æć‚»ć‚¹ć§ćć¾ć›ć‚“ć€‚", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "ćƒ«ćƒ¼ćƒ ć¾ćŸćÆć‚¹ćƒšćƒ¼ć‚¹ć«ć‚¢ć‚Æć‚»ć‚¹ć™ć‚‹éš›ć«%(errcode)s恮ć‚Øćƒ©ćƒ¼ćŒē™ŗē”Ÿć—ć¾ć—ćŸć€‚ć‚Øćƒ©ćƒ¼ē™ŗē”Ÿę™‚ć«ć“ć®ćƒ”ćƒƒć‚»ćƒ¼ć‚ø恌č”Øē¤ŗć•ć‚Œć¦ć„ć‚‹ćŖć‚‰ć€ćƒć‚°ćƒ¬ćƒćƒ¼ćƒˆć‚’é€äæ”ć—ć¦ćć ć•ć„ć€‚", + "Stop sharing and close": "å…±ęœ‰ć‚’åœę­¢ć—ć¦é–‰ć˜ć‚‹", + "An error occured whilst sharing your live location": "位ē½®ęƒ…å ±ļ¼ˆćƒ©ć‚¤ćƒ–ļ¼‰ć‚’å…±ęœ‰ć™ć‚‹éš›ć«ć‚Øćƒ©ćƒ¼ćŒē™ŗē”Ÿć—ć¾ć—ćŸ", + "New room": "ę–°ć—ć„ćƒ«ćƒ¼ćƒ ", + "New video room": "ꖰ恗恄惓惇ć‚Ŗé€šč©±ćƒ«ćƒ¼ćƒ ", + "%(count)s participants|other": "%(count)säŗŗć®å‚åŠ č€…", + "%(count)s participants|one": "1äŗŗć®å‚åŠ č€…", + "Create a video room": "惓惇ć‚Ŗé€šč©±ćƒ«ćƒ¼ćƒ ć‚’ä½œęˆ", + "Create video room": "惓惇ć‚Ŗé€šč©±ćƒ«ćƒ¼ćƒ ć‚’ä½œęˆ", + "Create room": "ćƒ«ćƒ¼ćƒ ć‚’ä½œęˆ", + "Threads help keep your conversations on-topic and easy to track.": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć‚’ä½æ恆ćØć€ä¼šč©±ć®ćƒ†ćƒ¼ćƒžć‚’ē¶­ęŒć—ćŸć‚Šć€ä¼šč©±ć‚’ē°”å˜ć«čæ½č·”恗恟悊恙悋恓ćØćŒć§ćć¾ć™ć€‚", + "Keep discussions organised with threads.": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć‚’ä½æć£ć¦ć€ä¼šč©±ć‚’ć¾ćØć‚ć¾ć—ć‚‡ć†ć€‚", + "Threads help keep conversations on-topic and easy to track. Learn more.": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć‚’ä½æ恆ćØć€ä¼šč©±ć®ćƒ†ćƒ¼ćƒžć‚’ē¶­ęŒć—ćŸć‚Šć€ä¼šč©±ć‚’ē°”å˜ć«čæ½č·”恗恟悊恙悋恓ćØćŒć§ćć¾ć™ć€‚č©³ć—ćēŸ„悋怂", + "Beta feature. Click to learn more.": "ćƒ™ćƒ¼ć‚æē‰ˆć®ę©Ÿčƒ½ć§ć™ć€‚ć‚ÆćƒŖ惃ć‚Æ恙悋ćØč©³ē“°ć‚’č”Øē¤ŗć—ć¾ć™ć€‚", + "Partial Support for Threads": "ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ć®éƒØ分ēš„ć‚µćƒćƒ¼ćƒˆ", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "ćƒ›ćƒ¼ćƒ ć‚µćƒ¼ćƒćƒ¼ćŒć‚µćƒćƒ¼ćƒˆć—ć¦ć„ćŖć„ćŸć‚ć€ć‚¹ćƒ¬ćƒƒćƒ‰ę©Ÿčƒ½ćÆäøå®‰å®šć‹ć‚‚ć—ć‚Œć¾ć›ć‚“ć€‚ć‚¹ćƒ¬ćƒƒćƒ‰ć®ćƒ”ćƒƒć‚»ćƒ¼ć‚øćÆå®‰å®šć—ć¦č”Øē¤ŗ恕悌ćŖć„ćŠćć‚ŒćŒć‚ć‚Šć¾ć™ć€‚č©³ć—ćēŸ„悋怂" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 5b8ab905114..467bb0badaa 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1271,7 +1271,7 @@ "Find a roomā€¦": "Zoek een kamerā€¦", "Find a roomā€¦ (e.g. %(exampleRoom)s)": "Zoek een kamerā€¦ (bv. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u de kamer niet kunt vinden is het mogelijk privĆ©, vraag dan om een uitnodiging of maak een nieuwe kamer aan.", - "Explore rooms": "Kamers verkennen", + "Explore rooms": "Kamers ontdekken", "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen", "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?", @@ -2765,7 +2765,7 @@ "%(seconds)ss left": "%(seconds)s's over", "Change server ACLs": "Wijzig server ACL's", "Show options to enable 'Do not disturb' mode": "Toon opties om de 'Niet storen' modus in te schakelen", - "You can select all or individual messages to retry or delete": "U kunt alles selecteren of per individueel bericht opnieuw verzenden of verwijderen", + "You can select all or individual messages to retry or delete": "U kunt alles selecteren of per individueel bericht opnieuw versturen of verwijderen", "Sending": "Wordt verstuurd", "Retry all": "Alles opnieuw proberen", "Delete all": "Verwijder alles", @@ -2775,7 +2775,7 @@ "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s", "View all %(count)s members|one": "1 lid bekijken", "View all %(count)s members|other": "Bekijk alle %(count)s personen", - "Failed to send": "Verzenden is mislukt", + "Failed to send": "Versturen is mislukt", "Enter your Security Phrase a second time to confirm it.": "Voor uw veiligheidswachtwoord een tweede keer in om het te bevestigen.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Kies een kamer of gesprek om hem toe te voegen. Dit is een Space voor u, niemand zal hiervan een melding krijgen. U kan er later meer toevoegen.", "What do you want to organise?": "Wat wilt u organiseren?", @@ -3530,7 +3530,7 @@ "Message pending moderation": "Bericht in afwachting van moderatie", "Message pending moderation: %(reason)s": "Bericht in afwachting van moderatie: %(reason)s", "Remove from room": "Verwijderen uit kamer", - "Failed to remove user": "Kan gebruiker niet verwijderen", + "Failed to remove user": "Kan persoon niet verwijderen", "Remove them from specific things I'm able to": "Verwijder ze van specifieke dingen die ik kan", "Remove them from everything I'm able to": "Verwijder ze van alles wat ik kan", "Remove from %(roomName)s": "Verwijderen uit %(roomName)s", @@ -3541,10 +3541,10 @@ "You don't have permission to view messages from before you joined.": "U heeft geen toestemming om berichten te bekijken voor voordat u lid word.", "You don't have permission to view messages from before you were invited.": "U bent niet gemachtigd om berichten te bekijken van voordat u werd uitgenodigd.", "From a thread": "Uit een conversatie", - "Remove users": "Gebruikers verwijderen", + "Remove users": "Personen verwijderen", "Keyboard": "Toetsenbord", "Widget": "Widget", - "Automatically send debug logs on decryption errors": "Automatisch foutopsporingslogboeken verzenden bij decoderingsfouten", + "Automatically send debug logs on decryption errors": "Automatisch foutopsporingslogboeken versturen bij decoderingsfouten", "Show join/leave messages (invites/removes/bans unaffected)": "Toon deelname/laat berichten (uitnodigingen/verwijderingen/bans onaangetast)", "Enable location sharing": "Locatie delen inschakelen", "Show extensible event representation of events": "Toon uitbreidbare gebeurtenisweergave van gebeurtenissen", @@ -3556,7 +3556,7 @@ "%(senderName)s has shared their location": "%(senderName)s heeft zijn locatie gedeeld", "%(senderName)s removed %(targetName)s": "%(senderName)s verwijderd %(targetName)s", "%(senderName)s removed %(targetName)s: %(reason)s": "%(senderName)s verwijderd %(targetName)s: %(reason)s", - "Removes user with given id from this room": "Verwijdert gebruiker met opgegeven ID uit deze kamer", + "Removes user with given id from this room": "Verwijder persoon met opgegeven ID uit deze kamer", "Previous autocomplete suggestion": "Vorige suggestie voor automatisch aanvullen", "Next autocomplete suggestion": "Volgende suggestie voor automatisch aanvullen", "Previous room or DM": "Vorige kamer of DM", @@ -3587,7 +3587,7 @@ "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!": "Als u weet wat u doet, Element is open-source, bekijk dan zeker onze GitHub (https://github.com/vector-im/element-web/) en draag bij!", "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!": "Als iemand u heeft gezegd iets hier te kopiĆ«ren/plakken, is de kans groot dat u wordt opgelicht!", "Wait!": "Wacht!", - "Unable to check if username has been taken. Try again later.": "Kan niet controleren of gebruikersnaam is gebruikt. Probeer het later nog eens.", + "Unable to check if username has been taken. Try again later.": "Kan niet controleren of inlognaam is gebruikt. Probeer het later nog eens.", "This address does not point at this room": "Dit adres verwijst niet naar deze kamer", "Pick a date to jump to": "Kies een datum om naar toe te springen", "Jump to date": "Spring naar datum", @@ -3619,7 +3619,7 @@ "<%(count)s spaces>|other": "<%(count)s spaces>", "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)sverzond een verborgen bericht", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)sverzond %(count)s verborgen berichten", - "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)sverzond %(count)s verborgen bericht", + "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)sverzond verborgen bericht", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)sverzond %(count)s verborgen berichten", "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sheeft een bericht verwijderd", "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sverwijderde %(count)s berichten", @@ -3628,7 +3628,7 @@ "%(severalUsers)schanged the pinned messages for the room %(count)s times.|one": "%(severalUsers)s hebben de vastgezette berichten voor de kamer gewijzigd.", "Maximise": "Maximaliseren", "You do not have permissions to add spaces to this space": "U bent niet gemachtigd om spaces aan deze space toe te voegen", - "Automatically send debug logs when key backup is not functioning": "Automatisch foutopsporingslogboeken verzenden wanneer de sleutelback-up niet werkt", + "Automatically send debug logs when key backup is not functioning": "Automatisch foutopsporingslogboeken versturen wanneer de sleutelback-up niet werkt", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Bedankt voor het proberen van de bĆØta. Ga alsjeblieft zo gedetailleerd mogelijk in op de details zodat we deze kunnen verbeteren.", "To leave, just return to this page or click on the beta badge when you search.": "Om te vertrekken, keert u gewoon terug naar deze pagina of klikt u op het bĆØta label tijdens het zoeken.", "How can I leave the beta?": "Hoe kan ik de bĆØta verlaten?", @@ -3638,5 +3638,206 @@ "A new, quick way to search spaces and rooms you're in.": "Een nieuwe, snelle manier om spaces en kamers te zoeken waarin u zich bevindt.", "The new search": "De nieuwe zoekopdracht", "New search experience": "Nieuwe zoekervaring", - "%(space1Name)s and %(space2Name)s": "%(space1Name)s en %(space2Name)s" + "%(space1Name)s and %(space2Name)s": "%(space1Name)s en %(space2Name)s", + "You do not have permission to invite people to this space.": "U bent niet gemachtigd om mensen voor deze space uit te nodigen.", + "No virtual room for this room": "Geen virtuele ruimte voor deze ruimte", + "Switches to this room's virtual room, if it has one": "Schakelt over naar de virtuele kamer van deze kamer, als die er is", + "Failed to invite users to %(roomName)s": "Kan personen niet uitnodigen voor %(roomName)s", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "U probeert toegang te krijgen tot een communitylink (%(groupId)s).
    Communities worden niet langer ondersteund en zijn vervangen door spaces.Lees hier meer over spaces.", + "That link is no longer supported": "Deze link wordt niet langer ondersteund", + "%(value)ss": "%(value)ss", + "%(value)sm": "%(value)sm", + "%(value)sh": "%(value)sh", + "%(value)sd": "%(value)sd", + "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Als deze pagina identificeerbare informatie bevat, zoals een kamer, persoon-ID, worden die gegevens verwijderd voordat ze naar de server worden verzonden.", + "Observe only": "Alleen observeren", + "Requester": "Aanvrager", + "Methods": "Methoden", + "Timeout": "Time-out", + "Phase": "Fase", + "Transaction": "Transactie", + "Cancelled": "Geannuleerd", + "Started": "Begonnen", + "Ready": "Gereed", + "Requested": "Aangevraagd", + "Unsent": "niet verstuurd", + "Edit values": "Bewerk waarden", + "Failed to save settings.": "Kan instellingen niet opslaan.", + "Number of users": "Aantal personen", + "Server": "Server", + "Server Versions": "Serverversies", + "Client Versions": "cliĆ«ntversies", + "Failed to load.": "Laden mislukt.", + "Capabilities": "Mogelijkheden", + "Send custom state event": "Aangepaste statusgebeurtenis versturen", + "Failed to send event!": "Kan gebeurtenis niet versturen!", + "Doesn't look like valid JSON.": "Lijkt niet op geldige JSON.", + "Send custom room account data event": "Gegevensgebeurtenis van aangepaste kamer account versturen", + "Send custom account data event": "Aangepaste accountgegevens gebeurtenis versturen", + "Search Dialog": "Dialoogvenster Zoeken", + "Join %(roomAddress)s": "%(roomAddress)s toetreden", + "Export Cancelled": "Export geannuleerd", + "Room ID: %(roomId)s": "Kamer ID: %(roomId)s", + "Server info": "Server info", + "Settings explorer": "Instellingen verkenner", + "Explore account data": "Accountgegevens ontdekken", + "Verification explorer": "Verificatie verkenner", + "View servers in room": "Servers in de kamer bekijken", + "Explore room account data": "Kamer accountgegevens ontdekken", + "Explore room state": "Kamerstatus ontdekken", + "Send custom timeline event": "Aangepaste tijdlijngebeurtenis versturen", + "Create room": "Ruimte aanmaken", + "Create video room": "Videokamer maken", + "Create a video room": "CreĆ«er een videokamer", + "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile changeā€¦)": "Schakel het vinkje uit als u ook systeemberichten van deze persoon wilt verwijderen (bijv. lidmaatschapswijziging, profielwijziging...)", + "Preserve system messages": "Systeemberichten behouden", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "U staat op het punt %(count)s bericht te verwijderen door %(user)s. Hierdoor worden ze permanent verwijderd voor iedereen in het gesprek. Wilt u doorgaan?", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "U staat op het punt %(count)s berichten te verwijderen door %(user)s. Hierdoor worden ze permanent verwijderd voor iedereen in het gesprek. Wilt u doorgaan?", + "%(featureName)s Beta feedback": "%(featureName)s BĆØta-feedback", + "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Help ons problemen te identificeren en %(analyticsOwner)s te verbeteren door anonieme gebruiksgegevens te delen. Om inzicht te krijgen in hoe mensen meerdere apparaten gebruiken, genereren we een willekeurige identificatie die door uw apparaten wordt gedeeld.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "U kunt de aangepaste serveropties gebruiken om u aan te melden bij andere Matrix-servers door een andere server-URL op te geven. Hierdoor kunt u %(brand)s gebruiken met een bestaand Matrix-account op een andere thuisserver.", + "Results are only revealed when you end the poll": "Resultaten worden pas onthuld als u de poll beĆ«indigt", + "Voters see results as soon as they have voted": "Kiezers zien resultaten zodra ze hebben gestemd", + "Closed poll": "Gesloten poll", + "Open poll": "Start poll", + "Poll type": "Poll type", + "Edit poll": "Bewerk poll", + "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)sheeft de vastgezette berichten voor de kamer gewijzigd", + "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)sheeft de vastgezette berichten voor de kamer %(count)s keer gewijzigd", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)shebben de vastgezette berichten voor de kamer gewijzigd", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)sheeft de vastgezette berichten voor de kamer %(count)s keer gewijzigd", + "What location type do you want to share?": "Welk locatietype wilt u delen?", + "Drop a Pin": "Zet een pin neer", + "My live location": "Mijn live locatie", + "My current location": "Mijn huidige locatie", + "%(displayName)s's live location": "De live locatie van %(displayName)s", + "We couldn't send your location": "We kunnen uw locatie niet versturen", + "%(brand)s could not send your location. Please try again later.": "%(brand)s kan uw locatie niet versturen. Probeer het later opnieuw.", + "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s heeft geen toestemming gekregen om uw locatie op te halen. Sta locatietoegang toe in uw browserinstellingen.", + "Click to drop a pin": "Klik om een pin neer te zetten", + "Click to move the pin": "Klik om de pin te verplaatsen", + "Share for %(duration)s": "Delen voor %(duration)s", + "Results will be visible when the poll is ended": "Resultaten zijn zichtbaar wanneer de poll is afgelopen", + "Sorry, you can't edit a poll after votes have been cast.": "Sorry, u kunt een poll niet bewerken nadat er gestemd is.", + "Can't edit poll": "Kan poll niet bewerken", + "Shared a location: ": "Een locatie gedeeld: ", + "Shared their location: ": "Hun locatie gedeeld: ", + "Unable to load map": "Kan kaart niet laden", + "Click": "Klik", + "Expand quotes": "Citaten uitvouwen", + "Collapse quotes": "Citaten invouwen", + "Beta feature. Click to learn more.": "BĆØtafunctie. Klik om meer te leren.", + "Beta feature": "BĆØtafunctie", + "Can't create a thread from an event with an existing relation": "Kan geen discussie maken van een gebeurtenis met een bestaande relatie", + "Pinned": "Vastgezet", + "Open thread": "Open discussie", + "%(count)s participants|one": "1 deelnemer", + "%(count)s participants|other": "%(count)s deelnemers", + "Connected": "Verbonden", + "Video": "Video", + "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s is geretourneerd tijdens een poging om toegang te krijgen tot de kamer of space. Als u denkt dat je dit bericht ten onrechte ziet, dien dan een bugrapport in.", + "Try again later, or ask a room or space admin to check if you have access.": "Probeer het later opnieuw of vraag een kamer- of space beheerder om te controleren of u toegang heeft.", + "This room or space is not accessible at this time.": "Deze kamer of space is op dit moment niet toegankelijk.", + "Are you sure you're at the right place?": "Weet u zeker dat je op de goede locatie bent?", + "This room or space does not exist.": "Deze kamer of space bestaat niet.", + "There's no preview, would you like to join?": "Er is geen preview, wilt u toetreden?", + "This invite was sent to %(email)s": "De uitnodiging is verzonden naar %(email)s", + "This invite was sent to %(email)s which is not associated with your account": "Deze uitnodiging is verzonden naar %(email)s die niet is gekoppeld aan uw account", + "You can still join here.": "U kunt hier nog toetreden.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "Er is een fout (%(errcode)s) geretourneerd tijdens het valideren van uw uitnodiging. U kunt proberen deze informatie door te geven aan de persoon die u heeft uitgenodigd.", + "Something went wrong with your invite.": "Er is iets misgegaan met uw uitnodiging.", + "You were banned by %(memberName)s": "U bent verbannen door %(memberName)s", + "Forget this space": "Vergeet deze space", + "You were removed by %(memberName)s": "U bent verwijderd door %(memberName)s", + "Loading preview": "Voorbeeld laden", + "Joining ā€¦": "Deelnemenā€¦", + "Currently removing messages in %(count)s rooms|one": "Momenteel berichten in %(count)s kamer aan het verwijderen", + "Currently removing messages in %(count)s rooms|other": "Momenteel berichten in %(count)s kamers aan het verwijderen", + "New video room": "Nieuwe video kamer", + "New room": "Nieuwe kamer", + "Busy": "Bezet", + "Remove messages sent by me": "Door mij verzonden berichten verwijderen", + "View older version of %(spaceName)s.": "Bekijk oudere versie van %(spaceName)s.", + "Upgrade this space to the recommended room version": "Upgrade deze ruimte naar de aanbevolen kamerversie", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Foutopsporingslogboeken bevatten toepassingsgebruiksgegevens, waaronder uw inlognaam, de ID's of aliassen van de kamers die u heeft bezocht, met welke UI-elementen u voor het laatst interactie heeft gehad en de inlognamen van andere personen. Ze bevatten geen berichten.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "Als u een bug via GitHub heeft ingediend, kunnen foutopsporingslogboeken ons helpen het probleem op te sporen. ", + "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "Spaces zijn een nieuwe manier om kamers en mensen te groeperen. Wat voor ruimte wilt u aanmaken? U kunt dit later wijzigen.", + "Match system": "Match systeem", + "Developer tools": "Ontwikkelaarstools", + "sends hearts": "stuurt hartjes", + "Sends the given message with hearts": "Stuurt het bericht met hartjes", + "Insert a trailing colon after user mentions at the start of a message": "Voeg een dubbele punt in nadat de persoon het aan het begin van een bericht heeft vermeld", + "Show polls button": "Toon polls-knop", + "Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Live locatie delen - deel huidige locatie (actieve ontwikkeling, en tijdelijk, locaties blijven bestaan in kamergeschiedenis)", + "Location sharing - pin drop (under active development)": "Locatie delen - pin drop (in actieve ontwikkeling)", + "Show current avatar and name for users in message history": "Toon huidige avatar en naam voor persoon in berichtgeschiedenis", + "Video rooms (under active development)": "Videokamers (in actieve ontwikkeling)", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Om te verlaten, keer terug naar deze pagina en gebruik de knop \"Verlaat de bĆØta\".", + "Use \"Reply in thread\" when hovering over a message.": "Gebruik \"Beantwoorden in gesprek\" wanneer u de muisaanwijzer op een bericht plaatst.", + "How can I start a thread?": "Hoe kan ik een discussie starten?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Discussies helpen om gesprekken on-topic te houden en gemakkelijk te volgen. Meer informatie.", + "Keep discussions organised with threads.": "Houd discussies georganiseerd met discussielijnen.", + "Failed to join": "Kan niet deelnemen", + "The person who invited you has already left, or their server is offline.": "De persoon die u heeft uitgenodigd is al vertrokken, of zijn server is offline.", + "The person who invited you has already left.": "De persoon die u heeft uitgenodigd is al vertrokken.", + "Sorry, your homeserver is too old to participate here.": "Sorry, uw server is te oud om hier deel te nemen.", + "There was an error joining.": "Er is een fout opgetreden bij het deelnemen.", + "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.": "%(brand)s is experimenteel in een mobiele webbrowser. Gebruik onze gratis native app voor een betere ervaring en de nieuwste functies.", + "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "Deze server is niet correct geconfigureerd om kaarten weer te geven, of de geconfigureerde kaartserver is mogelijk onbereikbaar.", + "The user's homeserver does not support the version of the space.": "De server van de persoon ondersteunt de versie van de ruimte niet.", + "This homeserver is not configured to display maps.": "Deze server is niet geconfigureerd om kaarten weer te geven.", + "User may or may not exist": "Persoon kan wel of niet bestaan", + "User does not exist": "Persoon bestaat niet", + "User is already invited to the room": "Persoon is al uitgenodigd voor de kamer", + "User is already in the room": "Persoon is al in de kamer", + "User is already in the space": "Persoon is al in de space", + "User is already invited to the space": "Persoon is al uitgenodigd voor de space", + "Toggle Code Block": "Codeblok wisselen", + "Toggle Link": "Koppeling wisselen", + "Accessibility": "Toegankelijkheid", + "Event ID: %(eventId)s": "Gebeurtenis ID: %(eventId)s", + "Give feedback": "Feedback geven", + "Threads are a beta feature": "Discussies zijn een bĆØtafunctie", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: gebruik \"Antwoord in discussie\" wanneer u de muisaanwijzer op een bericht plaatst.", + "Threads help keep your conversations on-topic and easy to track.": "Discussies helpen u gesprekken on-topic te houden en gemakkelijk bij te houden.", + "Reply to an ongoing thread or use ā€œ%(replyInThread)sā€ when hovering over a message to start a new one.": "Reageer op een lopende discussie of gebruik \"%(replyInThread)s\" wanneer u de muisaanwijzer op een bericht plaatst om een nieuwe te starten.", + "We'll create rooms for each of them.": "We zullen kamers voor elk van hen maken.", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "Als u de kamer die u zoekt niet kunt vinden, vraag dan om een uitnodiging of maak een nieuwe kamer aan.", + "This will be a one-off transition, as threads are now part of the Matrix specification.": "Dit zal een eenmalige overgang zijn, aangezien discussies nu deel uitmaken van de Matrix-specificatie.", + "As we prepare for it, we need to make some changes: threads created before this point will be displayed as regular replies.": "Terwijl we ons erop voorbereiden, moeten we enkele wijzigingen aanbrengen: discussies die vĆ³Ć³r dit punt zijn gemaakt, worden weergegeven als gewone antwoorden.", + "We're getting closer to releasing a public Beta for Threads.": "We komen dichter bij de release van een publieke bĆØtaversie voor Discussies.", + "Threads Approaching Beta šŸŽ‰": "Discussies naderen bĆØta šŸŽ‰", + "Stop sharing and close": "Stop met delen en sluit", + "Stop sharing": "Stop delen", + "An error occurred while stopping your live location, please try again": "Er is een fout opgetreden bij het stoppen van uw live locatie, probeer het opnieuw", + "An error occured whilst sharing your live location, please try again": "Er is een fout opgetreden bij het delen van uw live locatie, probeer het opnieuw", + "%(timeRemaining)s left": "%(timeRemaining)s over", + "You are sharing your live location": "U deelt uw live locatie", + "An error occured whilst sharing your live location": "Er is een fout opgetreden bij het delen van uw live locatie", + "No verification requests found": "Geen verificatieverzoeken gevonden", + "Open user settings": "Open persooninstellingen", + "Switch to space by number": "Overschakelen naar space op nummer", + "Next recently visited room or space": "Volgende recent bezochte kamer of space", + "Previous recently visited room or space": "Vorige recent bezochte kamer of ruimte", + "Live location enabled": "Live locatie ingeschakeld", + "Live location error": "Live locatie error", + "Live location ended": "Live locatie beĆ«indigd", + "Loading live location...": "Live locatie laden...", + "Live until %(expiryTime)s": "Live tot %(expiryTime)s", + "View live location": "Bekijk live locatie", + "Ban from room": "Verban van kamer", + "Unban from room": "Ontban van kamer", + "Ban from space": "Verban van space", + "Unban from space": "Unban van space", + "Disinvite from room": "Uitnodiging van kamer afwijzen", + "Remove from space": "Verwijder van space", + "Disinvite from space": "Uitnodiging van space afwijzen", + "Confirm signing out these devices|one": "Uitloggen van dit apparaat bevestigen", + "Confirm signing out these devices|other": "Uitloggen van deze apparaten bevestigen", + "Yes, enable": "Ja, inschakelen", + "Do you want to enable threads anyway?": "Wil je toch discussies inschakelen?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. Meer informatie.", + "Partial Support for Threads": "Gedeeltelijke ondersteuning voor Discussies", + "Right-click message context menu": "Rechtermuisknop op het bericht voor opties", + "Jump to the given date in the timeline": "Spring naar de opgegeven datum in de tijdlijn" } diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 8f9de471400..37804f2f1c0 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -387,7 +387,7 @@ "Failed to reject invitation": "Nepodarilo sa odmietnuÅ„ pozvanie", "Are you sure you want to leave the room '%(roomName)s'?": "Ste si istĆ­, že chcete opustiÅ„ miestnosÅ„ '%(roomName)s'?", "Signed Out": "Ste odhlĆ”senĆ­", - "For security, this session has been signed out. Please sign in again.": "KĆ“li bezpečnosti ste boli odhlĆ”senĆ­ z tejto relĆ”cie. ProsĆ­m, prihlĆ”ste sa znovu.", + "For security, this session has been signed out. Please sign in again.": "Z bezpečnostnĆ½ch dĆ“vodov bola tĆ”to relĆ”cia odhlĆ”senĆ”. ProsĆ­m, prihlĆ”ste sa znova.", "Logout": "OdhlĆ”siÅ„ sa", "Your Communities": "VaÅ”e komunity", "You're not currently a member of any communities.": "V sĆŗčasnosti nie ste členom žiadnej komunity.", @@ -830,69 +830,69 @@ "Verify this user by confirming the following emoji appear on their screen.": "Overte tohto pouÅ¾Ć­vateľa potvrdenĆ­m, že sa na jeho obrazovke zobrazujĆŗ nasledujĆŗce emotikony.", "Verify this user by confirming the following number appears on their screen.": "Overte tohoto pouÅ¾Ć­vateľa tĆ½m, že zistĆ­te, či sa na jeho obrazovke objavĆ­ nasledujĆŗce čƭslo.", "Unable to find a supported verification method.": "Nie je možnĆ© nĆ”jsÅ„ podporovanĆŗ metĆ³du overenia.", - "Dog": "Hlava psa", - "Cat": "Hlava mačky", - "Lion": "Hlava leva", + "Dog": "Pes", + "Cat": "Mačka", + "Lion": "Lev", "Horse": "KĆ“Åˆ", - "Unicorn": "Hlava jednorožca", - "Pig": "Hlava PrasaÅ„a", + "Unicorn": "Jednorožec", + "Pig": "Prasa", "Elephant": "Slon", - "Rabbit": "Hlava Zajaca", - "Panda": "Hlava Pandy", + "Rabbit": "Zajac", + "Panda": "Panda", "Rooster": "KohĆŗt", "Penguin": "Tučniak", "Turtle": "Korytnačka", "Fish": "Ryba", "Octopus": "Chobotnica", "Butterfly": "MotĆ½Ä¾", - "Flower": "TulipĆ”n", - "Tree": "ListnatĆ½ strom", + "Flower": "Kvet", + "Tree": "Strom", "Cactus": "Kaktus", "Mushroom": "Huba", "Globe": "Zemeguľa", - "Moon": "Polmesiac", + "Moon": "Mesiac", "Cloud": "Oblak", "Fire": "Oheň", "Banana": "BanĆ”n", - "Apple": "ČervenĆ© jablko", + "Apple": "Jablko", "Strawberry": "Jahoda", - "Corn": "KukuričnĆ½ klas", + "Corn": "Kukurica", "Pizza": "Pizza", - "Cake": "NarodeninovĆ” torta", - "Heart": "ČervenĆ© srdce", - "Smiley": "Å keriaca sa tvĆ”r", + "Cake": "Torta", + "Heart": "Srdce", + "Smiley": "SmajlĆ­k", "Robot": "Robot", - "Hat": "Cylinder", + "Hat": "KlobĆŗk", "Glasses": "Okuliare", - "Spanner": "FrancĆŗzsky kľĆŗč", - "Santa": "Santa Claus", - "Thumbs up": "palec nahor", + "Spanner": "VidlicovĆ½ kľĆŗč", + "Santa": "MikulĆ”Å”", + "Thumbs up": "Palec nahor", "Umbrella": "DĆ”Å¾dnik", "Hourglass": "PresĆ½pacie hodiny", "Clock": "BudĆ­k", - "Gift": "ZabalenĆ½ darček", + "Gift": "Darček", "Light bulb": "Žiarovka", - "Book": "ZatvorenĆ” kniha", + "Book": "Kniha", "Pencil": "Ceruzka", - "Paperclip": "Sponka na papier", + "Paperclip": "KancelĆ”rska sponka", "Scissors": "Nožnice", "Key": "KľĆŗč", "Hammer": "Kladivo", "Telephone": "TelefĆ³n", - "Flag": "KockovanĆ” zĆ”stava", - "Train": "RuÅ”eň", + "Flag": "ZĆ”stava", + "Train": "Vlak", "Bicycle": "Bicykel", "Aeroplane": "Lietadlo", "Rocket": "Raketa", "Trophy": "Trofej", - "Ball": "Futbal", + "Ball": "Lopta", "Guitar": "Gitara", "Trumpet": "TrĆŗbka", - "Bell": "Zvon", + "Bell": "Zvonec", "Anchor": "Kotva", "Headphones": "SlĆŗchadlĆ”", "Folder": "Fascikel", - "Pin": "PripnĆŗÅ„", + "Pin": "Å pendlĆ­k", "Yes": "Ɓno", "No": "Nie", "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Poslali sme vĆ”m email, aby sme mohli overiÅ„ vaÅ”u adresu. Postupujte podľa odoslanĆ½ch inÅ”trukciĆ­ a potom klepnite na nižŔie zobrazenĆ© tlačidlo.", @@ -1273,7 +1273,7 @@ "They match": "ZhodujĆŗ sa", "They don't match": "NezhodujĆŗ sa", "To be secure, do this in person or use a trusted way to communicate.": "Aby ste si boli istĆ½, urobte to osobne alebo použite dĆ“veryhodnĆ½ spĆ“sob komunikĆ”cie.", - "Lock": "ZĆ”mok", + "Lock": "ZĆ”mka", "If you can't scan the code above, verify by comparing unique emoji.": "Ak sa vĆ”m nepodarĆ­ naskenovaÅ„ uvedenĆ½ kĆ³d, overte pomocou porovnania jedinečnĆ½ch emotikonov.", "Verify by comparing unique emoji.": "Overenie porovnanĆ­m jedinečnej kombinĆ”cie emotikonov.", "Verify by emoji": "OveriÅ„ pomocou emotikonov", @@ -1333,7 +1333,7 @@ "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", "Help us improve %(brand)s": "PomĆ“Å¾te nĆ”m zlepÅ”ovaÅ„ %(brand)s", - "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "PosielaÅ„ anonymnĆ© dĆ”ta o pouÅ¾Ć­vanĆ­, ktorĆ© nĆ”m pomĆ“Å¾u zlepÅ”iÅ„ %(brand)s. Toto bude vyžadovaÅ„ suÅ”ienku.", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "PosielaÅ„ anonymnĆ© dĆ”ta o pouÅ¾Ć­vanĆ­, ktorĆ© nĆ”m pomĆ“Å¾u zlepÅ”iÅ„ %(brand)s. Toto bude vyžadovaÅ„ kolĆ”Äik.", "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 server admin.": "Kontaktujte svojho administrĆ”tora serveru.", @@ -1394,8 +1394,8 @@ "View rules": "ZobraziÅ„ pravidlĆ”", "You are currently subscribed to:": "AktuĆ”lne odoberĆ”te:", "āš  These settings are meant for advanced users.": "āš  Tieto nastavenia sĆŗ určenĆ© pre pokročilĆ½ch pouÅ¾Ć­vateľov.", - "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Pridajte pouÅ¾Ć­vateľov a servery, ktorĆ½ch chcete ignorovaÅ„. Použite hviezdičku '*', aby %(brand)s ju priradil každĆ©mu symbolu. NaprĆ­klad @bot:* by odignoroval vÅ”etkĆ½ch pouÅ¾Ć­vateľov s menom 'bot' na akomkoľvek serveri.", - "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.": "Ignorovanie ľudĆ­ sa vykonĆ”va prostrednĆ­ctvom zoznamov zĆ”kazov, ktorĆ© obsahujĆŗ pravidlĆ” pre zakazovanie. PrihlĆ”senie sa na zoznam zĆ”kazov znamenĆ”, že pouÅ¾Ć­vatelia/servery zablokovanĆ© tĆ½mto zoznamom budĆŗ pred vami skrytĆ©.", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Pridajte pouÅ¾Ć­vateľov a servery, ktorĆ½ch chcete ignorovaÅ„. Použite hviezdičku '*', aby ju %(brand)s priradil každĆ©mu symbolu. NaprĆ­klad @bot:* by odignoroval vÅ”etkĆ½ch pouÅ¾Ć­vateľov s menom 'bot' na akomkoľvek serveri.", + "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.": "Ignorovanie ľudĆ­ sa vykonĆ”va prostrednĆ­ctvom zoznamov zĆ”kazov, ktorĆ© obsahujĆŗ pravidlĆ” pre zakazovanie. Pridanie na zoznam zĆ”kazov znamenĆ”, že pouÅ¾Ć­vatelia/servery na tomto zozname pred vami skrytĆ©.", "Personal ban list": "OsobnĆ½ zoznam zĆ”kazov", "Server or user ID to ignore": "Server alebo ID pouÅ¾Ć­vateľa na odignorovanie", "eg: @bot:* or example.org": "napr.: @bot:* alebo napriklad.sk", @@ -3749,5 +3749,41 @@ "%(count)s participants|other": "%(count)s ĆŗčastnĆ­kov", "New video room": "NovĆ” video miestnosÅ„", "New room": "NovĆ” miestnosÅ„", - "Video rooms (under active development)": "Video miestnosti (v aktĆ­vnom vĆ½voji)" + "Video rooms (under active development)": "Video miestnosti (v aktĆ­vnom vĆ½voji)", + "Give feedback": "PoskytnĆŗÅ„ spƤtnĆŗ vƤzbu", + "Threads are a beta feature": "VlĆ”kna sĆŗ beta funkciou", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Použite položku \"OdpovedaÅ„ vo vlĆ”kne\", keď prejdete nad sprĆ”vu.", + "Threads help keep your conversations on-topic and easy to track.": "VlĆ”kna pomĆ”hajĆŗ udržiavaÅ„ konverzĆ”cie v tĆ©me a uľahčujĆŗ ich sledovanie.", + "%(featureName)s Beta feedback": "%(featureName)s Beta spƤtnĆ” vƤzba", + "Beta feature. Click to learn more.": "Beta funkcia. KliknutĆ­m sa dozviete viac.", + "Beta feature": "Beta funkcia", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Ak chcete odĆ­sÅ„, vrĆ”Å„te sa na tĆŗto strĆ”nku a použite tlačidlo \"OpustiÅ„ beta verziu\".", + "Use \"Reply in thread\" when hovering over a message.": "Použite položku \"OdpovedaÅ„ vo vlĆ”kne\", keď prejdete nad sprĆ”vu.", + "How can I start a thread?": "Ako mĆ“Å¾em začaÅ„ vlĆ”kno?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "VlĆ”kna pomĆ”hajĆŗ udržiavaÅ„ konverzĆ”cie v tĆ©me a uľahčujĆŗ ich sledovanie. Zistite viac.", + "Keep discussions organised with threads.": "Udržujte diskusie organizovanĆ© pomocou vlĆ”kien.", + "sends hearts": "poÅ”le srdiečka", + "Sends the given message with hearts": "OdoÅ”le danĆŗ sprĆ”vu so srdiečkami", + "Confirm signing out these devices|one": "Potvrďte odhlĆ”senie z tohto zariadenia", + "Confirm signing out these devices|other": "PotvrdiÅ„ odhlĆ”senie tĆ½chto zariadenĆ­", + "Live location ended": "Ukončenie polohy v reĆ”lnom čase", + "Loading live location...": "Načƭtavanie polohy v reĆ”lnom čase...", + "View live location": "ZobraziÅ„ polohu v reĆ”lnom čase", + "Live until %(expiryTime)s": "Poloha v reĆ”lnom čase do %(expiryTime)s", + "Live location enabled": "Poloha v reĆ”lnom čase zapnutĆ”", + "Live location error": "Chyba polohy v reĆ”lnom čase", + "Ban from room": "ZakĆ”zaÅ„ vstup do miestnosti", + "Unban from room": "ZruÅ”iÅ„ zĆ”kaz vstupu do miestnosti", + "Unban from space": "ZruÅ”iÅ„ zĆ”kaz vstupu do priestoru", + "Ban from space": "ZakĆ”zaÅ„ vstup do priestoru", + "Yes, enable": "Ɓno, povoliÅ„", + "Do you want to enable threads anyway?": "Chcete aj napriek tomu povoliÅ„ vlĆ”kna?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "VĆ”Å” domovskĆ½ server v sĆŗčasnosti nepodporuje vlĆ”kna, takže tĆ”to funkcia mĆ“Å¾e byÅ„ nespoľahlivĆ”. NiektorĆ© sprĆ”vy vo vlĆ”knach nemusia byÅ„ spoľahlivo dostupnĆ©. ZĆ­skajte viac informĆ”ciĆ­.", + "Partial Support for Threads": "ČiastočnĆ” podpora vlĆ”kien", + "Jump to the given date in the timeline": "PrejsÅ„ na zadanĆ½ dĆ”tum na časovej osi", + "Copy link": "KopĆ­rovaÅ„ odkaz", + "Right-click message context menu": "KontextovĆ© menu sprĆ”vy pravĆ½m kliknutĆ­m", + "Disinvite from room": "ZruÅ”iÅ„ pozvĆ”nku z miestnosti", + "Remove from space": "OdstrĆ”niÅ„ z priestoru", + "Disinvite from space": "ZruÅ”iÅ„ pozvĆ”nku z priestoru" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 08729ea7387..16c70db0f6c 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3820,5 +3820,17 @@ "%(count)s participants|other": "%(count)s pjesĆ«marrĆ«s", "New video room": "DhomĆ« e re me video", "New room": "DhomĆ« e re", - "Video rooms (under active development)": "Dhoma me video (nĆ«n zhvillim aktiv)" + "Video rooms (under active development)": "Dhoma me video (nĆ«n zhvillim aktiv)", + "Give feedback": "Jepni pĆ«rshtypjet", + "Threads are a beta feature": "Rrjedhat janĆ« njĆ« veƧori beta", + "Tip: Use \"Reply in thread\" when hovering over a message.": "NdihmĆ«z: PĆ«rdorni ā€œPĆ«rgjigjuni nĆ« rrjedhĆ«ā€, teksa kaloni kursorin sipĆ«r njĆ« mesazhi.", + "Threads help keep your conversations on-topic and easy to track.": "Rrjedhat ndihmojnĆ« qĆ« tĆ« mbahen bisedat tuaja brenda temĆ«s dhe tĆ« ndiqen kollaj.", + "%(featureName)s Beta feedback": "PĆ«rshtypje pĆ«r %(featureName)s Beta", + "Beta feature. Click to learn more.": "VeƧori nĆ« version beta. Klikoni pĆ«r tĆ« mĆ«suar mĆ« tepĆ«r.", + "Beta feature": "VeƧori nĆ« version beta", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "PĆ«r ta braktisur, kthehuni te kjo faqe dhe pĆ«rdorni butonin ā€œBraktise beta-nā€.", + "Use \"Reply in thread\" when hovering over a message.": "PĆ«rdorni ā€œPĆ«rgjigjuni nĆ« rrjedhĆ«ā€, teksa kaloni kursorin sipĆ«r njĆ« mesazhi.", + "How can I start a thread?": "Si mund tĆ« nis njĆ« rrjedhĆ«?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Rrjedhat ndihmojnĆ« qĆ« tĆ« mbahen bisedat tuaja brenda temĆ«s dhe tĆ« ndiqen kollaj. MĆ«soni mĆ« tepĆ«r.", + "Keep discussions organised with threads.": "Mbajini diskutimet tĆ« sistemuara nĆ« rrjedha." } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index ee86594d175..6351262b59e 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3703,5 +3703,29 @@ "This room or space is not accessible at this time.": "Det Ƥr rummet eller utrymmet Ƥr inte Ć„tkomligt fƶr tillfƤllet.", "Are you sure you're at the right place?": "Ƅr du sƤker pĆ„ att du Ƥr pĆ„ rƤtt stƤlle?", "This room or space does not exist.": "Det hƤr rummet eller utrymmet finns inte.", - "There's no preview, would you like to join?": "Det finns ingen fƶrhandsgranskning, vill du gĆ„ med?" + "There's no preview, would you like to join?": "Det finns ingen fƶrhandsgranskning, vill du gĆ„ med?", + "Shared a location: ": "Delade en plats: ", + "Shared their location: ": "Delade sin plats: ", + "Unable to load map": "Kunde inte ladda kartan", + "Click": "Klicka", + "Expand quotes": "Expandera citat", + "Collapse quotes": "Kollapsa citat", + "Beta feature. Click to learn more.": "Betafunktion. Klicka fƶr att lƤsa mer.", + "Beta feature": "Betafunktion", + "Can't create a thread from an event with an existing relation": "Kan inte skapa trĆ„d frĆ„n en hƤndelse med en existerande relation", + "sends hearts": "skicka hjƤrtan", + "Sends the given message with hearts": "Skickar det givna meddelandet med hjƤrtan", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Fƶr att lƤmna, Ć„tervƤnd till den hƤr sidan och anvƤnd \"LƤmna betan\"-knappen.", + "Use \"Reply in thread\" when hovering over a message.": "AnvƤnd \"Svara i trĆ„d\" nƤr du hĆ„ller pekaren ƶver ett meddelande.", + "How can I start a thread?": "Hur kan jag starta en trĆ„d?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "TrĆ„dar hjƤlper till att hĆ„lla diskussioner till Ƥmnet och lƤtta att hĆ„lla reda pĆ„. LƤs mer.", + "Keep discussions organised with threads.": "HĆ„ll diskussioner organiserade med trĆ„dar.", + "Confirm signing out these devices|one": "BekrƤfta utloggning av denna enhet", + "Confirm signing out these devices|other": "BekrƤfta utloggning av dessa enheter", + "Yes, enable": "Ja, aktivera", + "Do you want to enable threads anyway?": "Vill du aktivera trĆ„dar iallafall?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Din hemserver stƶder fƶr nƤrvarande inte trĆ„dar, sĆ„ den hƤr funktionen kan vara opĆ„litlig. Vissa trĆ„dade kanske inte Ƥr tillgƤngliga. LƤs mer.", + "Partial Support for Threads": "Delvist stƶd fƶr trĆ„dar", + "Right-click message context menu": "Kontextmeny vid hƶgerklick pĆ„ meddelande", + "Jump to the given date in the timeline": "Hoppa till det angivna datumet i tidslinjen" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 5019984872e..667242cb524 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -2375,7 +2375,7 @@ "Confirm signing out these devices": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²ŠøхіŠ“ Š· цŠøх ŠæрŠøстрŠ¾Ń—Š²", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²ŠøхіŠ“ іŠ· цŠøх ŠæрŠøстрŠ¾Ń—Š² Š·Š° Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ єŠ“ŠøŠ½Š¾Š³Š¾ Š²Ń…Š¾Š“у, щŠ¾Š± Š“Š¾Š²ŠµŃŃ‚Šø Š²Š°ŃˆŃƒ сŠæрŠ°Š²Š¶Š½Ń–ŃŃ‚ŃŒ.", "To continue, use Single Sign On to prove your identity.": "Š©Š¾Š± ŠæрŠ¾Š“Š¾Š²Š¶ŠøтŠø, сŠŗŠ¾Ń€ŠøстŠ°Š¹Ń‚ŠµŃŃ єŠ“ŠøŠ½ŠøŠ¼ Š²Ń…Š¾Š“Š¾Š¼ Š“Š»Ń ŠæіŠ“тŠ²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń Š¾ŃŠ¾Š±Šø.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²Ń…Ń–Š“ Š½Š° цŠµ ŠæрŠøстріŠ¹ Š·Š° Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ єŠ“ŠøŠ½Š¾Š³Š¾ Š²Ń…Š¾Š“у, щŠ¾Š± ŠæіŠ“тŠ²ŠµŃ€Š“ŠøтŠø Š²Š°ŃˆŃƒ Š¾ŃŠ¾Š±Ńƒ.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²ŠøхіŠ“ іŠ· цьŠ¾Š³Š¾ ŠæрŠøстрŠ¾ŃŽ Š·Š° Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ єŠ“ŠøŠ½Š¾Š³Š¾ Š²Ń…Š¾Š“у, щŠ¾Š± ŠæіŠ“тŠ²ŠµŃ€Š“ŠøтŠø Š²Š°ŃˆŃƒ Š¾ŃŠ¾Š±Ńƒ.", "Click the button below to confirm signing out these devices.|one": "ŠŠ°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ ŠŗŠ½Š¾ŠæŠŗу Š²Š½ŠøŠ·Ńƒ, щŠ¾Š± ŠæіŠ“тŠ²ŠµŃ€Š“ŠøтŠø Š²ŠøхіŠ“ іŠ· цьŠ¾Š³Š¾ ŠæрŠøстрŠ¾ŃŽ.", "This device": "Š¦ŠµŠ¹ ŠæрŠøстріŠ¹", "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Š’Š°ŃˆŃ– ŠæрŠøŠ²Š°Ń‚Š½Ń– ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń, Š·Š°Š·Š²ŠøчŠ°Š¹, Š·Š°ŃˆŠøфрŠ¾Š²Š°Š½Ń–, Š°Š»Šµ ця ŠŗіŠ¼Š½Š°Ń‚Š° ā€” Š½Ń–. Š—Š°Š·Š²ŠøчŠ°Š¹ цŠµ ŠæŠ¾Š²'яŠ·Š°Š½Š¾ Š· Š½ŠµŠæіŠ“трŠøŠ¼ŃƒŠ²Š°Š½ŠøŠ¼ ŠæрŠøстрŠ¾Ń”Š¼ Š°Š±Š¾ Š²ŠøŠŗŠ¾Ń€ŠøстŠ°Š½ŠøŠ¼ Š¼ŠµŃ‚Š¾Š“Š¾Š¼, Š½Š°ŠæрŠøŠŗŠ»Š°Š“, Š·Š°ŠæрŠ¾ŃˆŠµŠ½Š½Ń ŠµŠ»ŠµŠŗтрŠ¾Š½Š½Š¾ŃŽ ŠæŠ¾ŃˆŃ‚Š¾ŃŽ.", @@ -2428,7 +2428,7 @@ "Quick settings": "ŠØŠ²ŠøŠ“Šŗі Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń", "Home options": "ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€Šø Š“Š¾Š¼Ń–Š²ŠŗŠø", "Files": "Š¤Š°Š¹Š»Šø", - "Export chat": "Š•ŠŗсŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø чŠ°Ń‚", + "Export chat": "Š•ŠŗсŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø Š±ŠµŃŃ–Š“у", "View in room": "Š”ŠøŠ²ŠøтŠøся Š² ŠŗіŠ¼Š½Š°Ń‚Ń–", "Copy link to thread": "ŠšŠ¾ŠæіюŠ²Š°Ń‚Šø Š»Ń–Š½Šŗ трŠµŠ“у", "Thread options": "ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€Šø трŠµŠ“у", @@ -3780,5 +3780,44 @@ "%(count)s participants|other": "%(count)s учŠ°ŃŠ½ŠøŠŗіŠ²", "New video room": "ŠŠ¾Š²Š° Š²Ń–Š“ŠµŠ¾ŠŗіŠ¼Š½Š°Ń‚Š°", "New room": "ŠŠ¾Š²Š° ŠŗіŠ¼Š½Š°Ń‚Š°", - "Video rooms (under active development)": "Š’Ń–Š“ŠµŠ¾ŠŗіŠ¼Š½Š°Ń‚Šø (Š² Š°ŠŗтŠøŠ²Š½Ń–Š¹ рŠ¾Š·Ń€Š¾Š±Ń†Ń–)" + "Video rooms (under active development)": "Š’Ń–Š“ŠµŠ¾ŠŗіŠ¼Š½Š°Ń‚Šø (Š² Š°ŠŗтŠøŠ²Š½Ń–Š¹ рŠ¾Š·Ń€Š¾Š±Ń†Ń–)", + "Give feedback": "Š—Š°Š»ŠøштŠµ Š²Ń–Š“Š³ŃƒŠŗ", + "Threads are a beta feature": "Š¢Ń€ŠµŠ“Šø ā€” Š±ŠµŃ‚Š°Ń„ŃƒŠ½Šŗція", + "Tip: Use \"Reply in thread\" when hovering over a message.": "ŠŸŃ–Š“ŠŗŠ°Š·ŠŗŠ°: Š½Š°Š²ŠµŠ“іть ŠŗурсŠ¾Ń€ Š½Š° ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń Š¹ Š½Š°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ Ā«Š’Ń–Š“ŠæŠ¾Š²Ń–стŠø Š² трŠµŠ“Ā».", + "Threads help keep your conversations on-topic and easy to track.": "Š¢Ń€ŠµŠ“Šø Š“Š¾ŠæŠ¾Š¼Š°Š³Š°ŃŽŃ‚ŃŒ ŠæіŠ“трŠøŠ¼ŃƒŠ²Š°Ń‚Šø рŠ¾Š·Š¼Š¾Š²Šø Š·Š° тŠµŠ¼Š¾ŃŽ тŠ° Š·Š° Š½ŠøŠ¼Šø Š»ŠµŠ³ŠŗŠ¾ стŠµŠ¶ŠøтŠø.", + "%(featureName)s Beta feedback": "%(featureName)s ā€” Š²Ń–Š“Š³ŃƒŠŗ ŠæрŠ¾ Š±ŠµŃ‚Š°Š²ŠµŃ€ŃŃ–ŃŽ", + "Beta feature. Click to learn more.": "Š‘ŠµŃ‚Š°Ń„ŃƒŠ½Šŗція. ŠŠ°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ, щŠ¾Š± Š“іŠ·Š½Š°Ń‚Šøся Š±Ń–Š»ŃŒŃˆŠµ.", + "Beta feature": "Š‘ŠµŃ‚Š°Ń„ŃƒŠ½Šŗція", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "Š©Š¾Š± Š²ŠøŠ¹Ń‚Šø, ŠæŠ¾Š²ŠµŃ€Š½Ń–Ń‚ŃŒŃŃ Š“Š¾ цієї стŠ¾Ń€Ń–Š½ŠŗŠø Š¹ Š½Š°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ Ā«Š’ŠøŠ¹Ń‚Šø Š· Š±ŠµŃ‚Š°-тŠµŃŃ‚ŃƒŠ²Š°Š½Š½ŃĀ».", + "Use \"Reply in thread\" when hovering over a message.": "ŠŠ°Š²ŠµŠ“іть ŠŗурсŠ¾Ń€ Š½Š° ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń Š¹ Š½Š°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ Ā«Š’Ń–Š“ŠæŠ¾Š²Ń–стŠø Š² трŠµŠ“Ā».", + "How can I start a thread?": "ŠÆŠŗ стŠ²Š¾Ń€ŠøтŠø трŠµŠ“?", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Š¢Ń€ŠµŠ“Šø Š“Š¾ŠæŠ¾Š¼Š°Š³Š°ŃŽŃ‚ŃŒ рŠ¾Š·Š¼ŠµŠ¶Š¾Š²ŃƒŠ²Š°Ń‚Šø рŠ¾Š·Š¼Š¾Š²Šø Š½Š° ріŠ·Š½Ń– тŠµŠ¼Šø. Š”Ń–Š·Š½Š°Š¹Ń‚ŠµŃŃ Š±Ń–Š»ŃŒŃˆŠµ.", + "Keep discussions organised with threads.": "Š”ŠæіŠ»ŠŗуŠ¹Ń‚ŠµŃŃ Š·Š° тŠµŠ¼Š¾ŃŽ Š² трŠµŠ“Š°Ń….", + "sends hearts": "Š½Š°Š“сŠøŠ»Š°Ń” сŠµŃ€Š“ŠµŃ‡ŠŗŠ°", + "Sends the given message with hearts": "ŠŠ°Š“сŠøŠ»Š°Ń” цŠµ ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń Š· сŠµŃ€Š“ŠµŃ‡ŠŗŠ°Š¼Šø", + "Confirm signing out these devices|one": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²ŠøхіŠ“ іŠ· цьŠ¾Š³Š¾ ŠæрŠøстрŠ¾ŃŽ", + "Confirm signing out these devices|other": "ŠŸŃ–Š“тŠ²ŠµŃ€Š“ьтŠµ Š²ŠøхіŠ“ іŠ· цŠøх ŠæрŠøстрŠ¾Ń—Š²", + "Live location ended": "ŠŸŠ¾ŠŗŠ°Š· Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾ Š·Š°Š²ŠµŃ€ŃˆŠµŠ½Š¾", + "Loading live location...": "Š—Š°Š²Š°Š½Ń‚Š°Š¶ŠµŠ½Š½Ń Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾...", + "View live location": "ŠŸŠ¾ŠŗŠ°Š·ŃƒŠ²Š°Ń‚Šø Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾", + "Partial Support for Threads": "Š§Š°ŃŃ‚ŠŗŠ¾Š²Š° ŠæіŠ“трŠøŠ¼ŠŗŠ° трŠµŠ“іŠ²", + "Jump to the given date in the timeline": "ŠŸŠµŃ€ŠµŠ¹Ń‚Šø Š“Š¾ Š²ŠŗŠ°Š·Š°Š½Š¾Ń— Š“Š°Ń‚Šø Š² стрічці", + "Live location enabled": "ŠŸŠ¾ŠŗŠ°Š· Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾ Š²Š²Ń–Š¼ŠŗŠ½ŠµŠ½Š¾", + "Live location error": "ŠŸŠ¾Š¼ŠøŠ»ŠŗŠ° ŠæŠ¾ŠŗŠ°Š·Ńƒ Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾", + "Live until %(expiryTime)s": "ŠŠ°Š¶ŠøŠ²Š¾ Š“Š¾ %(expiryTime)s", + "Yes, enable": "Š¢Š°Šŗ, уŠ²Ń–Š¼ŠŗŠ½ŃƒŃ‚Šø", + "Do you want to enable threads anyway?": "Š£ŃŠµ Š¾Š“Š½Š¾ хŠ¾Ń‡ŠµŃ‚Šµ уŠ²Ń–Š¼ŠŗŠ½ŃƒŃ‚Šø трŠµŠ“Šø?", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Š’Š°Ńˆ Š“Š¾Š¼Š°ŃˆŠ½Ń–Š¹ сŠµŃ€Š²ŠµŃ€ Š½Šµ ŠæіŠ“трŠøŠ¼ŃƒŃ” трŠµŠ“Šø, тŠ¾Š¼Ńƒ ця фуŠ½Šŗція Š¼Š¾Š¶Šµ Š±ŃƒŃ‚Šø Š½ŠµŠæŠ¾Š²Š½Š¾ŃŠæрŠ°Š²Š½Š¾ŃŽ. Š”ŠµŃŠŗі трŠµŠ“Šø ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½ŃŒ Š¼Š¾Š¶ŃƒŃ‚ŃŒ Š±ŃƒŃ‚Šø Š½Šµ Š“Š¾ŃŃ‚ŃƒŠæŠ½ŠøŠ¼Šø. Š”Š¾ŠŗŠ»Š°Š“Š½Ń–ŃˆŠµ.", + "Ban from space": "Š—Š°Š±Š»Š¾ŠŗуŠ²Š°Ń‚Šø у ŠæрŠ¾ŃŃ‚Š¾Ń€Ń–", + "Unban from space": "Š Š¾Š·Š±Š»Š¾ŠŗуŠ²Š°Ń‚Šø у ŠæрŠ¾ŃŃ‚Š¾Ń€Ń–", + "Ban from room": "Š—Š°Š±Š»Š¾ŠŗуŠ²Š°Ń‚Šø Š² ŠŗіŠ¼Š½Š°Ń‚Ń–", + "Unban from room": "Š Š¾Š·Š±Š»Š¾ŠŗуŠ²Š°Ń‚Šø Š² ŠŗіŠ¼Š½Š°Ń‚Ń–", + "Right-click message context menu": "ŠŸŃ€Š°Š²Š° ŠŗŠ½Š¾ŠæŠŗŠ° Š¼Šøші ā€” ŠŗŠ¾Š½Ń‚ŠµŠŗстŠ½Šµ Š¼ŠµŠ½ŃŽ ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń", + "Use ā€œ%(replyInThread)sā€ when hovering over a message.": "Š—Š°ŃŃ‚Š¾ŃŠ¾Š²ŃƒŠ²Š°Ń‚Šø Ā«%(replyInThread)sĀ» ŠæісŠ»Ń Š½Š°Š²ŠµŠ“ŠµŠ½Š½Ń Š²ŠŗŠ°Š·Ń–Š²Š½ŠøŠŗŠ° Š½Š° ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń.", + "Tip: Use ā€œ%(replyInThread)sā€ when hovering over a message.": "ŠŸŠ¾Ń€Š°Š“Š°: Š’ŠøŠŗŠ¾Ń€ŠøстŠ¾Š²ŃƒŠ¹Ń‚Šµ Ā«%(replyInThread)sĀ» Š½Š°Š²Ń–Š²ŃˆŠø Š²ŠŗŠ°Š·Ń–Š²Š½ŠøŠŗ Š½Š° ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń.", + "Disinvite from room": "Š’Ń–Š“ŠŗŠ»ŠøŠŗŠ°Ń‚Šø Š·Š°ŠæрŠ¾ŃˆŠµŠ½Š½Ń Š“Š¾ ŠŗіŠ¼Š½Š°Ń‚Šø", + "Remove from space": "Š’ŠøŠ»ŃƒŃ‡ŠøтŠø Š· ŠæрŠ¾ŃŃ‚Š¾Ń€Ńƒ", + "Disinvite from space": "Š’Ń–Š“ŠŗŠ»ŠøŠŗŠ°Ń‚Šø Š·Š°ŠæрŠ¾ŃˆŠµŠ½Š½Ń Š“Š¾ ŠæрŠ¾ŃŃ‚Š¾Ń€Ńƒ", + "To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.": "Š©Š¾Š± Š²ŠøŠ¹Ń‚Šø, ŠæŠ¾Š²ŠµŃ€Š½Ń–Ń‚ŃŒŃŃ Š½Š° цю стŠ¾Ń€Ń–Š½Šŗу Š¹ Š½Š°Ń‚ŠøсŠ½Ń–Ń‚ŃŒ ŠŗŠ½Š¾ŠæŠŗу Ā«%(leaveTheBeta)sĀ».", + "No live locations": "ŠŸŠµŃ€ŠµŠ“Š°Š²Š°Š½Š½Ń Š¼Ń–сцŠµŠæŠµŃ€ŠµŠ±ŃƒŠ²Š°Š½Š½Ń Š½Š°Š¶ŠøŠ²Š¾ Š²Ń–Š“сутŠ½Ń–" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 507a203bb40..8bc0d27e04e 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3822,5 +3822,48 @@ "An error occurred while stopping your live location, please try again": "åœę­¢ę‚Øēš„å³ę™‚ä½ē½®ę™‚ē™¼ē”ŸéŒÆčŖ¤ļ¼Œč«‹å†č©¦äø€ę¬”", "Stop sharing and close": "åœę­¢åˆ†äŗ«äø¦é—œé–‰", "An error occured whilst sharing your live location, please try again": "分äŗ«ę‚Øēš„å³ę™‚ä½ē½®ę™‚ē™¼ē”ŸéŒÆčŖ¤ļ¼Œč«‹é‡č©¦", - "An error occured whilst sharing your live location": "分äŗ«ę‚Øēš„å³ę™‚ä½ē½®ę™‚ē™¼ē”ŸéŒÆčŖ¤" + "An error occured whilst sharing your live location": "分äŗ«ę‚Øēš„å³ę™‚ä½ē½®ę™‚ē™¼ē”ŸéŒÆčŖ¤", + "Give feedback": "ēµ¦äŗˆå›žé„‹", + "Threads are a beta feature": "čØŽč«–äø²ę˜Æęø¬č©¦ē‰ˆåŠŸčƒ½", + "Tip: Use \"Reply in thread\" when hovering over a message.": "ē§˜čØ£ļ¼šåœØęøøęØ™åœę–¼č؊ęÆ之äøŠę™‚ä½æē”Ø怌åœØčØŽč«–äø²äø­å›žč¦†ć€ć€‚", + "Threads help keep your conversations on-topic and easy to track.": "čØŽč«–äø²åÆ讓ę‚Øēš„å°č©±äøé›¢é”Œäø”ę˜“ę–¼čæ½č¹¤ć€‚", + "Create room": "å»ŗē«‹čŠå¤©å®¤", + "Create video room": "å»ŗē«‹č¦–čØŠčŠå¤©å®¤", + "Create a video room": "å»ŗē«‹č¦–čØŠčŠå¤©å®¤", + "%(featureName)s Beta feedback": "%(featureName)s ęø¬č©¦ē‰ˆå›žé„‹", + "Beta feature. Click to learn more.": "ęø¬č©¦ē‰ˆåŠŸčƒ½ć€‚é»žę“Šä»„å–å¾—ę›“å¤šč³‡čØŠć€‚", + "Beta feature": "ęø¬č©¦ē‰ˆåŠŸčƒ½", + "%(count)s participants|one": "1 å€‹åƒčˆ‡č€…", + "%(count)s participants|other": "%(count)s å€‹åƒčˆ‡č€…", + "New video room": "ꖰ視čØŠčŠå¤©å®¤", + "New room": "ę–°čŠå¤©å®¤", + "Video rooms (under active development)": "視čØŠčŠå¤©å®¤ļ¼ˆä»åœØē©ę„µé–‹ē™¼äø­ļ¼‰", + "To leave, return to this page and use the ā€œLeave the betaā€ button.": "č‹„č¦é›¢é–‹ļ¼Œčæ”å›žę­¤é é¢äø¦ä½æē”Øć€Œé›¢é–‹ęø¬č©¦ē‰ˆć€ęŒ‰éˆ•ć€‚", + "Use \"Reply in thread\" when hovering over a message.": "åœØęøøęØ™åœę–¼č؊ęÆ之äøŠę™‚ä½æē”Ø怌åœØčØŽč«–äø²äø­å›žč¦†ć€ć€‚", + "How can I start a thread?": "ęˆ‘č¦å¦‚ä½•å•Ÿå‹•čØŽč«–äø²ļ¼Ÿ", + "Threads help keep conversations on-topic and easy to track. Learn more.": "čØŽč«–äø²č®“å°č©±äøé›¢é”Œäø”ę˜“ę–¼čæ½č¹¤ć€‚å–å¾—ę›“å¤šč³‡čØŠć€‚", + "Keep discussions organised with threads.": "透過čØŽč«–äø²č®“čØŽč«–äæęŒęœ‰ę¢äøē“Šć€‚", + "sends hearts": "å‚³é€ę„›åæƒ", + "Sends the given message with hearts": "與ꄛåæƒäø€åŒå‚³é€ęŒ‡å®šēš„č؊ęÆ", + "Live location ended": "å³ę™‚ä½ē½®å·²ēµęŸ", + "Loading live location...": "ę­£åœØč¼‰å…„å³ę™‚ä½ē½®ā€¦ā€¦", + "View live location": "ęŖ¢č¦–å³ę™‚ä½ē½®", + "Confirm signing out these devices|one": "ē¢ŗčŖē™»å‡ŗę­¤č£ē½®", + "Confirm signing out these devices|other": "ē¢ŗčŖē™»å‡ŗ這äŗ›č£ē½®", + "Live location enabled": "å³ę™‚ä½ē½®å·²å•Ÿē”Ø", + "Live location error": "å³ę™‚ä½ē½®éŒÆčŖ¤", + "Live until %(expiryTime)s": "å³ę™‚åˆ†äŗ«ē›“到 %(expiryTime)s", + "Ban from room": "å¾žčŠå¤©å®¤å°éŽ–", + "Unban from room": "å¾žčŠå¤©å®¤å–ę¶ˆå°éŽ–", + "Ban from space": "從ē©ŗ間封鎖", + "Unban from space": "從ē©ŗé–“å–ę¶ˆå°éŽ–", + "Yes, enable": "ę˜Æēš„ļ¼Œč«‹å•Ÿē”Ø", + "Do you want to enable threads anyway?": "ę‚Øē„”č«–å¦‚ä½•éƒ½ęƒ³č¦å•Ÿē”ØčØŽč«–äø²å—Žļ¼Ÿ", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "ę‚Øēš„家ä¼ŗ꜍å™Øē›®å‰äøę”Æę“čØŽč«–äø²ļ¼Œę‰€ä»„ę­¤åŠŸčƒ½åÆčƒ½äøåÆ靠怂éƒØ份čØŽč«–äø²č؊ęÆåÆčƒ½ē„”ę³•åÆ靠地ä½æē”Øć€‚å–å¾—ę›“å¤šč³‡čØŠć€‚", + "Partial Support for Threads": "éƒØ份ę”Æę“čØŽč«–äø²", + "Jump to the given date in the timeline": "č·³č‡³ę™‚é–“č»øäø­ęŒ‡å®šēš„ę—„ꜟ", + "Disinvite from room": "å¾žčŠå¤©å®¤å–ę¶ˆé‚€č«‹", + "Remove from space": "從ē©ŗ間ē§»é™¤", + "Disinvite from space": "從ē©ŗé–“å–ę¶ˆé‚€č«‹", + "Right-click message context menu": "å³éµé»žę“Šč؊ęÆęƒ…å¢ƒéø單" } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 559558cee88..d9bd0817a4f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -133,7 +133,7 @@ export interface IBaseSetting { }; // Optional description which will be shown as microCopy under SettingsFlags - description?: string; + description?: string | (() => ReactNode); // The supported levels are required. Preferably, use the preset arrays // at the top of this file to define this rather than a custom array. @@ -242,9 +242,17 @@ export const SETTINGS: {[setting: string]: ISetting} = { disclaimer: () => SdkConfig.get().bug_report_endpoint_url && <>

    { _t("How can I start a thread?") }

    -

    { _t("Use \"Reply in thread\" when hovering over a message.") }

    +

    + { _t("Use ā€œ%(replyInThread)sā€ when hovering over a message.", { + replyInThread: _t("Reply in thread"), + }) } +

    { _t("How can I leave the beta?") }

    -

    { _t("To leave, return to this page and use the ā€œLeave the betaā€ button.") }

    +

    + { _t("To leave, return to this page and use the ā€œ%(leaveTheBeta)sā€ button.", { + leaveTheBeta: _t("Leave the beta"), + }) } +

    , feedbackLabel: "thread-feedback", feedbackSubheading: _td("Thank you for trying the beta, " + @@ -603,6 +611,16 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Automatically replace plain text Emoji'), default: false, }, + "MessageComposerInput.useMarkdown": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Enable Markdown'), + description: () => _t( + "Start messages with /plain to send without markdown and /md to send with.", + {}, + { code: (sub) => { sub } }, + ), + default: true, + }, "VideoView.flipVideoHorizontally": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Mirror local video feed'), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 95ea0e6993e..19a419afb7f 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -16,6 +16,7 @@ limitations under the License. */ import { logger } from "matrix-js-sdk/src/logger"; +import { ReactNode } from "react"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; @@ -257,9 +258,11 @@ export default class SettingsStore { * @param {string} settingName The setting to look up. * @return {String} The description for the setting, or null if not found. */ - public static getDescription(settingName: string) { - if (!SETTINGS[settingName]?.description) return null; - return _t(SETTINGS[settingName].description); + public static getDescription(settingName: string): string | ReactNode { + const description = SETTINGS[settingName]?.description; + if (!description) return null; + if (typeof description !== 'string') return description(); + return _t(description); } /** diff --git a/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts new file mode 100644 index 00000000000..5d64009b6fe --- /dev/null +++ b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts @@ -0,0 +1,87 @@ +/* +Copyright 2019 - 2022 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 SettingsHandler from "./SettingsHandler"; + +/** + * Abstract settings handler wrapping around localStorage making getValue calls cheaper + * by caching the values and listening for localStorage updates from other tabs. + */ +export default abstract class AbstractLocalStorageSettingsHandler extends SettingsHandler { + private itemCache = new Map(); + private objectCache = new Map(); + + protected constructor() { + super(); + + // Listen for storage changes from other tabs to bust the cache + window.addEventListener("storage", (e: StorageEvent) => { + if (e.key === null) { + this.itemCache.clear(); + this.objectCache.clear(); + } else { + this.itemCache.delete(e.key); + this.objectCache.delete(e.key); + } + }); + } + + protected getItem(key: string): any { + if (!this.itemCache.has(key)) { + const value = localStorage.getItem(key); + this.itemCache.set(key, value); + return value; + } + + return this.itemCache.get(key); + } + + protected getObject(key: string): T | null { + if (!this.objectCache.has(key)) { + try { + const value = JSON.parse(localStorage.getItem(key)); + this.objectCache.set(key, value); + return value; + } catch (err) { + console.error("Failed to parse localStorage object", err); + return null; + } + } + + return this.objectCache.get(key) as T; + } + + protected setItem(key: string, value: any): void { + this.itemCache.set(key, value); + localStorage.setItem(key, value); + } + + protected setObject(key: string, value: object): void { + this.objectCache.set(key, value); + localStorage.setItem(key, JSON.stringify(value)); + } + + // handles both items and objects + protected removeItem(key: string): void { + localStorage.removeItem(key); + this.itemCache.delete(key); + this.objectCache.delete(key); + } + + public isSupported(): boolean { + return localStorage !== undefined && localStorage !== null; + } +} diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts index 7d2fbaf236a..25c75c67a19 100644 --- a/src/settings/handlers/DeviceSettingsHandler.ts +++ b/src/settings/handlers/DeviceSettingsHandler.ts @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2022 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. @@ -16,17 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import { SettingLevel } from "../SettingLevel"; import { CallbackFn, WatchManager } from "../WatchManager"; +import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; /** * Gets and sets settings at the "device" level for the current device. * This handler does not make use of the roomId parameter. This handler * will special-case features to support legacy settings. */ -export default class DeviceSettingsHandler extends SettingsHandler { +export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { /** * Creates a new device settings handler * @param {string[]} featureNames The names of known features. @@ -43,15 +43,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications if (settingName === "notificationsEnabled") { - const value = localStorage.getItem("notifications_enabled"); + const value = this.getItem("notifications_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } else if (settingName === "notificationBodyEnabled") { - const value = localStorage.getItem("notifications_body_enabled"); + const value = this.getItem("notifications_body_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } else if (settingName === "audioNotificationsEnabled") { - const value = localStorage.getItem("audio_notifications_enabled"); + const value = this.getItem("audio_notifications_enabled"); if (typeof(value) === "string") return value === "true"; return null; // wrong type or otherwise not set } @@ -68,15 +68,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications if (settingName === "notificationsEnabled") { - localStorage.setItem("notifications_enabled", newValue); + this.setItem("notifications_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } else if (settingName === "notificationBodyEnabled") { - localStorage.setItem("notifications_body_enabled", newValue); + this.setItem("notifications_body_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } else if (settingName === "audioNotificationsEnabled") { - localStorage.setItem("audio_notifications_enabled", newValue); + this.setItem("audio_notifications_enabled", newValue); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); } @@ -87,7 +87,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { delete settings["useIRCLayout"]; settings["layout"] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(settings)); + this.setObject("mx_local_settings", settings); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); @@ -95,7 +95,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { const settings = this.getSettings() || {}; settings[settingName] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(settings)); + this.setObject("mx_local_settings", settings); this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); return Promise.resolve(); @@ -105,10 +105,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { return true; // It's their device, so they should be able to } - public isSupported(): boolean { - return localStorage !== undefined && localStorage !== null; - } - public watchSetting(settingName: string, roomId: string, cb: CallbackFn) { this.watchers.watchSetting(settingName, roomId, cb); } @@ -118,9 +114,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } private getSettings(): any { // TODO: [TS] Type return - const value = localStorage.getItem("mx_local_settings"); - if (!value) return null; - return JSON.parse(value); + return this.getObject("mx_local_settings"); } // Note: features intentionally don't use the same key as settings to avoid conflicts @@ -132,7 +126,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { return false; } - const value = localStorage.getItem("mx_labs_feature_" + featureName); + const value = this.getItem("mx_labs_feature_" + featureName); if (value === "true") return true; if (value === "false") return false; // Try to read the next config level for the feature. @@ -140,7 +134,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } private writeFeature(featureName: string, enabled: boolean | null) { - localStorage.setItem("mx_labs_feature_" + featureName, `${enabled}`); + this.setItem("mx_labs_feature_" + featureName, `${enabled}`); this.watchers.notifyUpdate(featureName, null, SettingLevel.DEVICE, enabled); } } diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.ts b/src/settings/handlers/RoomDeviceSettingsHandler.ts index 47fcecdfacd..c1d1b57e9b6 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.ts +++ b/src/settings/handlers/RoomDeviceSettingsHandler.ts @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 - 2022 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. @@ -15,15 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import { SettingLevel } from "../SettingLevel"; import { WatchManager } from "../WatchManager"; +import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; /** * Gets and sets settings at the "room-device" level for the current device in a particular * room. */ -export default class RoomDeviceSettingsHandler extends SettingsHandler { +export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { constructor(public readonly watchers: WatchManager) { super(); } @@ -32,7 +32,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { // Special case blacklist setting to use legacy values if (settingName === "blacklistUnverifiedDevices") { const value = this.read("mx_local_settings"); - if (value && value['blacklistUnverifiedDevicesPerRoom']) { + if (value?.['blacklistUnverifiedDevicesPerRoom']) { return value['blacklistUnverifiedDevicesPerRoom'][roomId]; } } @@ -49,16 +49,15 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { if (!value) value = {}; if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; - localStorage.setItem("mx_local_settings", JSON.stringify(value)); + this.setObject("mx_local_settings", value); this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); return Promise.resolve(); } if (newValue === null) { - localStorage.removeItem(this.getKey(settingName, roomId)); + this.removeItem(this.getKey(settingName, roomId)); } else { - newValue = JSON.stringify({ value: newValue }); - localStorage.setItem(this.getKey(settingName, roomId), newValue); + this.setObject(this.getKey(settingName, roomId), { value: newValue }); } this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); @@ -69,14 +68,8 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { return true; // It's their device, so they should be able to } - public isSupported(): boolean { - return localStorage !== undefined && localStorage !== null; - } - private read(key: string): any { - const rawValue = localStorage.getItem(key); - if (!rawValue) return null; - return JSON.parse(rawValue); + return this.getItem(key); } private getKey(settingName: string, roomId: string): string { diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 1d505b9b22b..bb4ddc4fcbf 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -34,6 +34,7 @@ import { import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { ActiveRoomChangedPayload } from "../../dispatcher/payloads/ActiveRoomChangedPayload"; +import { RoomViewStore } from "../RoomViewStore"; /** * A class for tracking the state of the right panel between layouts and @@ -55,6 +56,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } protected async onReady(): Promise { + this.viewedRoomId = RoomViewStore.instance.getRoomId(); this.matrixClient.on(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate); this.loadCacheFromSettings(); this.emitAndUpdateSettings(); @@ -348,6 +350,7 @@ export default class RightPanelStore extends ReadyWatchingStore { }; private handleViewedRoomChange(oldRoomId: Optional, newRoomId: Optional) { + if (!this.mxClient) return; // not ready, onReady will handle the first room this.viewedRoomId = newRoomId; // load values from byRoomCache with the viewedRoomId. this.loadCacheFromSettings(); diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 3aef3eb369e..51ab6cbed06 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -19,6 +19,7 @@ import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from " import { MatrixClient } from 'matrix-js-sdk/src/client'; import { logger } from 'matrix-js-sdk/src/logger'; import { M_POLL_START } from "matrix-events-sdk"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; @@ -262,3 +263,21 @@ export function editEvent( export function canCancel(status: EventStatus): boolean { return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING; } + +export const isLocationEvent = (event: MatrixEvent): boolean => { + const eventType = event.getType(); + return ( + M_LOCATION.matches(eventType) || + ( + eventType === EventType.RoomMessage && + M_LOCATION.matches(event.getContent().msgtype) + ) + ); +}; + +export function canForward(event: MatrixEvent): boolean { + return !( + isLocationEvent(event) || + M_POLL_START.matches(event.getType()) + ); +} diff --git a/src/utils/beacon/bounds.ts b/src/utils/beacon/bounds.ts new file mode 100644 index 00000000000..43c063b1c55 --- /dev/null +++ b/src/utils/beacon/bounds.ts @@ -0,0 +1,56 @@ +/* +Copyright 2022 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 { Beacon } from "matrix-js-sdk/src/matrix"; + +import { parseGeoUri } from "../location"; + +export type Bounds = { + north: number; + east: number; + west: number; + south: number; +}; + +/** + * Get the geo bounds of given list of beacons + * + * Latitude: + * equator: 0, North pole: 90, South pole -90 + * Longitude: + * Prime Meridian (Greenwich): 0 + * east of Greenwich has a positive longitude, max 180 + * west of Greenwich has a negative longitude, min -180 + */ +export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => { + const coords = beacons.filter(beacon => !!beacon.latestLocationState) + .map(beacon => parseGeoUri(beacon.latestLocationState.uri)); + + if (!coords.length) { + return; + } + + // sort descending + const sortedByLat = [...coords].sort((left, right) => right.latitude - left.latitude); + const sortedByLong = [...coords].sort((left, right) => right.longitude - left.longitude); + + return { + north: sortedByLat[0].latitude, + south: sortedByLat[sortedByLat.length - 1].latitude, + east: sortedByLong[0].longitude, + west: sortedByLong[sortedByLong.length -1].longitude, + }; +}; diff --git a/src/utils/colour.ts b/src/utils/colour.ts index 10c18dbfe76..96eabd4eb40 100644 --- a/src/utils/colour.ts +++ b/src/utils/colour.ts @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { split } from 'lodash'; + export function textToHtmlRainbow(str: string): string { const frequency = (2 * Math.PI) / str.length; - return Array.from(str) + return split(str, '') .map((c, i) => { if (c === " ") { return c; diff --git a/src/utils/humanize.ts b/src/utils/humanize.ts index 978d17424b3..47e2d83e8a0 100644 --- a/src/utils/humanize.ts +++ b/src/utils/humanize.ts @@ -30,7 +30,7 @@ const HOURS_1_DAY = 26; * @returns {string} The humanized time. */ export function humanizeTime(timeMillis: number): string { - const now = (new Date()).getTime(); + const now = Date.now(); let msAgo = now - timeMillis; const minutes = Math.abs(Math.ceil(msAgo / 60000)); const hours = Math.ceil(minutes / 60); diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index f3f6826c15c..13a298d5a56 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -17,10 +17,12 @@ limitations under the License. import { advanceDateAndTime, stubClient } from "./test-utils"; import { MatrixClientPeg as peg } from "../src/MatrixClientPeg"; +jest.useFakeTimers(); + describe("MatrixClientPeg", () => { afterEach(() => { localStorage.clear(); - advanceDateAndTime(0); + jest.restoreAllMocks(); }); it("setJustRegisteredUserId", () => { @@ -32,7 +34,7 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(true); expect(peg.userRegisteredWithinLastHours(24)).toBe(true); - advanceDateAndTime(1 * 60 * 60 * 1000); + advanceDateAndTime(1 * 60 * 60 * 1000 + 1); expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(true); @@ -50,7 +52,7 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(false); - advanceDateAndTime(1 * 60 * 60 * 1000); + advanceDateAndTime(1 * 60 * 60 * 1000 + 1); expect(peg.userRegisteredWithinLastHours(0)).toBe(false); expect(peg.userRegisteredWithinLastHours(1)).toBe(false); expect(peg.userRegisteredWithinLastHours(24)).toBe(false); diff --git a/test/components/views/beacon/BeaconListItem-test.tsx b/test/components/views/beacon/BeaconListItem-test.tsx new file mode 100644 index 00000000000..e7e9fbb7265 --- /dev/null +++ b/test/components/views/beacon/BeaconListItem-test.tsx @@ -0,0 +1,173 @@ +/* +Copyright 2022 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 { mount } from 'enzyme'; +import { + Beacon, + RoomMember, + MatrixEvent, +} from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; +import { act } from 'react-dom/test-utils'; + +import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem'; +import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithBeacons, +} from '../../../test-utils'; + +describe('', () => { + // 14.03.2022 16:15 + const now = 1647270879403; + // go back in time to create beacons and locations in the past + jest.spyOn(global.Date, 'now').mockReturnValue(now - 600000); + const roomId = '!room:server'; + const aliceId = '@alice:server'; + + const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(aliceId), + getRoom: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + }); + + const aliceBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true }, + '$alice-room1-1', + ); + const alicePinBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true, assetType: LocationAssetType.Pin, description: "Alice's car" }, + '$alice-room1-1', + ); + const pinBeaconWithoutDescription = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true, assetType: LocationAssetType.Pin }, + '$alice-room1-1', + ); + + const aliceLocation1 = makeBeaconEvent( + aliceId, { beaconInfoId: aliceBeaconEvent.getId(), geoUri: 'geo:51,41', timestamp: now - 1 }, + ); + const aliceLocation2 = makeBeaconEvent( + aliceId, { beaconInfoId: aliceBeaconEvent.getId(), geoUri: 'geo:52,42', timestamp: now - 500000 }, + ); + + const defaultProps = { + beacon: new Beacon(aliceBeaconEvent), + }; + + const getComponent = (props = {}) => + mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); + + const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => { + const beacons = makeRoomWithBeacons(roomId, mockClient, beaconInfoEvents, locationEvents); + + const member = new RoomMember(roomId, aliceId); + member.name = `Alice`; + const room = mockClient.getRoom(roomId); + jest.spyOn(room, 'getMember').mockReturnValue(member); + + return beacons; + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Date, 'now').mockReturnValue(now); + }); + + it('renders null when beacon is not live', () => { + const notLiveBeacon = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: false }, + ); + const [beacon] = setupRoomWithBeacons([notLiveBeacon]); + const component = getComponent({ beacon }); + expect(component.html()).toBeNull(); + }); + + it('renders null when beacon has no location', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent]); + const component = getComponent({ beacon }); + expect(component.html()).toBeNull(); + }); + + describe('when a beacon is live and has locations', () => { + it('renders beacon info', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.html()).toMatchSnapshot(); + }); + + describe('non-self beacons', () => { + it('uses beacon description as beacon name', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual("Alice's car"); + }); + + it('uses beacon owner mxid as beacon name for a beacon without description', () => { + const [beacon] = setupRoomWithBeacons([pinBeaconWithoutDescription], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual(aliceId); + }); + + it('renders location icon', () => { + const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('StyledLiveBeaconIcon').length).toBeTruthy(); + }); + }); + + describe('self locations', () => { + it('renders beacon owner avatar', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('MemberAvatar').length).toBeTruthy(); + }); + + it('uses beacon owner name as beacon name', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]); + const component = getComponent({ beacon }); + expect(component.find('BeaconStatus').props().label).toEqual('Alice'); + }); + }); + + describe('on location updates', () => { + it('updates last updated time on location updated', () => { + const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation2]); + const component = getComponent({ beacon }); + + expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated 9 minutes ago'); + + // update to a newer location + act(() => { + beacon.addLocations([aliceLocation1]); + component.setProps({}); + }); + + expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago'); + }); + }); + }); +}); diff --git a/test/components/views/beacon/BeaconMarker-test.tsx b/test/components/views/beacon/BeaconMarker-test.tsx index 5b730ff4387..efc9c7d22c5 100644 --- a/test/components/views/beacon/BeaconMarker-test.tsx +++ b/test/components/views/beacon/BeaconMarker-test.tsx @@ -22,12 +22,18 @@ import { Beacon, Room, RoomMember, + MatrixEvent, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; import BeaconMarker from '../../../../src/components/views/beacon/BeaconMarker'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithStateEvents, +} from '../../../test-utils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; describe('', () => { @@ -53,13 +59,9 @@ describe('', () => { // make fresh rooms every time // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); + const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { + const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient }); jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); - mockClient.getRoom.mockReturnValue(room1); - return room1; }; @@ -97,21 +99,21 @@ describe('', () => { }); it('renders nothing when beacon is not live', () => { - const room = makeRoomWithStateEvents([notLiveEvent]); + const room = setupRoom([notLiveEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent)); const component = getComponent({ beacon }); expect(component.html()).toBe(null); }); it('renders nothing when beacon has no location', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); const component = getComponent({ beacon }); expect(component.html()).toBe(null); }); it('renders marker when beacon has location', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent({ beacon }); @@ -119,7 +121,7 @@ describe('', () => { }); it('updates with new locations', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent({ beacon }); diff --git a/test/components/views/beacon/BeaconStatus-test.tsx b/test/components/views/beacon/BeaconStatus-test.tsx index db4153defac..68a6a34a30f 100644 --- a/test/components/views/beacon/BeaconStatus-test.tsx +++ b/test/components/views/beacon/BeaconStatus-test.tsx @@ -26,6 +26,7 @@ describe('', () => { const defaultProps = { displayStatus: BeaconDisplayStatus.Loading, label: 'test label', + withIcon: true, }; const getComponent = (props = {}) => mount(); @@ -40,6 +41,11 @@ describe('', () => { expect(component).toMatchSnapshot(); }); + it('renders without icon', () => { + const component = getComponent({ withIcon: false, displayStatus: BeaconDisplayStatus.Stopped }); + expect(component.find('StyledLiveBeaconIcon').length).toBeFalsy(); + }); + describe('active state', () => { it('renders without children', () => { // mock for stable snapshot diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 70dddd2710d..7adfbf86c8b 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -19,6 +19,7 @@ import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { MatrixClient, + MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier, @@ -26,9 +27,11 @@ import { import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; import { + findByTestId, getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, + makeRoomWithStateEvents, } from '../../../test-utils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; @@ -54,12 +57,9 @@ describe('', () => { // make fresh rooms every time // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); + const setupRoom = (stateEvents: MatrixEvent[] = []): Room => { + const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient }); jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); - mockClient.getRoom.mockReturnValue(room1); return room1; }; @@ -84,7 +84,7 @@ describe('', () => { mount(); it('renders a map with markers', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent(); @@ -96,7 +96,7 @@ describe('', () => { }); it('updates markers on changes to beacons', () => { - const room = makeRoomWithStateEvents([defaultEvent]); + const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon.addLocations([location1]); const component = getComponent(); @@ -118,4 +118,76 @@ describe('', () => { // two markers now! expect(component.find('BeaconMarker').length).toEqual(2); }); + + it('renders a fallback when no live beacons remain', () => { + const onFinished = jest.fn(); + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent({ onFinished }); + expect(component.find('BeaconMarker').length).toEqual(1); + + // this will replace the defaultEvent + // leading to no more live beacons + const anotherBeaconEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: false }, + '$bob-room1-1', + ); + + act(() => { + // emits RoomStateEvent.BeaconLiveness + room.currentState.setStateEvents([anotherBeaconEvent]); + }); + + component.setProps({}); + + // map placeholder + expect(findByTestId(component, 'beacon-view-dialog-map-fallback')).toMatchSnapshot(); + + act(() => { + findByTestId(component, 'beacon-view-dialog-fallback-close').at(0).simulate('click'); + }); + + expect(onFinished).toHaveBeenCalled(); + }); + + describe('sidebar', () => { + it('opens sidebar on view list button click', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + + act(() => { + findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeTruthy(); + }); + + it('closes sidebar on close button click', () => { + const room = setupRoom([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + + // open the sidebar + act(() => { + findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeTruthy(); + + // now close it + act(() => { + findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + component.setProps({}); + }); + + expect(component.find('DialogSidebar').length).toBeFalsy(); + }); + }); }); diff --git a/test/components/views/beacon/DialogSidebar-test.tsx b/test/components/views/beacon/DialogSidebar-test.tsx new file mode 100644 index 00000000000..a5a1f0e5e79 --- /dev/null +++ b/test/components/views/beacon/DialogSidebar-test.tsx @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar'; +import { findByTestId } from '../../../test-utils'; + +describe('', () => { + const defaultProps = { + beacons: [], + requestClose: jest.fn(), + }; + const getComponent = (props = {}) => + mount(); + + it('renders sidebar correctly', () => { + const component = getComponent(); + expect(component).toMatchSnapshot(); + }); + + it('closes on close button click', () => { + const requestClose = jest.fn(); + const component = getComponent({ requestClose }); + + act(() => { + findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + }); + expect(requestClose).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap new file mode 100644 index 00000000000..1518a60dba9 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index cde5fd8232c..e590cbcd9f3 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -61,6 +61,7 @@ exports[` renders marker when beacon has location 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -79,6 +80,7 @@ exports[` renders marker when beacon has location 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -111,6 +113,7 @@ exports[` renders marker when beacon has location 1`] = ` Symbol(kCapture): false, } } + useMemberColor={true} > renders marker when beacon has location 1`] = ` Symbol(kCapture): false, } } + useMemberColor={true} >
    active state renders without children 1`] = ` } displayStatus="Active" label="test label" + withIcon={true} >
    active state renders without children 1`] = `
    - test label + + test label + renders loading state 1`] = `
    renders stopped state 1`] = `
    renders a fallback when no live beacons remain 1`] = ` +
    +
    + + No live locations + + +
    + Close +
    +
    +
    +`; diff --git a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap new file mode 100644 index 00000000000..e3b6f104907 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders sidebar correctly 1`] = ` + +
    +
    + +

    + View List +

    +
    + +
    +
    +
    + +
    +
      +
    + +`; diff --git a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap index 4d8b4e76605..d34eedeb56e 100644 --- a/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/OwnBeaconStatus-test.tsx.snap @@ -9,6 +9,7 @@ exports[` renders without a beacon instance 1`] = ` displayLiveTimeRemaining={true} displayStatus="Loading" label="Live location enabled" + withIcon={true} >
    ', () => { }); }); + describe('map bounds', () => { + it('does not try to fit map bounds when no bounds provided', () => { + getComponent({ bounds: null }); + expect(mockMap.fitBounds).not.toHaveBeenCalled(); + }); + + it('fits map to bounds', () => { + const bounds = { north: 51, south: 50, east: 42, west: 41 }; + getComponent({ bounds }); + expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south], + [bounds.east, bounds.north]), { padding: 100 }); + }); + + it('handles invalid bounds', () => { + const logSpy = jest.spyOn(logger, 'error').mockImplementation(); + const bounds = { north: 'a', south: 'b', east: 42, west: 41 }; + getComponent({ bounds }); + expect(mockMap.fitBounds).not.toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith('Invalid map bounds', new Error('Invalid LngLat object: (41, NaN)')); + }); + + it('updates map bounds when bounds prop changes', () => { + const component = getComponent({ centerGeoUri: 'geo:51,42' }); + + const bounds = { north: 51, south: 50, east: 42, west: 41 }; + const bounds2 = { north: 53, south: 51, east: 45, west: 44 }; + component.setProps({ bounds }); + component.setProps({ bounds: bounds2 }); + expect(mockMap.fitBounds).toHaveBeenCalledTimes(2); + }); + }); + describe('children', () => { it('renders without children', () => { const component = getComponent({ children: null }); diff --git a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap index 41b4044c5aa..8a1910a5820 100644 --- a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap +++ b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap @@ -24,6 +24,7 @@ exports[` renders map correctly 1`] = ` "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ @@ -76,6 +77,7 @@ exports[` renders map correctly 1`] = ` "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ diff --git a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap index b2da037e221..d20c9bcd6ce 100644 --- a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap +++ b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap @@ -9,6 +9,7 @@ exports[` creates a marker on mount 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], @@ -45,6 +46,7 @@ exports[` removes marker on unmount 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], diff --git a/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap b/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap index 7f18eccc82b..0fbc9851687 100644 --- a/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap +++ b/test/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap @@ -8,6 +8,7 @@ exports[` renders buttons 1`] = ` "_eventsCount": 0, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction], "setStyle": [MockFunction], diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index 9ec5db5f2e1..5afbb05c78b 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -20,12 +20,16 @@ import { act } from 'react-dom/test-utils'; import maplibregl from 'maplibre-gl'; import { BeaconEvent, - Room, getBeaconInfoIdentifier, } from 'matrix-js-sdk/src/matrix'; import MBeaconBody from '../../../../src/components/views/messages/MBeaconBody'; -import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeRoomWithStateEvents, +} from '../../../test-utils'; import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; @@ -51,17 +55,6 @@ describe('', () => { getRoom: jest.fn(), }); - // make fresh rooms every time - // as we update room state - const makeRoomWithStateEvents = (stateEvents = []): Room => { - const room1 = new Room(roomId, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); - mockClient.getRoom.mockReturnValue(room1); - - return room1; - }; - const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, @@ -86,7 +79,6 @@ describe('', () => { }); const modalSpy = jest.spyOn(Modal, 'createTrackedDialog').mockReturnValue(undefined); - beforeEach(() => { jest.clearAllMocks(); }); @@ -97,7 +89,7 @@ describe('', () => { { isLive: false }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); @@ -109,7 +101,7 @@ describe('', () => { { isLive: true, timestamp: now - 600000, timeout: 500 }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); @@ -121,9 +113,8 @@ describe('', () => { { isLive: true, timestamp: now - 600000, timeout: 500 }, '$alice-room1-1', ); - makeRoomWithStateEvents([beaconInfoEvent]); + makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); - act(() => { component.find('.mx_MBeaconBody_map').simulate('click'); }); @@ -147,7 +138,7 @@ describe('', () => { '$alice-room1-2', ); - makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2]); + makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo1 }); // beacon1 has been superceded by beacon2 @@ -170,7 +161,7 @@ describe('', () => { '$alice-room1-2', ); - const room = makeRoomWithStateEvents([aliceBeaconInfo1]); + const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo1 }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo1)); @@ -195,7 +186,7 @@ describe('', () => { '$alice-room1-1', ); - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -228,14 +219,48 @@ describe('', () => { ); it('renders a live beacon without a location correctly', () => { - makeRoomWithStateEvents([aliceBeaconInfo]); + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); expect(component.text()).toEqual("Loading live location..."); }); it('does nothing on click when a beacon has no location', () => { - makeRoomWithStateEvents([aliceBeaconInfo]); + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('.mx_MBeaconBody_map').simulate('click'); + }); + + expect(modalSpy).not.toHaveBeenCalled(); + }); + + it('renders a live beacon with a location correctly', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + expect(component.find('Map').length).toBeTruthy; + }); + + it('opens maximised map view on click when beacon has a live location', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('Map').simulate('click'); + }); + + // opens modal + expect(modalSpy).toHaveBeenCalled(); + }); + + it('does nothing on click when a beacon has no location', () => { + makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { @@ -246,7 +271,7 @@ describe('', () => { }); it('renders a live beacon with a location correctly', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -255,7 +280,7 @@ describe('', () => { }); it('opens maximised map view on click when beacon has a live location', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -269,7 +294,7 @@ describe('', () => { }); it('updates latest location', () => { - const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); diff --git a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index e32cedfde48..2edfc8e22d3 100644 --- a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -124,6 +124,7 @@ exports[`MLocationBody without error renders map correctly 1`] = "_eventsCount": 1, "_maxListeners": undefined, "addControl": [MockFunction], + "fitBounds": [MockFunction], "removeControl": [MockFunction], "setCenter": [MockFunction] { "calls": Array [ diff --git a/test/components/views/typography/Heading-test.tsx b/test/components/views/typography/Heading-test.tsx index 7f8561bfae3..186d15ff90f 100644 --- a/test/components/views/typography/Heading-test.tsx +++ b/test/components/views/typography/Heading-test.tsx @@ -25,4 +25,8 @@ describe('', () => { it('renders h3 with correct attributes', () => { expect(getComponent({ size: 'h3' })).toMatchSnapshot(); }); + + it('renders h4 with correct attributes', () => { + expect(getComponent({ size: 'h4' })).toMatchSnapshot(); + }); }); diff --git a/test/components/views/typography/__snapshots__/Heading-test.tsx.snap b/test/components/views/typography/__snapshots__/Heading-test.tsx.snap index 592ee050e8e..d9511fd4d97 100644 --- a/test/components/views/typography/__snapshots__/Heading-test.tsx.snap +++ b/test/components/views/typography/__snapshots__/Heading-test.tsx.snap @@ -32,3 +32,14 @@ exports[` renders h3 with correct attributes 1`] = `
    `; + +exports[` renders h4 with correct attributes 1`] = ` +

    +
    + test +
    +

    +`; diff --git a/test/editor/deserialize-test.ts b/test/editor/deserialize-test.ts index 86594f78dfe..47ab6cb2f27 100644 --- a/test/editor/deserialize-test.ts +++ b/test/editor/deserialize-test.ts @@ -331,4 +331,78 @@ describe('editor/deserialize', function() { expect(parts).toMatchSnapshot(); }); }); + describe('plaintext messages', function() { + it('turns html tags back into markdown', function() { + const html = "bold and emphasized text this!"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "**bold** and _emphasized_ text [this](http://example.com/)!", + }); + }); + it('keeps backticks unescaped', () => { + const html = "this ā†’ ` is a backtick and here are 3 of them:\n```"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "this ā†’ ` is a backtick and here are 3 of them:\n```", + }); + }); + it('keeps backticks outside of code blocks', () => { + const html = "some `backticks`"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "some `backticks`", + }); + }); + it('keeps backslashes', () => { + const html = "C:\\My Documents"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "C:\\My Documents", + }); + }); + it('keeps asterisks', () => { + const html = "*hello*"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "*hello*", + }); + }); + it('keeps underscores', () => { + const html = "__emphasis__"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "__emphasis__", + }); + }); + it('keeps square brackets', () => { + const html = "[not an actual link](https://example.org)"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "[not an actual link](https://example.org)", + }); + }); + it('escapes angle brackets', () => { + const html = "> <del>no formatting here</del>"; + const parts = normalize(parseEvent(htmlMessage(html), createPartCreator(), { shouldEscape: false })); + expect(parts.length).toBe(1); + expect(parts[0]).toStrictEqual({ + type: "plain", + text: "> no formatting here", + }); + }); + }); }); diff --git a/test/editor/operations-test.ts b/test/editor/operations-test.ts index b9ab4cc4e85..3e4de224179 100644 --- a/test/editor/operations-test.ts +++ b/test/editor/operations-test.ts @@ -20,8 +20,10 @@ import { toggleInlineFormat, selectRangeOfWordAtCaret, formatRange, + formatRangeAsCode, } from "../../src/editor/operations"; import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar"; +import { longestBacktickSequence } from '../../src/editor/deserialize'; const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" }; @@ -43,6 +45,89 @@ describe('editor/operations: formatting operations', () => { expect(model.serializeParts()).toEqual([{ "text": "hello _world_!", "type": "plain" }]); }); + describe('escape backticks', () => { + it('works for escaping backticks in between texts', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hello ` world!"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.positionForOffset(13, false)); // hello ` world + + expect(range.parts[0].text.trim().includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text.trim())).toBe(1); + expect(model.serializeParts()).toEqual([{ "text": "hello ` world!", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "``hello ` world``!", "type": "plain" }]); + }); + + it('escapes longer backticks in between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hello```world"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hello```world + + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "hello```world", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "````hello```world````", "type": "plain" }]); + }); + + it('escapes non-consecutive with varying length backticks in between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hell```o`w`o``rld"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hell```o`w`o``rld + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]); + }); + + it('untoggles correctly if its already formatted', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("```hello``world```"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hello``world + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(3); + expect(model.serializeParts()).toEqual([{ "text": "```hello``world```", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "hello``world", "type": "plain" }]); + }); + it('untoggles correctly it contains varying length of backticks between text', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("````hell```o`w`o``rld````"), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), + model.getPositionAtEnd()); // hell```o`w`o``rld + expect(range.parts[0].text.includes("`")).toBeTruthy(); + expect(longestBacktickSequence(range.parts[0].text)).toBe(4); + expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]); + formatRangeAsCode(range); + expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]); + }); + }); + it('works for parts of words', () => { const renderer = createRenderer(); const pc = createPartCreator(); diff --git a/test/editor/serialize-test.ts b/test/editor/serialize-test.ts index 40f95e03773..d9482859015 100644 --- a/test/editor/serialize-test.ts +++ b/test/editor/serialize-test.ts @@ -19,58 +19,80 @@ import { htmlSerializeIfNeeded } from "../../src/editor/serialize"; import { createPartCreator } from "./mock"; describe('editor/serialize', function() { - it('user pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Alice"); + describe('with markdown', function() { + it('user pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Alice"); + }); + it('room pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.roomPill("#room:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("#room:hs.tld"); + }); + it('@room pill turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.atRoomPill("@room")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBeFalsy(); + }); + it('any markdown turns message into html', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("*hello* world")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("hello world"); + }); + it('displaynames ending in a backslash work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname\\"); + }); + it('displaynames containing an opening square bracket work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname[[", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname[["); + }); + it('displaynames containing a closing square bracket work', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Displayname]", "@user:server")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe("Displayname]"); + }); + it('escaped markdown should not retain backslashes', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world'); + }); + it('escaped markdown should convert HTML entities', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world < hey world!'); + }); }); - it('room pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.roomPill("#room:hs.tld")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("#room:hs.tld"); - }); - it('@room pill turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.atRoomPill("@room")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBeFalsy(); - }); - it('any markdown turns message into html', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain("*hello* world")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("hello world"); - }); - it('displaynames ending in a backslash work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname\\"); - }); - it('displaynames containing an opening square bracket work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname[[", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname[["); - }); - it('displaynames containing a closing square bracket work', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.userPill("Displayname]", "@user:server")], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe("Displayname]"); - }); - it('escaped markdown should not retain backslashes', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe('*hello* world'); - }); - it('escaped markdown should convert HTML entities', function() { - const pc = createPartCreator(); - const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); - const html = htmlSerializeIfNeeded(model, {}); - expect(html).toBe('*hello* world < hey world!'); + describe('with plaintext', function() { + it('markdown remains plaintext', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("*hello* world")], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe("*hello* world"); + }); + it('markdown should retain backslashes', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world')], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe('\\*hello\\* world'); + }); + it('markdown should convert HTML entities', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')], pc); + const html = htmlSerializeIfNeeded(model, { useMarkdown: false }); + expect(html).toBe('\\*hello\\* world < hey world!'); + }); }); }); diff --git a/test/end-to-end-tests/src/usecases/threads.ts b/test/end-to-end-tests/src/usecases/threads.ts index 49870815a6d..4baaa49ce94 100644 --- a/test/end-to-end-tests/src/usecases/threads.ts +++ b/test/end-to-end-tests/src/usecases/threads.ts @@ -92,7 +92,7 @@ export async function redactThreadMessage(session: ElementSession): Promise, + beaconInfoEvents: MatrixEvent[], + locationEvents?: MatrixEvent[], +): Beacon[] => { + const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient }); + const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event))); + if (locationEvents) { + beacons.forEach(beacon => beacon.addLocations(locationEvents)); + } + return beacons; +}; diff --git a/test/test-utils/room.ts b/test/test-utils/room.ts index 022f13e6c1a..b9224e38710 100644 --- a/test/test-utils/room.ts +++ b/test/test-utils/room.ts @@ -14,8 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from "jest-mock"; import { + MatrixClient, + MatrixEvent, EventType, + Room, } from "matrix-js-sdk/src/matrix"; import { mkEvent } from "./test-utils"; @@ -32,3 +36,17 @@ export const makeMembershipEvent = ( ts: Date.now(), }); +/** + * Creates a room + * sets state events on the room + * Sets client getRoom to return room + * returns room + */ +export const makeRoomWithStateEvents = ( + stateEvents: MatrixEvent[] = [], + { roomId, mockClient }: { roomId: string, mockClient: MockedObject}): Room => { + const room1 = new Room(roomId, mockClient, '@user:server.org'); + room1.currentState.setStateEvents(stateEvents); + mockClient.getRoom.mockReturnValue(room1); + return room1; +}; diff --git a/test/utils/beacon/bounds-test.ts b/test/utils/beacon/bounds-test.ts new file mode 100644 index 00000000000..bd4b37234b0 --- /dev/null +++ b/test/utils/beacon/bounds-test.ts @@ -0,0 +1,95 @@ +/* +Copyright 2022 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 { Beacon } from "matrix-js-sdk/src/matrix"; + +import { Bounds, getBeaconBounds } from "../../../src/utils/beacon/bounds"; +import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils"; + +describe('getBeaconBounds()', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const makeBeaconWithLocation = (latLon: {lat: number, lon: number}) => { + const geoUri = `geo:${latLon.lat},${latLon.lon}`; + const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true })); + // @ts-ignore private prop, sets internal live property so addLocations works + beacon.checkLiveness(); + const location = makeBeaconEvent(userId, { + beaconInfoId: beacon.beaconInfoId, + geoUri, + timestamp: Date.now() + 1, + }); + beacon.addLocations([location]); + + return beacon; + }; + + const geo = { + // northern hemi + // west of greenwich + london: { lat: 51.5, lon: -0.14 }, + reykjavik: { lat: 64.08, lon: -21.82 }, + // east of greenwich + paris: { lat: 48.85, lon: 2.29 }, + // southern hemi + // east + auckland: { lat: -36.85, lon: 174.76 }, // nz + // west + lima: { lat: -12.013843, lon: -77.008388 }, // peru + }; + + const london = makeBeaconWithLocation(geo.london); + const reykjavik = makeBeaconWithLocation(geo.reykjavik); + const paris = makeBeaconWithLocation(geo.paris); + const auckland = makeBeaconWithLocation(geo.auckland); + const lima = makeBeaconWithLocation(geo.lima); + + it('should return undefined when there are no beacons', () => { + expect(getBeaconBounds([])).toBeUndefined(); + }); + + it('should return undefined when no beacons have locations', () => { + const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId)); + expect(getBeaconBounds([beacon])).toBeUndefined(); + }); + + type TestCase = [string, Beacon[], Bounds]; + it.each([ + ['one beacon', [london], + { north: geo.london.lat, south: geo.london.lat, east: geo.london.lon, west: geo.london.lon }, + ], + ['beacons in the northern hemisphere, west of meridian', + [london, reykjavik], + { north: geo.reykjavik.lat, south: geo.london.lat, east: geo.london.lon, west: geo.reykjavik.lon }, + ], + ['beacons in the northern hemisphere, both sides of meridian', + [london, reykjavik, paris], + // reykjavik northmost and westmost, paris southmost and eastmost + { north: geo.reykjavik.lat, south: geo.paris.lat, east: geo.paris.lon, west: geo.reykjavik.lon }, + ], + ['beacons in the southern hemisphere', + [auckland, lima], + // lima northmost and westmost, auckland southmost and eastmost + { north: geo.lima.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon }, + ], + ['beacons in both hemispheres', + [auckland, lima, paris], + { north: geo.paris.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon }, + ], + ])('gets correct bounds for %s', (_description, beacons, expectedBounds) => { + expect(getBeaconBounds(beacons)).toEqual(expectedBounds); + }); +}); diff --git a/test/utils/colour-test.ts b/test/utils/colour-test.ts new file mode 100644 index 00000000000..720c34e07bb --- /dev/null +++ b/test/utils/colour-test.ts @@ -0,0 +1,24 @@ +/* +Copyright 2022 Emmanuel Ezeka + +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 { textToHtmlRainbow } from "../../src/utils/colour"; + +describe("textToHtmlRainbow", () => { + it('correctly transform text to html without splitting the emoji in two', () => { + expect(textToHtmlRainbow('šŸ»')).toBe('šŸ»'); + expect(textToHtmlRainbow('šŸ•ā€šŸ¦ŗ')).toBe('šŸ•ā€šŸ¦ŗ'); + }); +});